diff --git a/applets/comic/comic.cpp b/applets/comic/comic.cpp index dd02ebae9..1732afd11 100644 --- a/applets/comic/comic.cpp +++ b/applets/comic/comic.cpp @@ -1,813 +1,817 @@ /*************************************************************************** * Copyright (C) 2007 by Tobias Koenig * * Copyright (C) 2008 by Marco Martin * * Copyright (C) 2008-2011 Matthias Fuchs * * Copyright (C) 2012 Reza Fatahilah Shah * * Copyright (C) 2015 Marco Martin * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * ***************************************************************************/ #include "comic.h" #include "comicarchivedialog.h" #include "comicarchivejob.h" #include "checknewstrips.h" #include "stripselector.h" #include "comicsaver.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "comicmodel.h" #include "comicupdater.h" Q_GLOBAL_STATIC( ComicUpdater, globalComicUpdater ) const int ComicApplet::CACHE_LIMIT = 20; ComicApplet::ComicApplet( QObject *parent, const QVariantList &args ) : Plasma::Applet( parent, args ), mProxy(nullptr), mActiveComicModel(new ActiveComicModel(parent)), mDifferentComic( true ), mShowComicUrl( false ), mShowComicAuthor( false ), mShowComicTitle( false ), mShowComicIdentifier( false ), mShowErrorPicture( true ), mArrowsOnHover( true ), mMiddleClick( true ), mCheckNewComicStripsInterval(0), - mMaxComicLimit( CACHE_LIMIT ), + mMaxComicLimit( 0 ), mCheckNewStrips(nullptr), mActionShop(nullptr), mEngine(nullptr), mSavingDir(nullptr) { setHasConfigurationInterface( true ); } void ComicApplet::init() { globalComicUpdater->init( globalConfig() ); mSavingDir = new SavingDir(config()); configChanged(); mEngine = dataEngine(QStringLiteral("comic")); mModel = new ComicModel(mEngine, QStringLiteral("providers"), mTabIdentifier, this); mProxy = new QSortFilterProxyModel( this ); mProxy->setSourceModel( mModel ); mProxy->setSortCaseSensitivity( Qt::CaseInsensitive ); mProxy->sort( 1, Qt::AscendingOrder ); - //set maximum number of cached strips per comic, -1 means that there is no limit - KConfigGroup global = globalConfig(); - const int maxComicLimit = global.readEntry( "maxComicLimit", CACHE_LIMIT ); - if (mEngine) { - mEngine->connectSource( QLatin1String( "setting_maxComicLimit:" ) + QString::number( maxComicLimit ), this ); - } - mCurrentDay = QDate::currentDate(); mDateChangedTimer = new QTimer( this ); connect( mDateChangedTimer, &QTimer::timeout, this, &ComicApplet::checkDayChanged ); mDateChangedTimer->setInterval( 5 * 60 * 1000 ); // every 5 minutes mDateChangedTimer->start(); mActionNextNewStripTab = new QAction(QIcon::fromTheme(QStringLiteral("go-next-view")), i18nc("@action comic strip", "&Next Tab with a New Strip"), this); mActionNextNewStripTab->setShortcuts( KStandardShortcut::openNew() ); actions()->addAction(QStringLiteral("next new strip"), mActionNextNewStripTab); mActions.append( mActionNextNewStripTab ); connect( mActionNextNewStripTab, &QAction::triggered, this, &ComicApplet::showNextNewStrip ); mActionGoFirst = new QAction(QIcon::fromTheme(QStringLiteral("go-first")), i18nc("@action", "Jump to &First Strip"), this); mActions.append( mActionGoFirst ); connect( mActionGoFirst, &QAction::triggered, this, &ComicApplet::slotFirstDay ); mActionGoLast = new QAction(QIcon::fromTheme(QStringLiteral("go-last")), i18nc("@action", "Jump to &Current Strip"), this); mActions.append( mActionGoLast ); connect( mActionGoLast, &QAction::triggered, this, &ComicApplet::slotCurrentDay ); mActionGoJump = new QAction(QIcon::fromTheme(QStringLiteral("go-jump")), i18nc("@action", "Jump to Strip..."), this); mActions.append( mActionGoJump ); connect( mActionGoJump, &QAction::triggered, this, &ComicApplet::slotGoJump ); mActionShop = new QAction(i18nc("@action", "Visit the Shop &Website"), this); mActionShop->setEnabled( false ); mActions.append( mActionShop ); connect( mActionShop, &QAction::triggered, this, &ComicApplet::slotShop ); mActionSaveComicAs = new QAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18nc("@action", "&Save Comic As..."), this); mActions.append( mActionSaveComicAs ); connect( mActionSaveComicAs, &QAction::triggered, this, &ComicApplet::slotSaveComicAs ); mActionCreateComicBook = new QAction(QIcon::fromTheme(QStringLiteral("application-epub+zip")), i18nc("@action", "&Create Comic Book Archive..."), this); mActions.append( mActionCreateComicBook ); connect( mActionCreateComicBook, &QAction::triggered, this, &ComicApplet::createComicBook ); mActionScaleContent = new QAction(QIcon::fromTheme(QStringLiteral("zoom-original")), i18nc("@option:check Context menu of comic image", "&Actual Size"), this); mActionScaleContent->setCheckable( true ); mActionScaleContent->setChecked( mCurrent.scaleComic() ); mActions.append( mActionScaleContent ); connect( mActionScaleContent, &QAction::triggered, this, &ComicApplet::slotScaleToContent ); mActionStorePosition = new QAction(QIcon::fromTheme(QStringLiteral("go-home")), i18nc("@option:check Context menu of comic image", "Store Current &Position"), this); mActionStorePosition->setCheckable( true ); mActionStorePosition->setChecked(mCurrent.hasStored()); mActions.append( mActionStorePosition ); connect( mActionStorePosition, &QAction::triggered, this, &ComicApplet::slotStorePosition ); //make sure that tabs etc. are displayed even if the comic strip in the first tab does not work updateView(); updateUsedComics(); changeComic( true ); } ComicApplet::~ComicApplet() { delete mSavingDir; delete m_newStuffDialog; } void ComicApplet::dataUpdated( const QString &source, const Plasma::DataEngine::Data &data ) { setBusy(false); //disconnect prefetched comic strips if (mEngine && source != mOldSource ) { mEngine->disconnectSource( source, this ); return; } setConfigurationRequired( false ); //there was an error, display information as image const bool hasError = data[QStringLiteral("Error")].toBool(); const bool errorAutoFixable = data[QStringLiteral("Error automatically fixable")].toBool(); if ( hasError ) { const QString previousIdentifierSuffix = data[QStringLiteral("Previous identifier suffix")].toString(); if (mEngine && !mShowErrorPicture && !previousIdentifierSuffix.isEmpty() ) { mEngine->disconnectSource( source, this ); updateComic( previousIdentifierSuffix ); } return; } mCurrent.setData(data); setAssociatedApplicationUrls(QList() << mCurrent.websiteUrl()); //looking at the last index, thus not mark it as new KConfigGroup cg = config(); if (!mCurrent.hasNext() && mCheckNewComicStripsInterval) { setTabHighlighted( mCurrent.id(), false ); mActionNextNewStripTab->setEnabled( isTabHighlighted(mCurrent.id()) ); } //call the slot to check if the position needs to be saved slotStorePosition(); if (mEngine) { //disconnect if there is either no error, or an error that can not be fixed automatically if ( !errorAutoFixable ) { mEngine->disconnectSource( source, this ); } //prefetch the previous and following comic for faster navigation if (mCurrent.hasNext()) { const QString prefetch = mCurrent.id() + QLatin1Char(':') + mCurrent.next(); mEngine->connectSource( prefetch, this ); } if ( mCurrent.hasPrev()) { const QString prefetch = mCurrent.id() + QLatin1Char(':') + mCurrent.prev(); mEngine->connectSource( prefetch, this ); } } updateView(); refreshComicData(); } void ComicApplet::updateView() { updateContextMenu(); } void ComicApplet::getNewComics() { if (!mEngine) { return; } if (!m_newStuffDialog) { m_newStuffDialog = new KNS3::DownloadDialog( QStringLiteral("comic.knsrc") ); KNS3::DownloadDialog *strong = m_newStuffDialog.data(); strong->setTitle(i18nc("@title:window", "Download Comics")); connect(m_newStuffDialog.data(), SIGNAL(finished(int)), mEngine, SLOT(loadProviders())); } m_newStuffDialog.data()->show(); } void ComicApplet::positionFullView(QWindow *window) { if (!window || !window->screen()) { return; } window->setPosition(window->screen()->availableGeometry().center() - QPoint(window->size().width()/2, window->size().height()/2)); } void ComicApplet::changeComic( bool differentComic ) { if ( differentComic ) { KConfigGroup cg = config(); mActionStorePosition->setChecked(mCurrent.storePosition()); // assign mScaleComic the moment the new strip has been loaded (dataUpdated) as up to this point // the old one should be still shown with its scaling settings mActionScaleContent->setChecked( mCurrent.scaleComic() ); updateComic( mCurrent.stored() ); } else { updateComic( mCurrent.current() ); } } void ComicApplet::updateUsedComics() { const QString oldIdentifier = mCurrent.id(); mActiveComicModel->clear(); mCurrent = ComicData(); bool isFirst = true; QModelIndex data; KConfigGroup cg = config(); int tab = 0; for ( int i = 0; i < mProxy->rowCount(); ++i ) { if (mTabIdentifier.contains(mProxy->index( i, 0 ).data( Qt::UserRole).toString())) { data = mProxy->index( i, 1 ); if ( isFirst ) { isFirst = false; const QString id = data.data( Qt::UserRole ).toString(); mDifferentComic = ( oldIdentifier != id ); const QString title = data.data().toString(); mCurrent.init(id, config()); mCurrent.setTitle(title); } const QString name = data.data().toString(); const QString identifier = data.data( Qt::UserRole ).toString(); const QString iconPath = data.data( Qt::DecorationRole ).value().name(); //found a newer strip last time, which was not visited if (mCheckNewComicStripsInterval && !cg.readEntry(QLatin1String("lastStripVisited_") + identifier, true)) { mActiveComicModel->addComic(identifier, name, iconPath, true); } else { mActiveComicModel->addComic(identifier, name, iconPath); } ++tab; } } mActionNextNewStripTab->setVisible( mCheckNewComicStripsInterval ); mActionNextNewStripTab->setEnabled( isTabHighlighted(mCurrent.id()) ); delete mCheckNewStrips; mCheckNewStrips = nullptr; if (mEngine && mCheckNewComicStripsInterval ) { mCheckNewStrips = new CheckNewStrips( mTabIdentifier, mEngine, mCheckNewComicStripsInterval, this ); connect( mCheckNewStrips, &CheckNewStrips::lastStrip, this, &ComicApplet::slotFoundLastStrip ); } emit comicModelChanged(); } void ComicApplet::slotTabChanged(const QString &identifier) { bool differentComic = (mCurrent.id() != identifier); mCurrent = ComicData(); mCurrent.init(identifier, config()); changeComic( differentComic ); } void ComicApplet::checkDayChanged() { - if ( ( mCurrentDay != QDate::currentDate() ) || !mCurrent.hasImage() ) + if ( mCurrentDay != QDate::currentDate() ) { + updateComic( mCurrent.current() ); + mCurrentDay = QDate::currentDate(); + } else if ( !mCurrent.hasImage() ) { updateComic( mCurrent.stored() ); - - mCurrentDay = QDate::currentDate(); + } } void ComicApplet::configChanged() { KConfigGroup cg = config(); mTabIdentifier = cg.readEntry( "tabIdentifier", QStringList() ); if (mProxy) { updateUsedComics(); } const QString id = mTabIdentifier.count() ? mTabIdentifier.at( 0 ) : QString(); mCurrent = ComicData(); mCurrent.init(id, cg); mShowComicUrl = cg.readEntry( "showComicUrl", false ); mShowComicAuthor = cg.readEntry( "showComicAuthor", false ); mShowComicTitle = cg.readEntry( "showComicTitle", false ); mShowComicIdentifier = cg.readEntry( "showComicIdentifier", false ); mShowErrorPicture = cg.readEntry( "showErrorPicture", true ); mArrowsOnHover = cg.readEntry( "arrowsOnHover", true ); mMiddleClick = cg.readEntry( "middleClick", true ); mCheckNewComicStripsInterval = cg.readEntry( "checkNewComicStripsIntervall", 30 ); - KConfigGroup global = globalConfig(); - mMaxComicLimit = global.readEntry( "maxComicLimit", CACHE_LIMIT ); + + auto oldMaxComicLimit = mMaxComicLimit; + mMaxComicLimit = cg.readEntry( "maxComicLimit", CACHE_LIMIT ); + if (oldMaxComicLimit != mMaxComicLimit && mEngine) { + mEngine->disconnectSource( QLatin1String( "setting_maxComicLimit:" ) + QString::number( oldMaxComicLimit ), this ); + mEngine->connectSource( QLatin1String( "setting_maxComicLimit:" ) + QString::number( mMaxComicLimit ), this ); + } globalComicUpdater->load(); } void ComicApplet::saveConfig() { KConfigGroup cg = config(); cg.writeEntry( "comic", mCurrent.id() ); cg.writeEntry( "showComicUrl", mShowComicUrl ); cg.writeEntry( "showComicAuthor", mShowComicAuthor ); cg.writeEntry( "showComicTitle", mShowComicTitle ); cg.writeEntry( "showComicIdentifier", mShowComicIdentifier ); cg.writeEntry( "showErrorPicture", mShowErrorPicture ); cg.writeEntry( "arrowsOnHover", mArrowsOnHover ); cg.writeEntry( "middleClick", mMiddleClick ); cg.writeEntry( "tabIdentifier", mTabIdentifier ); cg.writeEntry( "checkNewComicStripsIntervall", mCheckNewComicStripsInterval ); cg.writeEntry( "maxComicLimit", mMaxComicLimit); globalComicUpdater->save(); } void ComicApplet::slotNextDay() { updateComic(mCurrent.next()); } void ComicApplet::slotPreviousDay() { updateComic(mCurrent.prev()); } void ComicApplet::slotFirstDay() { updateComic(mCurrent.first()); } void ComicApplet::slotCurrentDay() { updateComic(QString()); } void ComicApplet::slotFoundLastStrip( int index, const QString &identifier, const QString &suffix ) { Q_UNUSED(index) + if (mCurrent.id() != identifier) { + return; + } + KConfigGroup cg = config(); if (suffix != cg.readEntry(QLatin1String("lastStrip_") + identifier, QString())) { qDebug() << identifier << "has a newer strip."; cg.writeEntry(QLatin1String("lastStripVisited_") + identifier, false); updateComic(suffix); } } void ComicApplet::slotGoJump() { StripSelector *selector = StripSelectorFactory::create(mCurrent.type()); connect(selector, &StripSelector::stripChosen, this, &ComicApplet::updateComic); selector->select(mCurrent); } void ComicApplet::slotStorePosition() { mCurrent.storePosition(mActionStorePosition->isChecked()); } void ComicApplet::slotShop() { KRun::runUrl(mCurrent.shopUrl(), QStringLiteral("text/html"), nullptr, KRun::RunFlags()); } void ComicApplet::createComicBook() { ComicArchiveDialog *dialog = new ComicArchiveDialog(mCurrent.id(), mCurrent.title(), mCurrent.type(), mCurrent.current(), mCurrent.first(), mSavingDir->getDir()); dialog->setAttribute(Qt::WA_DeleteOnClose);//to have destroyed emitted upon closing connect( dialog, &ComicArchiveDialog::archive, this, &ComicApplet::slotArchive ); dialog->show(); } void ComicApplet::slotArchive( int archiveType, const QUrl &dest, const QString &fromIdentifier, const QString &toIdentifier ) { if (!mEngine) { return; } mSavingDir->setDir(dest.path()); const QString id = mCurrent.id(); qDebug() << "Archiving:" << id << archiveType << dest << fromIdentifier << toIdentifier; ComicArchiveJob *job = new ComicArchiveJob(dest, mEngine, static_cast< ComicArchiveJob::ArchiveType >( archiveType ), mCurrent.type(), id, this); job->setFromIdentifier(id + QLatin1Char(':') + fromIdentifier); job->setToIdentifier(id + QLatin1Char(':') + toIdentifier); if (job->isValid()) { connect(job, &ComicArchiveJob::finished, this, &ComicApplet::slotArchiveFinished); KIO::getJobTracker()->registerJob(job); job->start(); } else { qWarning() << "Archiving job is not valid."; delete job; } } void ComicApplet::slotArchiveFinished (KJob *job ) { if ( job->error() ) { KNotification::event(KNotification::Warning, i18n("Archiving comic failed"), job->errorText(), QStringLiteral("dialog-warning")); } } QList ComicApplet::contextualActions() { return mActions; } void ComicApplet::updateComic( const QString &identifierSuffix ) { const QString id = mCurrent.id(); setConfigurationRequired( id.isEmpty() ); if ( !id.isEmpty() && mEngine && mEngine->isValid() ) { setBusy(true); const QString identifier = id + QLatin1Char(':') + identifierSuffix; //disconnecting of the oldSource is needed, otherwise you could get data for comics you are not looking at if you use tabs //if there was an error only disconnect the oldSource if it had nothing to do with the error or if the comic changed, that way updates of the error can come in if ( !mIdentifierError.isEmpty() && !mIdentifierError.contains( id ) ) { mEngine->disconnectSource( mIdentifierError, this ); mIdentifierError.clear(); } if ( ( mIdentifierError != mOldSource ) && ( mOldSource != identifier ) ) { mEngine->disconnectSource( mOldSource, this ); } mOldSource = identifier; mEngine->connectSource( identifier, this ); slotScaleToContent(); } else { qWarning() << "Either no identifier was specified or the engine could not be created:" << "id" << id << "engine valid:" << ( mEngine && mEngine->isValid() ); setConfigurationRequired( true ); } updateContextMenu(); } void ComicApplet::updateContextMenu() { if (mCurrent.id().isEmpty()) { mActiveComicModel->clear(); mActionNextNewStripTab->setEnabled(false); mActionGoFirst->setEnabled(false); mActionGoLast->setEnabled(false); mActionScaleContent->setEnabled(false); if (mActionShop) { mActionShop->setEnabled(false); } mActionStorePosition->setEnabled(false); mActionGoJump->setEnabled(false); mActionSaveComicAs->setEnabled(false); mActionCreateComicBook->setEnabled(false); mActionScaleContent->setChecked(false); } else { mActionGoFirst->setVisible(mCurrent.hasFirst()); mActionGoFirst->setEnabled(mCurrent.hasPrev()); mActionGoLast->setEnabled(true); if (mActionShop) { mActionShop->setEnabled(mCurrent.shopUrl().isValid()); } mActionScaleContent->setEnabled(true); mActionStorePosition->setEnabled(true); mActionGoJump->setEnabled(true); mActionSaveComicAs->setEnabled(true); mActionCreateComicBook->setEnabled(true); } } void ComicApplet::slotSaveComicAs() { ComicSaver saver(mSavingDir); saver.save(mCurrent); } void ComicApplet::slotScaleToContent() { setShowActualSize(mActionScaleContent->isChecked()); } //QML QObject *ComicApplet::comicsModel() const { return mActiveComicModel; } QObject *ComicApplet::availableComicsModel() const { return mProxy; } bool ComicApplet::showComicUrl() const { return mShowComicUrl; } void ComicApplet::setShowComicUrl(bool show) { if (show == mShowComicUrl) return; mShowComicUrl = show; emit showComicUrlChanged(); } bool ComicApplet::showComicAuthor() const { return mShowComicAuthor; } void ComicApplet::setShowComicAuthor(bool show) { if (show == mShowComicAuthor) return; mShowComicAuthor = show; emit showComicAuthorChanged(); } bool ComicApplet::showComicTitle() const { return mShowComicTitle; } void ComicApplet::setShowComicTitle(bool show) { if (show == mShowComicTitle) return; mShowComicTitle = show; emit showComicTitleChanged(); } bool ComicApplet::showComicIdentifier() const { return mShowComicIdentifier; } void ComicApplet::setShowComicIdentifier(bool show) { if (show == mShowComicIdentifier) return; mShowComicIdentifier = show; emit showComicIdentifierChanged(); } bool ComicApplet::showErrorPicture() const { return mShowErrorPicture; } void ComicApplet::setShowErrorPicture(bool show) { if (show == mShowErrorPicture) return; mShowErrorPicture = show; emit showErrorPictureChanged(); } bool ComicApplet::arrowsOnHover() const { return mArrowsOnHover; } void ComicApplet::setArrowsOnHover(bool show) { if (show == mArrowsOnHover) return; mArrowsOnHover = show; emit arrowsOnHoverChanged(); } bool ComicApplet::middleClick() const { return mMiddleClick; } void ComicApplet::setMiddleClick(bool show) { if (show == mMiddleClick) return; mMiddleClick = show; emit middleClickChanged(); saveConfig(); } QVariantMap ComicApplet::comicData() const { return mComicData; } QStringList ComicApplet::tabIdentifiers() const { return mTabIdentifier; } void ComicApplet::setTabIdentifiers(const QStringList &tabs) { if (mTabIdentifier == tabs) { return; } mTabIdentifier = tabs; emit tabIdentifiersChanged(); saveConfig(); changeComic( mDifferentComic ); } void ComicApplet::refreshComicData() { mComicData[QStringLiteral("image")] = mCurrent.image(); mComicData[QStringLiteral("prev")] = mCurrent.prev(); mComicData[QStringLiteral("next")] = mCurrent.next(); mComicData[QStringLiteral("additionalText")] = mCurrent.additionalText(); mComicData[QStringLiteral("websiteUrl")] = mCurrent.websiteUrl().toString(); mComicData[QStringLiteral("websiteHost")] = mCurrent.websiteUrl().host(); mComicData[QStringLiteral("imageUrl")] = mCurrent.websiteUrl().toString(); mComicData[QStringLiteral("shopUrl")] = mCurrent.websiteUrl().toString(); mComicData[QStringLiteral("first")] = mCurrent.first(); mComicData[QStringLiteral("stripTitle")] = mCurrent.stripTitle(); mComicData[QStringLiteral("author")] = mCurrent.author(); mComicData[QStringLiteral("title")] = mCurrent.title(); mComicData[QStringLiteral("suffixType")] = QStringLiteral("Date"); mComicData[QStringLiteral("current")] = mCurrent.current(); //mComicData[QStringLiteral("last")] = mCurrent.last(); mComicData[QStringLiteral("currentReadable")] = mCurrent.currentReadable(); mComicData[QStringLiteral("firstStripNum")] = mCurrent.firstStripNum(); mComicData[QStringLiteral("maxStripNum")] = mCurrent.maxStripNum(); mComicData[QStringLiteral("isLeftToRight")] = mCurrent.isLeftToRight(); mComicData[QStringLiteral("isTopToBottom")] = mCurrent.isTopToBottom(); emit comicDataChanged(); } bool ComicApplet::showActualSize() const { return mCurrent.scaleComic(); } void ComicApplet::setShowActualSize(bool show) { if (show == mCurrent.scaleComic()) return; mCurrent.setScaleComic(show); emit showActualSizeChanged(); } int ComicApplet::checkNewComicStripsInterval() const { return mCheckNewComicStripsInterval; } void ComicApplet::setCheckNewComicStripsInterval(int interval) { if (mCheckNewComicStripsInterval == interval) { return; } mCheckNewComicStripsInterval = interval; emit checkNewComicStripsIntervalChanged(); } int ComicApplet::providerUpdateInterval() const { return globalComicUpdater->interval(); } void ComicApplet::setProviderUpdateInterval(int interval) { if (globalComicUpdater->interval() == interval) { return; } globalComicUpdater->setInterval(interval); emit providerUpdateIntervalChanged(); } void ComicApplet::setMaxComicLimit(int limit) { if (mMaxComicLimit == limit) { return; } mMaxComicLimit = limit; emit maxComicLimitChanged(); } int ComicApplet::maxComicLimit() const { return mMaxComicLimit; } //Endof QML void ComicApplet::setTabHighlighted(const QString &id, bool highlight) { //Search for matching id for (int i = 0; i < mActiveComicModel->rowCount(); ++i) { QStandardItem *item = mActiveComicModel->item(i); QString currentId = item->data(ActiveComicModel::ComicKeyRole).toString(); if (id == currentId) { if (highlight != item->data(ActiveComicModel::ComicHighlightRole).toBool()) { item->setData(highlight, ActiveComicModel::ComicHighlightRole); emit tabHighlightRequest(id, highlight); } } } } bool ComicApplet::isTabHighlighted(const QString &id) const { for (int i = 0; i < mActiveComicModel->rowCount(); ++i) { QStandardItem *item = mActiveComicModel->item(i); QString currentId = item->data(ActiveComicModel::ComicKeyRole).toString(); if (id == currentId) { return item->data(ActiveComicModel::ComicHighlightRole).toBool(); } } return false; } K_EXPORT_PLASMA_APPLET_WITH_JSON(comic, ComicApplet, "metadata.json") #include "comic.moc" diff --git a/applets/comic/comicupdater.cpp b/applets/comic/comicupdater.cpp index 106ef979c..971f6cb1d 100644 --- a/applets/comic/comicupdater.cpp +++ b/applets/comic/comicupdater.cpp @@ -1,103 +1,104 @@ /*************************************************************************** * Copyright (C) 2007 by Tobias Koenig * * Copyright (C) 2008-2010 Matthias Fuchs * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * ***************************************************************************/ #include "comicupdater.h" #include "comicmodel.h" #include #include #include #include #include ComicUpdater::ComicUpdater( QObject *parent ) : QObject( parent ), mDownloadManager(nullptr), mUpdateIntervall( 3 ), m_updateTimer(nullptr) { } ComicUpdater::~ComicUpdater() { } void ComicUpdater::init(const KConfigGroup &group) { mGroup = group; } void ComicUpdater::load() { //check when the last update happened and update if necessary mUpdateIntervall = mGroup.readEntry( "updateInterval", 3 ); if ( mUpdateIntervall ) { mLastUpdate = mGroup.readEntry( "lastUpdate", QDateTime() ); checkForUpdate(); } } void ComicUpdater::save() { mGroup.writeEntry( "updateInterval", mUpdateIntervall ); } void ComicUpdater::setInterval( int interval ) { mUpdateIntervall = interval; } int ComicUpdater::interval() const { return mUpdateIntervall; } void ComicUpdater::checkForUpdate() { //start a timer to check each hour, if KNS3 should look for updates if ( !m_updateTimer ) { m_updateTimer = new QTimer(this); connect(m_updateTimer, &QTimer::timeout, this, &ComicUpdater::checkForUpdate); m_updateTimer->start( 1 * 60 * 60 * 1000 ); } if ( !mLastUpdate.isValid() || ( mLastUpdate.addDays( mUpdateIntervall ) < QDateTime::currentDateTime() ) ) { - mGroup.writeEntry( "lastUpdate", QDateTime::currentDateTime() ); + mLastUpdate = QDateTime::currentDateTime(); + mGroup.writeEntry( "lastUpdate", mLastUpdate ); downloadManager()->checkForUpdates(); } } void ComicUpdater::slotUpdatesFound(const KNSCore::EntryInternal::List& entries) { for ( int i = 0; i < entries.count(); ++i ) { downloadManager()->installEntry( entries[ i ] ); } } KNSCore::DownloadManager *ComicUpdater::downloadManager() { if ( !mDownloadManager ) { mDownloadManager = new KNSCore::DownloadManager(QStringLiteral("comic.knsrc"), this ); connect(mDownloadManager, &KNSCore::DownloadManager::searchResult, this, &ComicUpdater::slotUpdatesFound); } return mDownloadManager; } diff --git a/applets/weather/package/contents/ui/NoticesView.qml b/applets/weather/package/contents/ui/NoticesView.qml index f9f522c61..e5338aeb6 100644 --- a/applets/weather/package/contents/ui/NoticesView.qml +++ b/applets/weather/package/contents/ui/NoticesView.qml @@ -1,73 +1,77 @@ /* * Copyright 2018 Friedrich W. H. Kossebau * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.9 import QtQuick.Layouts 1.3 import org.kde.plasma.components 3.0 as PlasmaComponents import org.kde.plasma.extras 2.0 as PlasmaExtras ColumnLayout { id: root property alias model: categoryRepeater.model - readonly property bool hasContent: model && model.length > 0 && model[0].length > 0 && model[1].length > 0 + readonly property bool hasContent: model && model.length > 0 && (model[0].length > 0 || model[1].length > 0) spacing: units.largeSpacing Repeater { id: categoryRepeater delegate: ColumnLayout { property var categoryData: modelData + readonly property bool categoryHasNotices: categoryData.length > 0 + visible: categoryHasNotices + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter PlasmaExtras.Heading { level: 4 + Layout.alignment: Qt.AlignHCenter text: index == 0 ? i18nc("@title:column weather warnings", "Warnings Issued") : i18nc("@title:column weather watches" ,"Watches Issued") } Repeater { id: repeater model: categoryData delegate: PlasmaComponents.Label { font.underline: true color: theme.linkColor text: modelData.description MouseArea { anchors.fill: parent onClicked: { Qt.openUrlExternally(modelData.info); } } } } } } Item { Layout.fillHeight: true } } diff --git a/applets/weather/package/contents/ui/SwitchPanel.qml b/applets/weather/package/contents/ui/SwitchPanel.qml index b5a0d99f5..6ac4bf7e8 100644 --- a/applets/weather/package/contents/ui/SwitchPanel.qml +++ b/applets/weather/package/contents/ui/SwitchPanel.qml @@ -1,156 +1,155 @@ /* * Copyright 2018 Friedrich W. H. Kossebau * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.9 import QtQuick.Layouts 1.3 import QtQuick.Controls 2.2 as QtControls import org.kde.plasma.components 3.0 as PlasmaComponents ColumnLayout { id: root property alias forecastModel: forecastView.model property alias detailsModel: detailsView.model property alias noticesModel: noticesView.model property alias forecastViewTitle: forecastTabButton.text readonly property bool hasDetailsContent: detailsModel && detailsModel.length > 0 - readonly property bool hasNoticesContent: noticesModel && noticesModel.length > 0 && - noticesModel[0].length > 0 && noticesModel[1].length > 0 + readonly property alias hasNoticesContent: noticesView.hasContent function removePage(page) { // fill-in for removeItem, replace for QQC >= 2.3 for (var n = 0; n < swipeView.count; n++) { if (page === swipeView.itemAt(n)) { swipeView.removeItem(n); tabBar.itemAt(n).visible = false; tabBar.removeItem(n); } } page.visible = false } PlasmaComponents.TabButton { id: detailsTabButton visible: false; text: i18nc("@title:tab", "Details") } DetailsView { id: detailsView visible: false; model: detailsModel } PlasmaComponents.TabButton { id: noticesTabButton visible: false; text: i18nc("@title:tab", "Notices") } NoticesView { id: noticesView visible: false; model: noticesModel } PlasmaComponents.TabBar { id: tabBar Layout.fillWidth: true visible: hasDetailsContent || hasNoticesContent PlasmaComponents.TabButton { id: forecastTabButton } } QtControls.SwipeView { id: swipeView Layout.fillWidth: true Layout.fillHeight: true Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter Layout.minimumWidth: Math.max(forecastView.Layout.minimumWidth, detailsView.Layout.minimumWidth, noticesView.Layout.minimumWidth) Layout.minimumHeight: Math.max(forecastView.Layout.minimumHeight, detailsView.Layout.minimumHeight, noticesView.Layout.minimumHeight) clip: true // previous/next views are prepared outside of view, do not render them currentIndex: tabBar.currentIndex ColumnLayout { ForecastView { id: forecastView Layout.alignment: Qt.AlignTop | Qt.AlignHCenter Layout.fillWidth: false } } onCurrentIndexChanged: { tabBar.setCurrentIndex(currentIndex); } } // perhaps onHasDetailsContentChanged: { if (hasDetailsContent) { tabBar.insertItem(1, detailsTabButton); detailsTabButton.visible = true; swipeView.insertItem(1, detailsView); detailsView.visible = true } else { removePage(detailsView); } } onHasNoticesContentChanged: { if (hasNoticesContent) { tabBar.addItem(noticesTabButton); noticesTabButton.visible = true; swipeView.addItem(noticesView); noticesView.visible = true } else { removePage(noticesView); } } Component.onDestruction: { // prevent double deletion, take back inserted items if (hasDetailsContent) { removePage(detailsView); } if (hasNoticesContent) { removePage(noticesView); } } } diff --git a/dataengines/comic/comic.cpp b/dataengines/comic/comic.cpp index 03d89a348..ef6d804f1 100644 --- a/dataengines/comic/comic.cpp +++ b/dataengines/comic/comic.cpp @@ -1,328 +1,327 @@ /* * Copyright (C) 2007 Tobias Koenig * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License version 2 as * published by the Free Software Foundation * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "comic.h" #include #include #include #include #include #include #include #include #include #include "cachedprovider.h" #include "comicproviderkross.h" ComicEngine::ComicEngine(QObject* parent, const QVariantList& args) : Plasma::DataEngine(parent, args), mEmptySuffix(false) { setPollingInterval(0); loadProviders(); } ComicEngine::~ComicEngine() { } void ComicEngine::init() { connect(&m_networkConfigurationManager, &QNetworkConfigurationManager::onlineStateChanged, this, &ComicEngine::onOnlineStateChanged); } void ComicEngine::onOnlineStateChanged(bool isOnline) { if (isOnline && !mIdentifierError.isEmpty()) { sourceRequestEvent(mIdentifierError); } } void ComicEngine::loadProviders() { mProviders.clear(); removeAllData(QLatin1String("providers")); auto comics = KPackage::PackageLoader::self()->listPackages(QStringLiteral("Plasma/Comic")); for (auto comic : comics) { mProviders << comic.pluginId(); //qDebug() << "ComicEngine::loadProviders() service name=" << comic.name(); QStringList data; data << comic.name(); QFileInfo file(comic.iconName()); if (file.isRelative()) { data << QStandardPaths::locate(QStandardPaths::GenericDataLocation, QString::fromLatin1("plasma/comics/%1/%2").arg(comic.pluginId(), comic.iconName())); } else { data << comic.iconName(); } setData(QLatin1String("providers"), comic.pluginId(), data); } forceImmediateUpdateOfAllVisualizations(); } bool ComicEngine::updateSourceEvent(const QString &identifier) { if (identifier == QLatin1String("providers")) { loadProviders(); return true; } else if (identifier.startsWith(QLatin1String("setting_maxComicLimit:"))) { bool worked; const int maxComicLimit = identifier.mid(22).toInt(&worked); if (worked) { CachedProvider::setMaxComicLimit(maxComicLimit); } return worked; } else { if (m_jobs.contains(identifier)) { return true; } - // check whether it is cached already... - if (CachedProvider::isCached(identifier)) { + const QStringList parts = identifier.split(QLatin1Char(':'), QString::KeepEmptyParts); + + // check whether it is cached, make sure second part present + if (parts.count() > 1 && CachedProvider::isCached(identifier)) { QVariantList args; args << QLatin1String("String") << identifier; ComicProvider *provider = new CachedProvider(this, args); m_jobs[identifier] = provider; connect(provider, SIGNAL(finished(ComicProvider*)), this, SLOT(finished(ComicProvider*))); connect(provider, SIGNAL(error(ComicProvider*)), this, SLOT(error(ComicProvider*))); return true; } // ... start a new query otherwise - const QStringList parts = identifier.split(QLatin1Char(':'), QString::KeepEmptyParts); - - //: are mandatory if (parts.count() < 2) { setData(identifier, QLatin1String("Error"), true); qWarning() << "Less than two arguments specified."; return false; } if (!mProviders.contains(parts[0])) { // User might have installed more from GHNS loadProviders(); if (!mProviders.contains(parts[0])) { setData(identifier, QLatin1String("Error"), true); qWarning() << identifier << "comic plugin does not seem to be installed."; return false; } } // check if there is a connection if (!m_networkConfigurationManager.isOnline()) { mIdentifierError = identifier; setData(identifier, QLatin1String("Error"), true); setData(identifier, QLatin1String("Error automatically fixable"), true); setData(identifier, QLatin1String("Identifier"), identifier); setData(identifier, QLatin1String("Previous identifier suffix"), lastCachedIdentifier(identifier)); qDebug() << "No connection."; return true; } KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Comic"), parts[0]); bool isCurrentComic = parts[1].isEmpty(); QVariantList args; ComicProvider *provider = nullptr; //const QString type = service->property(QLatin1String("X-KDE-PlasmaComicProvider-SuffixType"), QVariant::String).toString(); const QString type = pkg.metadata().value(QStringLiteral("X-KDE-PlasmaComicProvider-SuffixType")); if (type == QLatin1String("Date")) { QDate date = QDate::fromString(parts[1], Qt::ISODate); if (!date.isValid()) date = QDate::currentDate(); args << QLatin1String("Date") << date; } else if (type == QLatin1String("Number")) { args << QLatin1String("Number") << parts[1].toInt(); } else if (type == QLatin1String("String")) { args << QLatin1String("String") << parts[1]; } args << QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("plasma/comics/") + parts[0] + QLatin1String("/metadata.desktop")); //provider = service->createInstance(this, args); provider = new ComicProviderKross(this, args); if (!provider) { setData(identifier, QLatin1String("Error"), true); return false; } provider->setIsCurrent(isCurrentComic); m_jobs[identifier] = provider; connect(provider, SIGNAL(finished(ComicProvider*)), this, SLOT(finished(ComicProvider*))); connect(provider, SIGNAL(error(ComicProvider*)), this, SLOT(error(ComicProvider*))); return true; } } bool ComicEngine::sourceRequestEvent(const QString &identifier) { setData(identifier, DataEngine::Data()); return updateSourceEvent(identifier); } void ComicEngine::finished(ComicProvider *provider) { // sets the data setComicData(provider); if (provider->image().isNull()) { error(provider); return; } // different comic -- with no error yet -- has been chosen, old error is invalidated QString temp = mIdentifierError.left(mIdentifierError.indexOf(QLatin1Char(':')) + 1); if (!mIdentifierError.isEmpty() && provider->identifier().indexOf(temp) == -1) { mIdentifierError.clear(); } // comic strip with error worked now if (!mIdentifierError.isEmpty() && (mIdentifierError == provider->identifier())){ mIdentifierError.clear(); } // store in cache if it's not the response of a CachedProvider, // if there is a valid image and if there is a next comic // (if we're on today's comic it could become stale) if (!provider->inherits("CachedProvider") && !provider->image().isNull() && !provider->nextIdentifier().isEmpty()) { CachedProvider::Settings info; info[QLatin1String("websiteUrl")] = provider->websiteUrl().toString(QUrl::PrettyDecoded); info[QLatin1String("imageUrl")] = provider->imageUrl().url(); info[QLatin1String("shopUrl")] = provider->shopUrl().toString(QUrl::PrettyDecoded); info[QLatin1String("nextIdentifier")] = provider->nextIdentifier(); info[QLatin1String("previousIdentifier")] = provider->previousIdentifier(); info[QLatin1String("title")] = provider->name(); info[QLatin1String("suffixType")] = provider->suffixType(); info[QLatin1String("lastCachedStripIdentifier")] = provider->identifier().mid(provider->identifier().indexOf(QLatin1Char(':')) + 1); QString isLeftToRight; QString isTopToBottom; info[QLatin1String("isLeftToRight")] = isLeftToRight.setNum(provider->isLeftToRight()); info[QLatin1String("isTopToBottom")] = isTopToBottom.setNum(provider->isTopToBottom()); //data that should be only written if available if (!provider->comicAuthor().isEmpty()) { info[QLatin1String("comicAuthor")] = provider->comicAuthor(); } if (!provider->firstStripIdentifier().isEmpty()) { info[QLatin1String("firstStripIdentifier")] = provider->firstStripIdentifier(); } if (!provider->additionalText().isEmpty()) { info[QLatin1String("additionalText")] = provider->additionalText(); } if (!provider->stripTitle().isEmpty()) { info[QLatin1String("stripTitle")] = provider->stripTitle(); } CachedProvider::storeInCache(provider->identifier(), provider->image(), info); } provider->deleteLater(); const QString key = m_jobs.key(provider); if (!key.isEmpty()) { m_jobs.remove(key); } } void ComicEngine::error(ComicProvider *provider) { // sets the data setComicData(provider); QString identifier(provider->identifier()); mIdentifierError = identifier; qWarning() << identifier << "plugging reported an error."; /** * Requests for the current day have no suffix (date or id) * set initially, so we have to remove the 'faked' suffix * here again to not confuse the applet. */ if (provider->isCurrent()) identifier = identifier.left(identifier.indexOf(QLatin1Char(':')) + 1); setData(identifier, QLatin1String("Identifier"), identifier); setData(identifier, QLatin1String("Error"), true); // if there was an error loading the last cached comic strip, do not return its id anymore const QString lastCachedId = lastCachedIdentifier(identifier); if (lastCachedId != provider->identifier().mid(provider->identifier().indexOf(QLatin1Char(':')) + 1)) { // sets the previousIdentifier to the identifier of a strip that has been cached before setData(identifier, QLatin1String("Previous identifier suffix"), lastCachedId); } setData(identifier, QLatin1String("Next identifier suffix"), QString()); const QString key = m_jobs.key(provider); if (!key.isEmpty()) { m_jobs.remove(key); } provider->deleteLater(); } void ComicEngine::setComicData(ComicProvider *provider) { QString identifier(provider->identifier()); /** * Requests for the current day have no suffix (date or id) * set initially, so we have to remove the 'faked' suffix * here again to not confuse the applet. */ if (provider->isCurrent()) identifier = identifier.left(identifier.indexOf(QLatin1Char(':')) + 1); setData(identifier, QLatin1String("Image"), provider->image()); setData(identifier, QLatin1String("Website Url"), provider->websiteUrl()); setData(identifier, QLatin1String("Image Url"), provider->imageUrl()); setData(identifier, QLatin1String("Shop Url"), provider->shopUrl()); setData(identifier, QLatin1String("Next identifier suffix"), provider->nextIdentifier()); setData(identifier, QLatin1String("Previous identifier suffix"), provider->previousIdentifier()); setData(identifier, QLatin1String("Comic Author"), provider->comicAuthor()); setData(identifier, QLatin1String("Additional text"), provider->additionalText()); setData(identifier, QLatin1String("Strip title"), provider->stripTitle()); setData(identifier, QLatin1String("First strip identifier suffix"), provider->firstStripIdentifier()); setData(identifier, QLatin1String("Identifier"), provider->identifier()); setData(identifier, QLatin1String("Title"), provider->name()); setData(identifier, QLatin1String("SuffixType"), provider->suffixType()); setData(identifier, QLatin1String("isLeftToRight"), provider->isLeftToRight()); setData(identifier, QLatin1String("isTopToBottom"), provider->isTopToBottom()); setData(identifier, QLatin1String("Error"), false); } QString ComicEngine::lastCachedIdentifier(const QString &identifier) const { const QString id = identifier.left(identifier.indexOf(QLatin1Char(':'))); QString data = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/plasma_engine_comic/"); data += QString::fromLatin1(QUrl::toPercentEncoding(id)); QSettings settings(data + QLatin1String(".conf"), QSettings::IniFormat); QString previousIdentifier = settings.value(QLatin1String("lastCachedStripIdentifier"), QString()).toString(); return previousIdentifier; } K_EXPORT_PLASMA_DATAENGINE_WITH_JSON(comic, ComicEngine, "plasma-dataengine-comic.json") #include "comic.moc" diff --git a/dataengines/potd/bingprovider.json b/dataengines/potd/bingprovider.json index eb67301dc..50fd67ea1 100644 --- a/dataengines/potd/bingprovider.json +++ b/dataengines/potd/bingprovider.json @@ -1,61 +1,62 @@ { "KPlugin": { "Description": "Bing Provider", + "Description[ast]": "Fornidor Bing", "Description[ca@valencia]": "Proveïdor Bing", "Description[ca]": "Proveïdor Bing", "Description[cs]": "Poskytovatel Bing", "Description[de]": "Bing-Anbieter", "Description[en_GB]": "Bing Provider", "Description[es]": "Proveedor de Bing", "Description[eu]": "Bing hornitzailea", "Description[fi]": "Bing-tarjoaja", "Description[fr]": "Fournisseur Bing", "Description[gl]": "Fornecedor de Bing", "Description[id]": "Penyedia Bing", "Description[it]": "Fornitore Bing", "Description[ko]": "Bing 공급자", "Description[nl]": "Bing-provider", "Description[nn]": "Bing-tilbydar", "Description[pl]": "Dostawca Bing", "Description[pt]": "Fornecedor do Bing", "Description[pt_BR]": "Fornecedor Bing", "Description[ru]": "Источник данных Bing", "Description[sk]": "Poskytovateľ Bing", "Description[sv]": "Tillhandahåll från Bing", "Description[uk]": "Постачальник даних Bing", "Description[x-test]": "xxBing Providerxx", "Description[zh_CN]": "必应提供源", "Description[zh_TW]": "Bing 提供者", "Icon": "", "Name": "Bing's Picture of the Day", "Name[ca@valencia]": "Imatge del dia del Bing", "Name[ca]": "Imatge del dia del Bing", "Name[cs]": "Obrázek dne na Bing", "Name[de]": "Bild des Tages von Bing", "Name[en_GB]": "Bing's Picture of the Day", "Name[es]": "Imagen del día de Bing", "Name[eu]": "Bing-eren eguneko argazkia", "Name[fi]": "Bingin päivän kuva", "Name[fr]": "Image du jour Bing", "Name[gl]": "Imaxe do día de Bing", "Name[id]": "Bing's Picture of the Day", "Name[it]": "Immagine del giorno di Bing", "Name[ko]": "Bing 오늘의 그림", "Name[nl]": "Afbeelding van de dag van Bing", "Name[nn]": "Dagens bilete frå Bing", "Name[pl]": "Obraz dnia Binga", "Name[pt]": "Imagem do Dia do Bing", "Name[pt_BR]": "Imagem do dia do Bing", "Name[ru]": "Изображение дня Bing", "Name[sk]": "Obrázok dňa Bing", "Name[sv]": "Dagens bild på Bing", "Name[uk]": "Картинка дня Bing", "Name[x-test]": "xxBing's Picture of the Dayxx", "Name[zh_CN]": "每日一图(必应)", "Name[zh_TW]": "今天的 Bing 影像", "ServiceTypes": [ "PlasmaPoTD/Plugin" ] }, "X-KDE-PlasmaPoTDProvider-Identifier": "bing" }