diff --git a/discover/DiscoverMainWindow.cpp b/discover/DiscoverMainWindow.cpp index 339c6724..247268f5 100644 --- a/discover/DiscoverMainWindow.cpp +++ b/discover/DiscoverMainWindow.cpp @@ -1,406 +1,407 @@ /* * Copyright (C) 2012 Aleix Pol Gonzalez * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library/Lesser General Public License * version 2, or (at your option) any later version, 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/Lesser 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 "DiscoverMainWindow.h" #include "PaginateModel.h" #include "UnityLauncher.h" #include "FeaturedModel.h" #include "CachedNetworkAccessManager.h" #include "DiscoverDeclarativePlugin.h" // Qt includes #include #include #include #include #include #include #include #include #include #include #include #include // KDE includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include // #include // DiscoverCommon includes #include #include #include #include #include #include #include #include #include #include DiscoverMainWindow::DiscoverMainWindow(CompactMode mode) : QObject() , m_collection(this) , m_engine(new QQmlApplicationEngine) , m_mode(mode) , m_networkAccessManagerFactory(new CachedNetworkAccessManagerFactory) { setObjectName(QStringLiteral("DiscoverMain")); KDeclarative::KDeclarative kdeclarative; kdeclarative.setDeclarativeEngine(m_engine); kdeclarative.setupBindings(); qmlRegisterType("org.kde.discover.app", 1, 0, "UnityLauncher"); qmlRegisterType("org.kde.discover.app", 1, 0, "PaginateModel"); qmlRegisterType("org.kde.discover.app", 1, 0, "KConcatenateRowsProxyModel"); qmlRegisterType("org.kde.discover.app", 1, 0, "FeaturedModel"); qmlRegisterType("org.kde.discover.app", 1, 0, "QSortFilterProxyModel"); qmlRegisterSingletonType(QUrl(QStringLiteral("qrc:/qml/DiscoverSystemPalette.qml")), "org.kde.discover.app", 1, 0, "DiscoverSystemPalette"); qmlRegisterType(); qmlRegisterType(); qmlRegisterType(); qmlRegisterUncreatableType("org.kde.discover.app", 1, 0, "DiscoverMainWindow", QStringLiteral("don't do that")); setupActions(); auto uri = "org.kde.discover"; DiscoverDeclarativePlugin* plugin = new DiscoverDeclarativePlugin; plugin->setParent(this); plugin->initializeEngine(m_engine, uri); plugin->registerTypes(uri); //Here we set up a cache for the screenshots delete m_engine->networkAccessManagerFactory(); m_engine->setNetworkAccessManagerFactory(m_networkAccessManagerFactory.data()); m_engine->rootContext()->setContextProperty(QStringLiteral("app"), this); connect(m_engine, &QQmlApplicationEngine::objectCreated, this, &DiscoverMainWindow::integrateObject); m_engine->load(QUrl(QStringLiteral("qrc:/qml/DiscoverWindow.qml"))); } DiscoverMainWindow::~DiscoverMainWindow() { delete m_engine; } bool DiscoverMainWindow::isRoot() { return ::getuid() == 0; } QStringList DiscoverMainWindow::modes() const { QStringList ret; QObject* obj = rootObject(); for(int i = obj->metaObject()->propertyOffset(); imetaObject()->propertyCount(); i++) { QMetaProperty p = obj->metaObject()->property(i); QByteArray compName = p.name(); if(compName.startsWith("top") && compName.endsWith("Comp")) { ret += QString::fromLatin1(compName.mid(3, compName.length()-7)); } } return ret; } void DiscoverMainWindow::openMode(const QString& _mode) { if(!modes().contains(_mode)) qWarning() << "unknown mode" << _mode; QString mode = _mode; mode[0] = mode[0].toUpper(); QObject* obj = rootObject(); const QByteArray propertyName = "top"+mode.toLatin1()+"Comp"; const QVariant modeComp = obj->property(propertyName.constData()); if (!modeComp.isValid()) qWarning() << "couldn't open mode" << _mode; else obj->setProperty("currentTopLevel", modeComp); } void DiscoverMainWindow::openMimeType(const QString& mime) { emit listMimeInternal(mime); } void DiscoverMainWindow::openCategory(const QString& category) { rootObject()->setProperty("defaultStartup", false); auto action = new OneTimeAction( [this, category]() { Category* cat = CategoryModel::global()->findCategoryByName(category); if (cat) { emit listCategoryInternal(cat); } else { showPassiveNotification(i18n("Could not find category '%1'", category)); rootObject()->setProperty("defaultStartup", false); } } , this); if (ResourcesModel::global()->backends().isEmpty()) { connect(ResourcesModel::global(), &ResourcesModel::backendsChanged, action, &OneTimeAction::trigger); } else { action->trigger(); } } void DiscoverMainWindow::openLocalPackage(const QUrl& localfile) { if (!QFile::exists(localfile.toLocalFile())) { // showPassiveNotification(i18n("Trying to open unexisting file '%1'", localfile.toString())); qWarning() << "Trying to open unexisting file" << localfile; return; } rootObject()->setProperty("defaultStartup", false); auto action = new OneTimeAction( [this, localfile]() { auto res = ResourcesModel::global()->resourceForFile(localfile); qDebug() << "all initialized..." << res; if (res) { emit openApplicationInternal(res); } else { rootObject()->setProperty("defaultStartup", true); showPassiveNotification(i18n("Couldn't open %1", localfile.toDisplayString())); } } , this); if (ResourcesModel::global()->backends().isEmpty()) { connect(ResourcesModel::global(), &ResourcesModel::backendsChanged, action, &OneTimeAction::trigger); } else { action->trigger(); } } void DiscoverMainWindow::openApplication(const QUrl& url) { Q_ASSERT(!url.isEmpty()); Q_EMIT openUrl(url); } void DiscoverMainWindow::integrateObject(QObject* object) { if (!object) { qWarning() << "Errors when loading the GUI"; QTimer::singleShot(0, QCoreApplication::instance(), [](){ QCoreApplication::instance()->exit(1); }); return; } Q_ASSERT(object == rootObject()); KConfigGroup window(KSharedConfig::openConfig(), "Window"); if (window.hasKey("geometry")) rootObject()->setGeometry(window.readEntry("geometry", QRect())); if (window.hasKey("visibility")) { QWindow::Visibility visibility(QWindow::Visibility(window.readEntry("visibility", QWindow::Windowed))); rootObject()->setVisibility(qMax(visibility, QQuickView::AutomaticVisibility)); } object->installEventFilter(this); + connect(object, &QObject::destroyed, qGuiApp, &QCoreApplication::quit); } bool DiscoverMainWindow::eventFilter(QObject * object, QEvent * event) { if (object!=rootObject()) return false; if (event->type() == QEvent::Close) { if (ResourcesModel::global()->isBusy()) { qWarning() << "not closing because there's still pending tasks"; Q_EMIT preventedClose(); return true; } KConfigGroup window(KSharedConfig::openConfig(), "Window"); window.writeEntry("geometry", rootObject()->geometry()); window.writeEntry("visibility", rootObject()->visibility()); } return false; } void DiscoverMainWindow::setupActions() { QAction *quitAction = KStandardAction::quit(QCoreApplication::instance(), &QCoreApplication::quit, actionCollection()); actionCollection()->addAction(QStringLiteral("file_quit"), quitAction); if (KAuthorized::authorizeAction(QStringLiteral("help_contents"))) { auto mHandBookAction = KStandardAction::helpContents(this, &DiscoverMainWindow::appHelpActivated, this); actionCollection()->addAction(mHandBookAction->objectName(), mHandBookAction); } if (KAuthorized::authorizeAction(QStringLiteral("help_report_bug")) && !KAboutData::applicationData().bugAddress().isEmpty()) { auto mReportBugAction = KStandardAction::reportBug(this, &DiscoverMainWindow::reportBug, this); actionCollection()->addAction(mReportBugAction->objectName(), mReportBugAction); } if (KAuthorized::authorizeAction(QStringLiteral("switch_application_language"))) { // if (KLocalizedString::availableApplicationTranslations().count() > 1) { auto mSwitchApplicationLanguageAction = KStandardAction::create(KStandardAction::SwitchApplicationLanguage, this, &DiscoverMainWindow::switchApplicationLanguage, this); actionCollection()->addAction(mSwitchApplicationLanguageAction->objectName(), mSwitchApplicationLanguageAction); // } } if (KAuthorized::authorizeAction(QStringLiteral("help_about_app"))) { auto mAboutAppAction = KStandardAction::aboutApp(this, &DiscoverMainWindow::aboutApplication, this); actionCollection()->addAction(mAboutAppAction->objectName(), mAboutAppAction); } auto mKeyBindignsAction = KStandardAction::keyBindings(this, &DiscoverMainWindow::configureShortcuts, this); actionCollection()->addAction(mKeyBindignsAction->objectName(), mKeyBindignsAction); } QAction * DiscoverMainWindow::action(const QString& name) { return actionCollection()->action(name); } QString DiscoverMainWindow::iconName(const QIcon& icon) { return icon.name(); } void DiscoverMainWindow::appHelpActivated() { QDesktopServices::openUrl(QUrl(QStringLiteral("help:/"))); } void DiscoverMainWindow::aboutApplication() { static QPointer dialog; if (!dialog) { dialog = new KAboutApplicationDialog(KAboutData::applicationData(), nullptr); dialog->setAttribute(Qt::WA_DeleteOnClose); } dialog->show(); } void DiscoverMainWindow::reportBug() { static QPointer dialog; if (!dialog) { dialog = new KBugReport(KAboutData::applicationData(), nullptr); dialog->setAttribute(Qt::WA_DeleteOnClose); } dialog->show(); } void DiscoverMainWindow::switchApplicationLanguage() { // auto langDialog = new KSwitchLanguageDialog(nullptr); // connect(langDialog, SIGNAL(finished(int)), this, SLOT(dialogFinished())); // langDialog->show(); } void DiscoverMainWindow::configureShortcuts() { KShortcutsDialog dlg(KShortcutsEditor::AllActions, KShortcutsEditor::LetterShortcutsAllowed, nullptr); dlg.setModal(true); dlg.addCollection(actionCollection()); qDebug() << "saving shortcuts..." << dlg.configure(/*bSaveSettings*/); } void DiscoverMainWindow::setCompactMode(DiscoverMainWindow::CompactMode mode) { if (m_mode != mode) { m_mode = mode; Q_EMIT compactModeChanged(m_mode); } } class DiscoverTestExecutor : public QObject { public: DiscoverTestExecutor(QObject* rootObject, QQmlEngine* engine, const QUrl &url) : QObject(engine) { connect(engine, &QQmlEngine::quit, this, &DiscoverTestExecutor::finish, Qt::QueuedConnection); QQmlComponent* component = new QQmlComponent(engine, url, engine); m_testObject = component->create(engine->rootContext()); if (!m_testObject) { qWarning() << "error loading test" << url << m_testObject << component->errors(); Q_ASSERT(false); } m_testObject->setProperty("appRoot", QVariant::fromValue(rootObject)); connect(engine, &QQmlEngine::warnings, this, &DiscoverTestExecutor::processWarnings); } void processWarnings(const QList &warnings) { foreach(const QQmlError &warning, warnings) { if (warning.url().path().endsWith(QLatin1String("DiscoverTest.qml"))) { qWarning() << "Test failed!" << warnings; qGuiApp->exit(1); } } m_warnings << warnings; } void finish() { //The CI doesn't seem to have icons, remove when it's not an issue anymore m_warnings.erase(std::remove_if(m_warnings.begin(), m_warnings.end(), [](const QQmlError& err) -> bool { return err.description().contains(QLatin1String("QML Image: Failed to get image from provider: image://icon/")); })); if (m_warnings.isEmpty()) qDebug() << "cool no warnings!"; else qDebug() << "test finished succesfully despite" << m_warnings; qGuiApp->exit(m_warnings.count()); } private: QObject* m_testObject; QList m_warnings; }; void DiscoverMainWindow::loadTest(const QUrl& url) { new DiscoverTestExecutor(rootObject(), engine(), url); } QWindow* DiscoverMainWindow::rootObject() const { return qobject_cast(m_engine->rootObjects().at(0)); } void DiscoverMainWindow::showPassiveNotification(const QString& msg) { QTimer::singleShot(100, this, [this, msg](){ QMetaObject::invokeMethod(rootObject(), "showPassiveNotification", Qt::QueuedConnection, Q_ARG(QVariant, msg), Q_ARG(QVariant, {}), Q_ARG(QVariant, {}), Q_ARG(QVariant, {})); }); } diff --git a/discover/main.cpp b/discover/main.cpp index 6f41f918..5ad63209 100644 --- a/discover/main.cpp +++ b/discover/main.cpp @@ -1,152 +1,155 @@ /* * Copyright (C) 2012 Aleix Pol Gonzalez * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library/Lesser General Public License * version 2, or (at your option) any later version, 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/Lesser 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. */ // #define QT_QML_DEBUG #include #include #include #include #include #include #include #include "DiscoverMainWindow.h" #include #include "DiscoverVersion.h" #include #include typedef QHash StringCompactMode; Q_GLOBAL_STATIC_WITH_ARGS(StringCompactMode, s_decodeCompactMode, (StringCompactMode { { QLatin1String("auto"), DiscoverMainWindow::Auto }, { QLatin1String("compact"), DiscoverMainWindow::Compact }, { QLatin1String("full"), DiscoverMainWindow::Full } })) QCommandLineParser* createParser() { QCommandLineParser* parser = new QCommandLineParser; parser->addOption(QCommandLineOption(QStringLiteral("application"), i18n("Directly open the specified application by its package name."), QStringLiteral("name"))); parser->addOption(QCommandLineOption(QStringLiteral("mime"), i18n("Open with a program that can deal with the given mimetype."), QStringLiteral("name"))); parser->addOption(QCommandLineOption(QStringLiteral("category"), i18n("Display a list of entries with a category."), QStringLiteral("name"))); parser->addOption(QCommandLineOption(QStringLiteral("mode"), i18n("Open Discover in a said mode. Modes correspond to the toolbar buttons."), QStringLiteral("name"))); parser->addOption(QCommandLineOption(QStringLiteral("listmodes"), i18n("List all the available modes."))); parser->addOption(QCommandLineOption(QStringLiteral("compact"), i18n("Compact Mode (auto/compact/full)."), QStringLiteral("mode"), QStringLiteral("auto"))); parser->addOption(QCommandLineOption(QStringLiteral("local-filename"), i18n("Local package file to install"), QStringLiteral("package"))); parser->addOption(QCommandLineOption(QStringLiteral("listbackends"), i18n("List all the available backends."))); parser->addOption(QCommandLineOption(QStringLiteral("test"), QStringLiteral("Test file"), QStringLiteral("file.qml"))); parser->addPositionalArgument(QStringLiteral("urls"), i18n("Supports appstream: url scheme")); DiscoverBackendsFactory::setupCommandLine(parser); KAboutData::applicationData().setupCommandLine(parser); parser->addHelpOption(); parser->addVersionOption(); return parser; } void processArgs(QCommandLineParser* parser, DiscoverMainWindow* mainWindow) { if(parser->isSet(QStringLiteral("application"))) mainWindow->openApplication(QUrl(parser->value(QStringLiteral("application")))); else if(parser->isSet(QStringLiteral("mime"))) mainWindow->openMimeType(parser->value(QStringLiteral("mime"))); else if(parser->isSet(QStringLiteral("category"))) mainWindow->openCategory(parser->value(QStringLiteral("category"))); if(parser->isSet(QStringLiteral("mode"))) mainWindow->openMode(parser->value(QStringLiteral("mode"))); if(parser->isSet(QStringLiteral("local-filename"))) mainWindow->openLocalPackage(QUrl::fromUserInput(parser->value(QStringLiteral("local-filename")), {}, QUrl::AssumeLocalFile)); foreach(const QString &arg, parser->positionalArguments()) { const QUrl url = QUrl::fromUserInput(arg, {}, QUrl::AssumeLocalFile); if (url.isLocalFile()) mainWindow->openLocalPackage(url); else mainWindow->openApplication(url); } } int main(int argc, char** argv) { QApplication app(argc, argv); app.setWindowIcon(QIcon::fromTheme(QStringLiteral("plasmadiscover"))); app.setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); app.setAttribute(Qt::AA_UseHighDpiPixmaps, true); KCrash::initialize(); KLocalizedString::setApplicationDomain("plasma-discover"); KAboutData about(QStringLiteral("discover"), i18n("Discover"), version, i18n("An application explorer"), KAboutLicense::GPL, i18n("© 2010-2016 Plasma Development Team")); about.addAuthor(i18n("Aleix Pol Gonzalez"), QString(), QStringLiteral("aleixpol@blue-systems.com")); about.addAuthor(i18n("Jonathan Thomas"), QString(), QStringLiteral("echidnaman@kubuntu.org")); about.setProductName("discover/discover"); KAboutData::setApplicationData(about); DiscoverMainWindow *mainWindow = nullptr; { QScopedPointer parser(createParser()); parser->process(app); about.processCommandLine(parser.data()); DiscoverBackendsFactory::processCommandLine(parser.data(), parser->isSet(QStringLiteral("test"))); if(parser->isSet(QStringLiteral("listbackends"))) { QTextStream(stdout) << i18n("Available backends:\n"); DiscoverBackendsFactory f; foreach(const QString& name, f.allBackendNames(false, true)) QTextStream(stdout) << " * " << name << '\n'; return 0; } if (parser->isSet(QStringLiteral("test"))) { QStandardPaths::setTestModeEnabled(true); } KDBusService* service = new KDBusService(KDBusService::Unique, &app); mainWindow = new DiscoverMainWindow(s_decodeCompactMode->value(parser->value(QStringLiteral("compact")), DiscoverMainWindow::Full)); QObject::connect(&app, &QCoreApplication::aboutToQuit, mainWindow, &DiscoverMainWindow::deleteLater); QObject::connect(service, &KDBusService::activateRequested, mainWindow, [mainWindow](const QStringList &arguments, const QString &/*workingDirectory*/){ + if (!mainWindow->rootObject()) + QCoreApplication::instance()->quit(); + mainWindow->rootObject()->raise(); if (arguments.isEmpty()) return; QScopedPointer parser(createParser()); parser->parse(arguments); processArgs(parser.data(), mainWindow); }); processArgs(parser.data(), mainWindow); if(parser->isSet(QStringLiteral("listmodes"))) { QTextStream(stdout) << i18n("Available modes:\n"); foreach(const QString& mode, mainWindow->modes()) QTextStream(stdout) << " * " << mode << '\n'; delete mainWindow; return 0; } if (parser->isSet(QStringLiteral("test"))) { const QUrl testFile = QUrl::fromUserInput(parser->value(QStringLiteral("test")), {}, QUrl::AssumeLocalFile); Q_ASSERT(!testFile.isEmpty() && testFile.isLocalFile()); mainWindow->loadTest(testFile); } } return app.exec(); } diff --git a/libdiscover/Category/Category.h b/libdiscover/Category/Category.h index f947323e..cc7355fd 100644 --- a/libdiscover/Category/Category.h +++ b/libdiscover/Category/Category.h @@ -1,92 +1,91 @@ /*************************************************************************** * Copyright © 2010 Jonathan Thomas * * * * 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) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * 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 . * ***************************************************************************/ #ifndef CATEGORY_H #define CATEGORY_H #include #include #include #include #include #include "discovercommon_export.h" class QDomNode; enum FilterType { InvalidFilter, CategoryFilter, PkgSectionFilter, PkgWildcardFilter, PkgNameFilter, AppstreamIdWildcardFilter }; class DISCOVERCOMMON_EXPORT Category : public QObject { Q_OBJECT public: Q_PROPERTY(QString name READ name CONSTANT) Q_PROPERTY(QString icon READ icon CONSTANT) Q_PROPERTY(QObject* parent READ parent CONSTANT) Q_PROPERTY(QUrl decoration READ decoration CONSTANT) Q_PROPERTY(QVariantList subcategories READ subCategoriesVariant CONSTANT) explicit Category(QSet pluginNames, QObject* parent = nullptr); Category(const QString& name, const QString& iconName, const QVector< QPair< FilterType, QString > >& orFilters, const QSet &pluginName, const QVector& subCategories, const QUrl& decoration, bool isAddons); ~Category() override; QString name() const; QString icon() const; QVector > andFilters() const; QVector > orFilters() const; QVector > notFilters() const; QVector subCategories() const; QVariantList subCategoriesVariant() const; static void addSubcategory(QVector& list, Category* cat); void parseData(const QString& path, const QDomNode& data); bool blacklistPlugins(const QSet& pluginName); bool isAddons() const { return m_isAddons; } QUrl decoration() const; bool matchesCategoryName(const QString &name) const; Q_SCRIPTABLE bool contains(Category* cat) const; Q_SCRIPTABLE bool contains(const QVariantList &cats) const; static bool categoryLessThan(Category *c1, const Category *c2); private: QString m_name; QString m_iconString; QUrl m_decoration; QVector > m_andFilters; QVector > m_orFilters; QVector > m_notFilters; QVector m_subCategories; QVector > parseIncludes(const QDomNode &data); QSet m_plugins; bool m_isAddons = false; }; -Q_DECLARE_METATYPE(QList) #endif diff --git a/libdiscover/backends/KNSBackend/KNSBackend.cpp b/libdiscover/backends/KNSBackend/KNSBackend.cpp index 971e7b1e..0ea70dcd 100644 --- a/libdiscover/backends/KNSBackend/KNSBackend.cpp +++ b/libdiscover/backends/KNSBackend/KNSBackend.cpp @@ -1,409 +1,407 @@ /*************************************************************************** * Copyright © 2012 Aleix Pol Gonzalez * * * * 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) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * 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 . * ***************************************************************************/ // Qt includes #include #include #include #include #include #include // Attica includes #include #include // KDE includes #include #include #include #include #include #include // DiscoverCommon includes #include "Transaction/Transaction.h" #include "Category/Category.h" // Own includes #include "KNSBackend.h" #include "KNSResource.h" #include "KNSReviews.h" #include #include "utils.h" class KNSBackendFactory : public AbstractResourcesBackendFactory { Q_OBJECT Q_PLUGIN_METADATA(IID "org.kde.muon.AbstractResourcesBackendFactory") Q_INTERFACES(AbstractResourcesBackendFactory) public: KNSBackendFactory() { connect(KNSCore::QuestionManager::instance(), &KNSCore::QuestionManager::askQuestion, this, [](KNSCore::Question* q) { qWarning() << q->question() << q->questionType(); q->setResponse(KNSCore::Question::InvalidResponse); }); } QVector newInstance(QObject* parent, const QString &/*name*/) const override { QVector ret; for (const QString &path: QStandardPaths::standardLocations(QStandardPaths::GenericConfigLocation)) { QDirIterator dirIt(path, {QStringLiteral("*.knsrc")}, QDir::Files); for(; dirIt.hasNext(); ) { dirIt.next(); auto bk = new KNSBackend(parent, QStringLiteral("plasma"), dirIt.filePath()); ret += bk; } } return ret; } }; KNSBackend::KNSBackend(QObject* parent, const QString& iconName, const QString &knsrc) : AbstractResourcesBackend(parent) , m_fetching(false) , m_isValid(true) , m_reviews(new KNSReviews(this)) , m_name(knsrc) , m_iconName(iconName) , m_updater(new StandardBackendUpdater(this)) { const QString fileName = QFileInfo(m_name).fileName(); setName(fileName); setObjectName(knsrc); const KConfig conf(m_name); if (!conf.hasGroup("KNewStuff3")) { markInvalid(QStringLiteral("Config group not found! Check your KNS3 installation.")); return; } m_categories = QStringList{ fileName }; const KConfigGroup group = conf.group("KNewStuff3"); m_extends = group.readEntry("Extends", QStringList()); m_reviews->setProviderUrl(QUrl(group.readEntry("ProvidersUrl", QString()))); setFetching(true); m_engine = new KNSCore::Engine(this); m_engine->init(m_name); #if KNEWSTUFFCORE_VERSION_MAJOR==5 && KNEWSTUFFCORE_VERSION_MINOR>=36 m_engine->setPageSize(100); #endif // Setting setFetching to false when we get an error ensures we don't end up in an eternally-fetching state connect(m_engine, &KNSCore::Engine::signalError, this, [this](const QString &error) { if(error == QLatin1Literal("All categories are missing")) { markInvalid(error); } m_responsePending = false; Q_EMIT searchFinished(); Q_EMIT availableForQueries(); this->setFetching(false); qWarning() << "kns error" << objectName() << error; passiveMessage(i18n("%1: %2", name(), error)); }); connect(m_engine, &KNSCore::Engine::signalEntriesLoaded, this, &KNSBackend::receivedEntries, Qt::QueuedConnection); connect(m_engine, &KNSCore::Engine::signalEntryChanged, this, &KNSBackend::statusChanged); connect(m_engine, &KNSCore::Engine::signalEntryDetailsLoaded, this, &KNSBackend::statusChanged); connect(m_engine, &KNSCore::Engine::signalProvidersLoaded, this, &KNSBackend::fetchInstalled); const QVector> filters = { {CategoryFilter, fileName } }; const QSet backendName = { name() }; QString displayName = group.readEntry("Name", QString()); if (displayName.isEmpty()) { displayName = fileName.mid(0, fileName.indexOf(QLatin1Char('.'))); displayName[0] = displayName[0].toUpper(); } static const QSet knsrcPlasma = { QStringLiteral("aurorae.knsrc"), QStringLiteral("icons.knsrc"), QStringLiteral("kfontinst.knsrc"), QStringLiteral("lookandfeel.knsrc"), QStringLiteral("plasma-themes.knsrc"), QStringLiteral("plasmoids.knsrc"), QStringLiteral("wallpaper.knsrc"), QStringLiteral("xcursor.knsrc"), QStringLiteral("cgcgtk3.knsrc"), QStringLiteral("cgcicon.knsrc"), QStringLiteral("cgctheme.knsrc"), //GTK integration QStringLiteral("kwinswitcher.knsrc"), QStringLiteral("kwineffect.knsrc"), QStringLiteral("kwinscripts.knsrc") //KWin }; auto actualCategory = new Category(displayName, QStringLiteral("plasma"), filters, backendName, {}, QUrl(), true); const auto topLevelName = knsrcPlasma.contains(fileName)? i18n("Plasma Addons") : i18n("Application Addons"); const QUrl decoration(knsrcPlasma.contains(fileName)? QStringLiteral("https://c2.staticflickr.com/4/3148/3042248532_20bd2e38f4_b.jpg") : QStringLiteral("https://c2.staticflickr.com/8/7067/6847903539_d9324dcd19_o.jpg")); auto addonsCategory = new Category(topLevelName, QStringLiteral("plasma"), filters, backendName, {actualCategory}, decoration, true); m_rootCategories = { addonsCategory }; } KNSBackend::~KNSBackend() = default; void KNSBackend::markInvalid(const QString &message) { qWarning() << "invalid kns backend!" << m_name << "because:" << message; m_isValid = false; setFetching(false); } void KNSBackend::fetchInstalled() { auto search = new OneTimeAction([this]() { Q_EMIT startingSearch(); m_onePage = true; m_responsePending = true; m_engine->checkForInstalled(); }, this); if (m_responsePending) { connect(this, &KNSBackend::availableForQueries, search, &OneTimeAction::trigger, Qt::QueuedConnection); } else { search->trigger(); } } void KNSBackend::setFetching(bool f) { if(m_fetching!=f) { m_fetching = f; emit fetchingChanged(); } } bool KNSBackend::isValid() const { return m_isValid; } KNSResource* KNSBackend::resourceForEntry(const KNSCore::EntryInternal& entry) { KNSResource* r = static_cast(m_resourcesByName.value(entry.uniqueId())); if (!r) { r = new KNSResource(entry, m_categories, this); m_resourcesByName.insert(entry.uniqueId(), r); } else { r->setEntry(entry); } return r; } void KNSBackend::receivedEntries(const KNSCore::EntryInternal::List& entries) { m_responsePending = false; const auto resources = kTransform>(entries, [this](const KNSCore::EntryInternal& entry){ return resourceForEntry(entry); }); if (!resources.isEmpty()) { Q_EMIT receivedResources(resources); } if(resources.isEmpty()) { Q_EMIT searchFinished(); Q_EMIT availableForQueries(); setFetching(false); return; } // qDebug() << "received" << objectName() << this << m_resourcesByName.count(); if (!m_responsePending && !m_onePage) { // We _have_ to set this first. If we do not, we may run into a situation where the // data request will conclude immediately, causing m_responsePending to remain true // for perpetuity as the slots will be called before the function returns. m_responsePending = true; m_engine->requestMoreData(); } else { Q_EMIT availableForQueries(); } } void KNSBackend::statusChanged(const KNSCore::EntryInternal& entry) { resourceForEntry(entry); } class KNSTransaction : public Transaction { public: KNSTransaction(QObject* parent, KNSResource* res, Transaction::Role role) : Transaction(parent, res, role) , m_id(res->entry().uniqueId()) { setCancellable(false); auto manager = res->knsBackend()->engine(); connect(manager, &KNSCore::Engine::signalEntryChanged, this, &KNSTransaction::anEntryChanged); } void anEntryChanged(const KNSCore::EntryInternal& entry) { if (entry.uniqueId() == m_id) { switch (entry.status()) { case KNS3::Entry::Invalid: qWarning() << "invalid status for" << entry.uniqueId() << entry.status(); break; case KNS3::Entry::Installing: case KNS3::Entry::Updating: setStatus(CommittingStatus); break; case KNS3::Entry::Downloadable: case KNS3::Entry::Installed: case KNS3::Entry::Deleted: case KNS3::Entry::Updateable: if (status() != DoneStatus) { setStatus(DoneStatus); } break; } } } void cancel() override {} private: const QString m_id; }; Transaction* KNSBackend::removeApplication(AbstractResource* app) { auto res = qobject_cast(app); auto t = new KNSTransaction(this, res, Transaction::RemoveRole); m_engine->uninstall(res->entry()); return t; } Transaction* KNSBackend::installApplication(AbstractResource* app) { auto res = qobject_cast(app); m_engine->install(res->entry()); return new KNSTransaction(this, res, Transaction::InstallRole); } Transaction* KNSBackend::installApplication(AbstractResource* app, const AddonList& /*addons*/) { return installApplication(app); } int KNSBackend::updatesCount() const { return m_updater->updatesCount(); } AbstractReviewsBackend* KNSBackend::reviewsBackend() const { return m_reviews; } static ResultsStream* voidStream() { return new ResultsStream(QStringLiteral("KNS-void"), {}); } ResultsStream* KNSBackend::search(const AbstractResourcesBackend::Filters& filter) { - Q_ASSERT(isValid()); - - if ((!filter.resourceUrl.isEmpty() && filter.resourceUrl.scheme() != QLatin1String("kns")) || !filter.mimetype.isEmpty()) + if (!m_isValid || (!filter.resourceUrl.isEmpty() && filter.resourceUrl.scheme() != QLatin1String("kns")) || !filter.mimetype.isEmpty()) return voidStream(); if (filter.resourceUrl.scheme() == QLatin1String("kns")) { return findResourceByPackageName(filter.resourceUrl); } else if (filter.state >= AbstractResource::Installed) { QVector ret; foreach(AbstractResource* r, m_resourcesByName) { if(r->state()>=filter.state && (r->name().contains(filter.search, Qt::CaseInsensitive) || r->comment().contains(filter.search, Qt::CaseInsensitive))) ret += r; } return new ResultsStream(QStringLiteral("KNS-installed"), ret); } else if (filter.category && filter.category->matchesCategoryName(m_categories.constFirst())) { return searchStream(filter.search); } else if (!filter.category && !filter.search.isEmpty()) { return searchStream(filter.search); } return voidStream(); } ResultsStream* KNSBackend::searchStream(const QString &searchText) { Q_EMIT startingSearch(); auto stream = new ResultsStream(QStringLiteral("KNS-search-")+name()); auto start = [this, stream, searchText]() { // No need to explicitly launch a search, setting the search term already does that for us m_engine->setSearchTerm(searchText); m_onePage = false; m_responsePending = true; connect(this, &KNSBackend::receivedResources, stream, &ResultsStream::resourcesFound); connect(this, &KNSBackend::searchFinished, stream, &ResultsStream::finish); connect(this, &KNSBackend::startingSearch, stream, &ResultsStream::finish); }; if (m_responsePending) { connect(this, &KNSBackend::availableForQueries, stream, start, Qt::QueuedConnection); } else { start(); } return stream; } ResultsStream * KNSBackend::findResourceByPackageName(const QUrl& search) { if (search.scheme() != QLatin1String("kns") || search.host() != name()) return voidStream(); const auto pathParts = search.path().split(QLatin1Char('/'), QString::SkipEmptyParts); if (pathParts.size() != 2) { passiveMessage(i18n("Wrong KNewStuff URI: %1", search.toString())); return voidStream(); } const auto providerid = pathParts.at(0); const auto entryid = pathParts.at(1); auto stream = new ResultsStream(QStringLiteral("KNS-byname-")+entryid); auto start = [this, entryid, stream, providerid]() { m_responsePending = true; m_engine->fetchEntryById(entryid); m_onePage = false; connect(m_engine, &KNSCore::Engine::signalError, stream, &ResultsStream::finish); connect(m_engine, &KNSCore::Engine::signalEntryDetailsLoaded, stream, [this, stream, entryid, providerid](const KNSCore::EntryInternal &entry) { if (entry.uniqueId() == entryid && providerid == QUrl(entry.providerId()).host()) { stream->resourcesFound({resourceForEntry(entry)}); } m_responsePending = false; QTimer::singleShot(0, this, &KNSBackend::availableForQueries); stream->finish(); }); }; if (m_responsePending) { connect(this, &KNSBackend::availableForQueries, stream, start); } else { start(); } return stream; } bool KNSBackend::isFetching() const { return m_fetching; } AbstractBackendUpdater* KNSBackend::backendUpdater() const { return m_updater; } QString KNSBackend::displayName() const { return QStringLiteral("KNewStuff"); } #include "KNSBackend.moc" diff --git a/libdiscover/backends/PackageKitBackend/PackageKitBackend.cpp b/libdiscover/backends/PackageKitBackend/PackageKitBackend.cpp index 472fd906..2f520ed1 100644 --- a/libdiscover/backends/PackageKitBackend/PackageKitBackend.cpp +++ b/libdiscover/backends/PackageKitBackend/PackageKitBackend.cpp @@ -1,628 +1,631 @@ /*************************************************************************** * Copyright © 2012 Aleix Pol Gonzalez * * Copyright © 2013 Lukas Appelhans * * * * 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) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * 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 . * ***************************************************************************/ #include "PackageKitBackend.h" #include "PackageKitSourcesBackend.h" #include "PackageKitResource.h" #include "PackageKitUpdater.h" #include "AppPackageKitResource.h" #include "PKTransaction.h" #include "LocalFilePKResource.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "utils.h" #include "config-paths.h" MUON_BACKEND_PLUGIN(PackageKitBackend) static QString locateService(const QString &filename) { return QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("applications/")+filename); } PackageKitBackend::PackageKitBackend(QObject* parent) : AbstractResourcesBackend(parent) , m_updater(new PackageKitUpdater(this)) , m_refresher(nullptr) , m_isFetching(0) , m_reviews(AppStreamIntegration::global()->reviews()) { QTimer* t = new QTimer(this); connect(t, &QTimer::timeout, this, &PackageKitBackend::refreshDatabase); t->setInterval(60 * 60 * 1000); t->setSingleShot(false); t->start(); m_delayedDetailsFetch.setSingleShot(true); m_delayedDetailsFetch.setInterval(0); connect(&m_delayedDetailsFetch, &QTimer::timeout, this, &PackageKitBackend::performDetailsFetch); // Kubuntu-based auto service = locateService(QStringLiteral("software-properties-kde.desktop")); if (!service.isEmpty()) m_messageActions += createActionForService(service); // openSUSE-based service = locateService(QStringLiteral("YaST2/sw_source.desktop")); if (!service.isEmpty()) m_messageActions += createActionForService(service); connect(PackageKit::Daemon::global(), &PackageKit::Daemon::updatesChanged, this, &PackageKitBackend::fetchUpdates); connect(PackageKit::Daemon::global(), &PackageKit::Daemon::isRunningChanged, this, &PackageKitBackend::checkDaemonRunning); connect(m_reviews.data(), &OdrsReviewsBackend::ratingsReady, this, &AbstractResourcesBackend::emitRatingsReady); SourcesModel::global()->addSourcesBackend(new PackageKitSourcesBackend(this)); QTimer::singleShot(0, this, &PackageKitBackend::delayedInit); } PackageKitBackend::~PackageKitBackend() { } void PackageKitBackend::delayedInit() { QString error; const bool b = m_appdata.load(&error); reloadPackageList(); if (!b && m_packages.packages.isEmpty()) { qWarning() << "Could not open the AppStream metadata pool" << error; QTimer::singleShot(0, this, [this]() { Q_EMIT passiveMessage(i18n("Please make sure that Appstream is properly set up on your system")); }); } } QAction* PackageKitBackend::createActionForService(const QString &servicePath) { QAction* action = new QAction(this); KDesktopFile parser(servicePath); action->setIcon(QIcon::fromTheme(parser.readIcon())); action->setText(parser.readName()); connect(action, &QAction::triggered, action, [servicePath, this](){ bool b = QProcess::startDetached(QStringLiteral(CMAKE_INSTALL_FULL_LIBEXECDIR_KF5 "/discover/runservice"), {servicePath}); if (!b) qWarning() << "Could not start" << servicePath; }); return action; } bool PackageKitBackend::isFetching() const { return m_isFetching; } void PackageKitBackend::acquireFetching(bool f) { if (f) m_isFetching++; else m_isFetching--; if ((!f && m_isFetching==0) || (f && m_isFetching==1)) { emit fetchingChanged(); } Q_ASSERT(m_isFetching>=0); } void PackageKitBackend::reloadPackageList() { acquireFetching(true); if (m_refresher) { disconnect(m_refresher.data(), &PackageKit::Transaction::finished, this, &PackageKitBackend::reloadPackageList); } const auto components = m_appdata.components(); QStringList neededPackages; neededPackages.reserve(components.size()); foreach(const AppStream::Component& component, components) { if (component.kind() == AppStream::Component::KindFirmware) continue; const auto pkgNames = component.packageNames(); if (pkgNames.isEmpty()) { if (component.kind() == AppStream::Component::KindDesktopApp) { const QString file = locateService(component.desktopId()); if (!file.isEmpty()) { auto trans = PackageKit::Daemon::searchFiles(file); connect(trans, &PackageKit::Transaction::package, this, [trans](PackageKit::Transaction::Info info, const QString &packageID){ if (info == PackageKit::Transaction::InfoInstalled) trans->setProperty("installedPackage", packageID); }); connect(trans, &PackageKit::Transaction::finished, this, [this, trans, component](PackageKit::Transaction::Exit status) { const auto pkgidVal = trans->property("installedPackage"); if (status == PackageKit::Transaction::ExitSuccess && !pkgidVal.isNull()) { const auto pkgid = pkgidVal.toString(); acquireFetching(true); auto res = addComponent(component, {PackageKit::Daemon::packageName(pkgid)}); res->addPackageId(PackageKit::Transaction::InfoInstalled, pkgid, true); acquireFetching(false); } }); continue; } } qDebug() << "no packages for" << component.name(); continue; } neededPackages += pkgNames; addComponent(component, pkgNames); } acquireFetching(false); neededPackages.removeDuplicates(); resolvePackages(neededPackages); } AppPackageKitResource* PackageKitBackend::addComponent(const AppStream::Component& component, const QStringList& pkgNames) { Q_ASSERT(isFetching()); Q_ASSERT(!pkgNames.isEmpty()); const auto res = new AppPackageKitResource(component, pkgNames.at(0), this); m_packages.packages[component.id()] = res; foreach (const QString& pkg, pkgNames) { m_packages.packageToApp[pkg] += component.id(); } foreach (const QString& pkg, component.extends()) { m_packages.extendedBy[pkg] += res; } return res; } class TransactionSet : public QObject { Q_OBJECT public: TransactionSet(const QVector &transactions) : m_transactions(transactions) { foreach(PackageKit::Transaction* t, transactions) { connect(t, &PackageKit::Transaction::finished, this, &TransactionSet::transactionFinished); } } void transactionFinished(PackageKit::Transaction::Exit exit) { PackageKit::Transaction* t = qobject_cast(sender()); if (exit != PackageKit::Transaction::ExitSuccess) { qWarning() << "failed" << exit << t; } m_transactions.removeAll(t); if (m_transactions.isEmpty()) { Q_EMIT allFinished(); } } Q_SIGNALS: void allFinished(); private: QVector m_transactions; }; void PackageKitBackend::clearPackages(const QStringList& packageNames) { const auto resources = resourcesByPackageNames>(packageNames); for(auto res: resources) { qobject_cast(res)->clearPackageIds(); } } void PackageKitBackend::resolvePackages(const QStringList &packageNames) { PackageKit::Transaction * tArch = PackageKit::Daemon::resolve(packageNames, PackageKit::Transaction::FilterArch); connect(tArch, &PackageKit::Transaction::package, this, &PackageKitBackend::addPackageArch); connect(tArch, &PackageKit::Transaction::errorCode, this, &PackageKitBackend::transactionError); PackageKit::Transaction * tNotArch = PackageKit::Daemon::resolve(packageNames, PackageKit::Transaction::FilterNotArch); connect(tNotArch, &PackageKit::Transaction::package, this, &PackageKitBackend::addPackageNotArch); connect(tNotArch, &PackageKit::Transaction::errorCode, this, &PackageKitBackend::transactionError); TransactionSet* merge = new TransactionSet({tArch, tNotArch}); connect(merge, &TransactionSet::allFinished, this, &PackageKitBackend::getPackagesFinished); fetchUpdates(); } void PackageKitBackend::fetchUpdates() { + if (m_updater->isProgressing()) + return; + PackageKit::Transaction * tUpdates = PackageKit::Daemon::getUpdates(); connect(tUpdates, &PackageKit::Transaction::finished, this, &PackageKitBackend::getUpdatesFinished); connect(tUpdates, &PackageKit::Transaction::package, this, &PackageKitBackend::addPackageToUpdate); connect(tUpdates, &PackageKit::Transaction::errorCode, this, &PackageKitBackend::transactionError); m_updatesPackageId.clear(); m_updater->setProgressing(true); } void PackageKitBackend::addPackageArch(PackageKit::Transaction::Info info, const QString& packageId, const QString& summary) { addPackage(info, packageId, summary, true); } void PackageKitBackend::addPackageNotArch(PackageKit::Transaction::Info info, const QString& packageId, const QString& summary) { addPackage(info, packageId, summary, false); } void PackageKitBackend::addPackage(PackageKit::Transaction::Info info, const QString &packageId, const QString &summary, bool arch) { const QString packageName = PackageKit::Daemon::packageName(packageId); QSet r = resourcesByPackageName(packageName); if (r.isEmpty()) { auto pk = new PackageKitResource(packageName, summary, this); r = { pk }; m_packagesToAdd.insert(pk); } foreach(auto res, r) static_cast(res)->addPackageId(info, packageId, arch); } void PackageKitBackend::getPackagesFinished() { for(auto it = m_packages.packages.cbegin(); it != m_packages.packages.cend(); ++it) { auto pkr = qobject_cast(it.value()); if (pkr->packages().isEmpty()) { qWarning() << "Failed to find package for" << it.key(); m_packagesToDelete += pkr; } } includePackagesToAdd(); } void PackageKitBackend::includePackagesToAdd() { if (m_packagesToAdd.isEmpty() && m_packagesToDelete.isEmpty()) return; acquireFetching(true); foreach(PackageKitResource* res, m_packagesToAdd) { m_packages.packages[res->packageName()] = res; } foreach(PackageKitResource* res, m_packagesToDelete) { const auto pkgs = m_packages.packageToApp.value(res->packageName(), {res->packageName()}); foreach(const auto &pkg, pkgs) { auto res = m_packages.packages.take(pkg); if (res) { emit resourceRemoved(res); res->deleteLater(); } } } m_packagesToAdd.clear(); m_packagesToDelete.clear(); acquireFetching(false); } void PackageKitBackend::transactionError(PackageKit::Transaction::Error, const QString& message) { qWarning() << "Transaction error: " << message << sender(); Q_EMIT passiveMessage(message); } void PackageKitBackend::packageDetails(const PackageKit::Details& details) { const QSet resources = resourcesByPackageName(PackageKit::Daemon::packageName(details.packageId())); if (resources.isEmpty()) qWarning() << "couldn't find package for" << details.packageId(); foreach(AbstractResource* res, resources) { qobject_cast(res)->setDetails(details); } } QSet PackageKitBackend::resourcesByPackageName(const QString& name) const { return resourcesByPackageNames>({name}); } template T PackageKitBackend::resourcesByPackageNames(const QStringList &pkgnames) const { T ret; ret.reserve(pkgnames.size()); for(const QString &name : pkgnames) { const QStringList names = m_packages.packageToApp.value(name, QStringList(name)); foreach(const QString& name, names) { AbstractResource* res = m_packages.packages.value(name); if (res) ret += res; } } return ret; } void PackageKitBackend::checkForUpdates() { refreshDatabase(); } void PackageKitBackend::refreshDatabase() { if (!m_refresher) { acquireFetching(true); m_refresher = PackageKit::Daemon::refreshCache(false); connect(m_refresher.data(), &PackageKit::Transaction::errorCode, this, &PackageKitBackend::transactionError); connect(m_refresher.data(), &PackageKit::Transaction::finished, this, [this]() { reloadPackageList(); acquireFetching(false); delete m_refresher; }); } else { qWarning() << "already resetting"; } } ResultsStream* PackageKitBackend::search(const AbstractResourcesBackend::Filters& filter) { if (!filter.resourceUrl.isEmpty()) { return findResourceByPackageName(filter.resourceUrl); } else if (filter.search.isEmpty()) { return new ResultsStream(QStringLiteral("PackageKitStream-all"), kFilter>(m_packages.packages, [](AbstractResource* res) { return !res->isTechnical(); })); } else { const QList components = m_appdata.search(filter.search); const QStringList ids = kTransform(components, [](const AppStream::Component& comp) { return comp.id(); }); auto stream = new ResultsStream(QStringLiteral("PackageKitStream-search")); if (!ids.isEmpty()) { const auto resources = resourcesByPackageNames>(ids); QTimer::singleShot(0, this, [stream, resources] () { stream->resourcesFound(resources); }); } PackageKit::Transaction * tArch = PackageKit::Daemon::resolve(filter.search, PackageKit::Transaction::FilterArch); connect(tArch, &PackageKit::Transaction::package, this, &PackageKitBackend::addPackageArch); connect(tArch, &PackageKit::Transaction::package, this, [tArch](PackageKit::Transaction::Info /*info*/, const QString &packageId){ tArch->setProperty("packageId", packageId); }); connect(tArch, &PackageKit::Transaction::finished, stream, [stream, tArch, ids, this](PackageKit::Transaction::Exit status) { getPackagesFinished(); if (status == PackageKit::Transaction::Exit::ExitSuccess) { const auto packageId = tArch->property("packageId"); if (!packageId.isNull()) { const auto res = resourcesByPackageNames>({PackageKit::Daemon::packageName(packageId.toString())}); stream->resourcesFound(kFilter>(res, [ids](AbstractResource* res){ return !ids.contains(res->appstreamId()); })); } } stream->finish(); }, Qt::QueuedConnection); return stream; } } ResultsStream * PackageKitBackend::findResourceByPackageName(const QUrl& url) { AbstractResource* pkg = nullptr; if (url.scheme() == QLatin1String("appstream")) { if (url.host().isEmpty()) passiveMessage(i18n("Malformed appstream url '%1'", url.toDisplayString())); else pkg = m_packages.packages.value(url.host()); } return new ResultsStream(QStringLiteral("PackageKitStream-url"), pkg ? QVector{pkg} : QVector{}); } int PackageKitBackend::updatesCount() const { return m_updatesPackageId.count(); } Transaction* PackageKitBackend::installApplication(AbstractResource* app, const AddonList& addons) { Transaction* t = nullptr; if(!addons.addonsToInstall().isEmpty()) { QVector appsToInstall; if(!app->isInstalled()) appsToInstall << app; foreach(const QString& toInstall, addons.addonsToInstall()) { appsToInstall += m_packages.packages.value(toInstall); Q_ASSERT(appsToInstall.last()); } t = new PKTransaction(appsToInstall, Transaction::ChangeAddonsRole); } if (!addons.addonsToRemove().isEmpty()) { QVector appsToRemove = kTransform>(addons.addonsToRemove(), [this](const QString& toRemove){ return m_packages.packages.value(toRemove); }); t = new PKTransaction(appsToRemove, Transaction::RemoveRole); } if (!app->isInstalled()) t = installApplication(app); return t; } Transaction* PackageKitBackend::installApplication(AbstractResource* app) { return new PKTransaction({app}, Transaction::InstallRole); } Transaction* PackageKitBackend::removeApplication(AbstractResource* app) { Q_ASSERT(!isFetching()); return new PKTransaction({app}, Transaction::RemoveRole); } QSet PackageKitBackend::upgradeablePackages() const { QSet ret; ret.reserve(m_updatesPackageId.size()); Q_FOREACH (const QString& pkgid, m_updatesPackageId) { const QString pkgname = PackageKit::Daemon::packageName(pkgid); const auto pkgs = resourcesByPackageName(pkgname); if (pkgs.isEmpty()) { qWarning() << "couldn't find resource for" << pkgid; } ret.unite(pkgs); } return ret; } void PackageKitBackend::addPackageToUpdate(PackageKit::Transaction::Info info, const QString& packageId, const QString& summary) { if (info != PackageKit::Transaction::InfoBlocked) { m_updatesPackageId += packageId; addPackage(info, packageId, summary, true); } } void PackageKitBackend::getUpdatesFinished(PackageKit::Transaction::Exit, uint) { if (!m_updatesPackageId.isEmpty()) { PackageKit::Transaction* transaction = PackageKit::Daemon::getDetails(m_updatesPackageId.toList()); connect(transaction, &PackageKit::Transaction::details, this, &PackageKitBackend::packageDetails); connect(transaction, &PackageKit::Transaction::errorCode, this, &PackageKitBackend::transactionError); connect(transaction, &PackageKit::Transaction::finished, this, &PackageKitBackend::getUpdatesDetailsFinished); } m_updater->setProgressing(false); includePackagesToAdd(); emit updatesCountChanged(); } void PackageKitBackend::getUpdatesDetailsFinished(PackageKit::Transaction::Exit exit, uint) { if (exit != PackageKit::Transaction::ExitSuccess) { qWarning() << "Couldn't figure out the updates on PackageKit backend" << exit; } } bool PackageKitBackend::isPackageNameUpgradeable(const PackageKitResource* res) const { return !upgradeablePackageId(res).isEmpty(); } QString PackageKitBackend::upgradeablePackageId(const PackageKitResource* res) const { QString name = res->packageName(); foreach (const QString& pkgid, m_updatesPackageId) { if (PackageKit::Daemon::packageName(pkgid) == name) return pkgid; } return QString(); } void PackageKitBackend::fetchDetails(const QString& pkgid) { if (!m_delayedDetailsFetch.isActive()) { m_delayedDetailsFetch.start(); } m_packageNamesToFetchDetails += pkgid; } void PackageKitBackend::performDetailsFetch() { Q_ASSERT(!m_packageNamesToFetchDetails.isEmpty()); PackageKit::Transaction* transaction = PackageKit::Daemon::getDetails(m_packageNamesToFetchDetails.toList()); connect(transaction, &PackageKit::Transaction::details, this, &PackageKitBackend::packageDetails); connect(transaction, &PackageKit::Transaction::errorCode, this, &PackageKitBackend::transactionError); } void PackageKitBackend::checkDaemonRunning() { if (!PackageKit::Daemon::isRunning()) { qWarning() << "PackageKit stopped running!"; } } AbstractBackendUpdater* PackageKitBackend::backendUpdater() const { return m_updater; } QList PackageKitBackend::messageActions() const { return m_messageActions; } QVector PackageKitBackend::extendedBy(const QString& id) const { return m_packages.extendedBy[id]; } AbstractReviewsBackend* PackageKitBackend::reviewsBackend() const { return m_reviews.data(); } AbstractResource * PackageKitBackend::resourceForFile(const QUrl& file) { QMimeDatabase db; const auto mime = db.mimeTypeForUrl(file); if ( mime.inherits(QLatin1String("application/vnd.debian.binary-package")) || mime.inherits(QLatin1String("application/x-rpm")) || mime.inherits(QLatin1String("application/x-tar")) || mime.inherits(QLatin1String("application/x-xz-compressed-tar")) ) { return new LocalFilePKResource(file, this); } return nullptr; } static QString readDistroName() { QProcess process; process.setEnvironment({QStringLiteral("LC_ALL=C")}); process.start(QStringLiteral("lsb_release"), {QStringLiteral("-sd")}); process.waitForFinished(); auto output = process.readAll().trimmed(); if (output.startsWith('\"') && output.endsWith('\"')) output = output.mid(1, output.length()-2); return QString::fromLocal8Bit(output); } QString PackageKitBackend::displayName() const { static const QString distro = readDistroName(); return distro; } #include "PackageKitBackend.moc" diff --git a/libdiscover/backends/PackageKitBackend/PackageKitNotifier.cpp b/libdiscover/backends/PackageKitBackend/PackageKitNotifier.cpp index 5ce030f6..97f083e4 100644 --- a/libdiscover/backends/PackageKitBackend/PackageKitNotifier.cpp +++ b/libdiscover/backends/PackageKitBackend/PackageKitNotifier.cpp @@ -1,167 +1,160 @@ /*************************************************************************** * Copyright © 2013 Lukas Appelhans * * Copyright © 2015 Aleix Pol Gonzalez * * * * 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) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * 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 . * ***************************************************************************/ #include "PackageKitNotifier.h" #include #include #include #include #include #include #include PackageKitNotifier::PackageKitNotifier(QObject* parent) : BackendNotifierModule(parent) - , m_update(NoUpdate) , m_securityUpdates(0) , m_normalUpdates(0) { if (PackageKit::Daemon::global()->isRunning()) { recheckSystemUpdateNeeded(); } connect(PackageKit::Daemon::global(), &PackageKit::Daemon::networkStateChanged, this, &PackageKitNotifier::recheckSystemUpdateNeeded); connect(PackageKit::Daemon::global(), &PackageKit::Daemon::updatesChanged, this, &PackageKitNotifier::recheckSystemUpdateNeeded); connect(PackageKit::Daemon::global(), &PackageKit::Daemon::isRunningChanged, this, &PackageKitNotifier::recheckSystemUpdateNeeded); //Check if there's packages after 5' QTimer::singleShot(5 * 60 * 1000, this, &PackageKitNotifier::refreshDatabase); int interval = 24 * 60 * 60 * 1000; QTimer *regularCheck = new QTimer(this); regularCheck->setInterval(interval); //refresh at least once every day connect(regularCheck, &QTimer::timeout, this, &PackageKitNotifier::refreshDatabase); const QString aptconfig = QStandardPaths::findExecutable(QStringLiteral("apt-config")); if (!aptconfig.isEmpty()) { auto process = checkAptVariable(aptconfig, QLatin1String("Apt::Periodic::Update-Package-Lists"), [regularCheck](const QStringRef& value) { bool ok; int time = value.toInt(&ok); if (ok && time > 0) regularCheck->setInterval(time * 60 * 60 * 1000); else qWarning() << "couldn't understand value for timer:" << value; }); connect(process, static_cast(&QProcess::finished), regularCheck, static_cast(&QTimer::start)); } else regularCheck->start(); } PackageKitNotifier::~PackageKitNotifier() { } void PackageKitNotifier::recheckSystemUpdateNeeded() { if (PackageKit::Daemon::global()->isRunning()) { PackageKit::Transaction * trans = PackageKit::Daemon::getUpdates(); trans->setProperty("normalUpdates", 0); trans->setProperty("securityUpdates", 0); - trans->setProperty("update", NoUpdate); connect(trans, &PackageKit::Transaction::package, this, &PackageKitNotifier::package); connect(trans, &PackageKit::Transaction::finished, this, &PackageKitNotifier::finished); } } void PackageKitNotifier::package(PackageKit::Transaction::Info info, const QString &/*packageID*/, const QString &/*summary*/) { PackageKit::Transaction * trans = qobject_cast(sender()); switch (info) { case PackageKit::Transaction::InfoBlocked: break; //skip, we ignore blocked updates case PackageKit::Transaction::InfoSecurity: - trans->setProperty("update", qMax(Security, trans->property("update").toInt())); trans->setProperty("securityUpdates", trans->property("securityUpdates").toInt()+1); break; default: - trans->setProperty("update", qMax(Normal, trans->property("update").toInt())); trans->setProperty("normalUpdates", trans->property("normalUpdates").toInt()+1); break; } } void PackageKitNotifier::finished(PackageKit::Transaction::Exit /*exit*/, uint) { const PackageKit::Transaction * trans = qobject_cast(sender()); const uint normalUpdates = trans->property("normalUpdates").toInt(); const uint securityUpdates = trans->property("securityUpdates").toInt(); - const Update update = Update(trans->property("update").toInt()); - - const bool changed = update != m_update || normalUpdates != m_normalUpdates || securityUpdates != m_securityUpdates; + const bool changed = normalUpdates != m_normalUpdates || securityUpdates != m_securityUpdates; m_normalUpdates = normalUpdates; m_securityUpdates = securityUpdates; - m_update = update; if (changed) { Q_EMIT foundUpdates(); } } bool PackageKitNotifier::isSystemUpToDate() const { - return m_update == NoUpdate; + return m_securityUpdates == 0 && m_normalUpdates == 0; } uint PackageKitNotifier::securityUpdatesCount() { return m_securityUpdates; } uint PackageKitNotifier::updatesCount() { return m_normalUpdates; } void PackageKitNotifier::refreshDatabase() { if (!m_refresher) { m_refresher = PackageKit::Daemon::refreshCache(false); connect(m_refresher.data(), &PackageKit::Transaction::finished, this, [this]() { recheckSystemUpdateNeeded(); delete m_refresher; }); } } QProcess* PackageKitNotifier::checkAptVariable(const QString &aptconfig, const QLatin1String& varname, std::function func) { QProcess* process = new QProcess; process->start(aptconfig, {QStringLiteral("dump")}); connect(process, static_cast(&QProcess::finished), this, [func, process, varname](int code) { if (code != 0) return; QRegularExpression rx(QLatin1Char('^') + varname + QStringLiteral(" \"(.*?)\"$")); QTextStream stream(process); QString line; while (stream.readLineInto(&line)) { const auto match = rx.match(line); if (match.hasMatch()) { func(match.capturedRef(1)); } } }); connect(process, static_cast(&QProcess::finished), process, &QObject::deleteLater); return process; } diff --git a/libdiscover/backends/PackageKitBackend/PackageKitNotifier.h b/libdiscover/backends/PackageKitBackend/PackageKitNotifier.h index ddd3485f..1fead439 100644 --- a/libdiscover/backends/PackageKitBackend/PackageKitNotifier.h +++ b/libdiscover/backends/PackageKitBackend/PackageKitNotifier.h @@ -1,65 +1,58 @@ /*************************************************************************** * Copyright © 2013 Lukas Appelhans * * * * 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) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * 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 . * ***************************************************************************/ #ifndef PACKAGEKITNOTIFIER_H #define PACKAGEKITNOTIFIER_H #include #include #include #include #include class QProcess; class PackageKitNotifier : public BackendNotifierModule { Q_OBJECT Q_PLUGIN_METADATA(IID "org.kde.discover.BackendNotifierModule") Q_INTERFACES(BackendNotifierModule) public: - enum Update { - NoUpdate, - Security, - Normal - }; - Q_ENUM(Update) explicit PackageKitNotifier(QObject* parent = nullptr); ~PackageKitNotifier() override; bool isSystemUpToDate() const override; uint securityUpdatesCount() override; uint updatesCount() override; void recheckSystemUpdateNeeded() override; void refreshDatabase(); private Q_SLOTS: void package(PackageKit::Transaction::Info info, const QString &packageID, const QString &summary); void finished(PackageKit::Transaction::Exit exit, uint); private: QProcess* checkAptVariable(const QString &aptconfig, const QLatin1String& varname, std::function func); - Update m_update; uint m_securityUpdates; uint m_normalUpdates; QPointer m_refresher; }; #endif diff --git a/notifier/DiscoverNotifier.cpp b/notifier/DiscoverNotifier.cpp index b2ccce36..35f82611 100644 --- a/notifier/DiscoverNotifier.cpp +++ b/notifier/DiscoverNotifier.cpp @@ -1,189 +1,188 @@ /*************************************************************************** * Copyright © 2014 Aleix Pol Gonzalez * * * * 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) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * 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 . * ***************************************************************************/ #include "DiscoverNotifier.h" #include "BackendNotifierFactory.h" #include #include #include #include #include #include -#include DiscoverNotifier::DiscoverNotifier(QObject * parent) : QObject(parent) , m_verbose(false) { configurationChanged(); m_backends = BackendNotifierFactory().allBackends(); foreach(BackendNotifierModule* module, m_backends) { connect(module, &BackendNotifierModule::foundUpdates, this, &DiscoverNotifier::updateStatusNotifier); } connect(&m_timer, &QTimer::timeout, this, &DiscoverNotifier::showUpdatesNotification); m_timer.setSingleShot(true); - m_timer.setInterval(180000); + m_timer.setInterval(1000); updateStatusNotifier(); } DiscoverNotifier::~DiscoverNotifier() = default; void DiscoverNotifier::configurationChanged() { KConfig notifierConfig(QStringLiteral("plasma-discover-notifierrc"), KConfig::NoGlobals); KConfigGroup notifyTypeGroup(¬ifierConfig, "NotificationType"); m_verbose = notifyTypeGroup.readEntry("Verbose", false); } void DiscoverNotifier::showMuon() { KRun::runCommand(QStringLiteral("plasma-discover --mode update"), nullptr); } bool DiscoverNotifier::isSystemUpToDate() const { for(BackendNotifierModule* module : m_backends) { if(!module->isSystemUpToDate()) return false; } return true; } void DiscoverNotifier::showUpdatesNotification() { if (state()==NoUpdates) { //it's not very helpful to notify that everyting is in order return; } //TODO: Better message strings QString msg = message(); if (m_verbose) { msg += QLatin1Char(' ') + extendedMessage(); } KNotification::event(QStringLiteral("Update"), i18n("System update available"), msg, QStringLiteral("system-software-update"), nullptr, KNotification::CloseOnTimeout, QStringLiteral("muonabstractnotifier")); } void DiscoverNotifier::updateStatusNotifier() { if (!isSystemUpToDate()) { m_timer.start(); } emit updatesChanged(); } DiscoverNotifier::State DiscoverNotifier::state() const { bool security = false, normal = false; for(BackendNotifierModule* module : m_backends) { security |= module->securityUpdatesCount()>0; normal |= security || module->updatesCount()>0; if (security) break; } if (security) return SecurityUpdates; else if (normal) return NormalUpdates; else return NoUpdates; } QString DiscoverNotifier::iconName() const { switch(state()) { case SecurityUpdates: return QStringLiteral("update-high"); case NormalUpdates: return QStringLiteral("update-low"); case NoUpdates: return QStringLiteral("update-none"); } return QString(); } QString DiscoverNotifier::message() const { switch(state()) { case SecurityUpdates: return i18n("Security updates available"); case NormalUpdates: return i18n("Updates available"); case NoUpdates: return i18n("System up to date"); } return QString(); } QString DiscoverNotifier::extendedMessage() const { uint securityCount = securityUpdatesCount(); uint count = updatesCount(); if (count > 0 && securityCount > 0) { QString allUpdates = i18ncp("First part of '%1, %2'", "1 package to update", "%1 packages to update", count); QString securityUpdates = i18ncp("Second part of '%1, %2'", "of which 1 is security update", "of which %1 are security updates", securityCount); return i18nc("%1 is '%1 packages to update' and %2 is 'of which %1 is security updates'", "%1, %2", allUpdates, securityUpdates); } else if (count > 0) { return i18np("1 package to update", "%1 packages to update", count); } else if (securityCount > 0) { return i18np("1 security update", "%1 security updates", securityCount); } else { return i18n("No packages to update"); } } void DiscoverNotifier::recheckSystemUpdateNeeded() { foreach(BackendNotifierModule* module, m_backends) module->recheckSystemUpdateNeeded(); } uint DiscoverNotifier::securityUpdatesCount() const { uint ret = 0; foreach(BackendNotifierModule* module, m_backends) ret += module->securityUpdatesCount(); return ret; } uint DiscoverNotifier::updatesCount() const { uint ret = 0; foreach(BackendNotifierModule* module, m_backends) ret += module->updatesCount(); return ret + securityUpdatesCount(); } QStringList DiscoverNotifier::loadedModules() const { QStringList ret; for(BackendNotifierModule* module : m_backends) ret += QString::fromLatin1(module->metaObject()->className()); return ret; } diff --git a/notifier/plasmoid/metadata.desktop b/notifier/plasmoid/metadata.desktop index 348f2583..e46ab5e1 100644 --- a/notifier/plasmoid/metadata.desktop +++ b/notifier/plasmoid/metadata.desktop @@ -1,103 +1,103 @@ [Desktop Entry] Name=Updates Name[ar]=التّحديثات Name[ca]=Actualitzacions Name[ca@valencia]=Actualitzacions Name[cs]=Aktualizace Name[da]=Opdateringer Name[de]=Aktualisierungen Name[el]=Ενημερώσεις Name[en_GB]=Updates Name[es]=Actualizaciones Name[et]=Uuendused Name[eu]=Eguneraketak Name[fi]=Päivitykset Name[fr]=Mises à jour Name[gl]=Actualizacións Name[he]=עדכונים Name[hu]=Frissítések Name[ia]=Actualisationes Name[id]=Pembaruan Name[it]=Aggiornamenti Name[ko]=업데이트 Name[nb]=Oppdateringer Name[nl]=Elementen voor bijwerken Name[nn]=Oppdateringar Name[pa]=ਅੱਪਡੇਟ Name[pl]=Uaktualnienia Name[pt]=Actualizações Name[pt_BR]=Atualizações Name[ru]=Обновления Name[sk]=Aktualizácie Name[sl]=Posodobitve Name[sr]=Допуне Name[sr@ijekavian]=Допуне Name[sr@ijekavianlatin]=Dopune Name[sr@latin]=Dopune Name[sv]=Uppdateringar Name[tr]=Güncellemeler Name[uk]=Оновлення Name[x-test]=xxUpdatesxx Name[zh_CN]=更新 Name[zh_TW]=更新 Comment=Helps you keep your system up to date Comment[ar]=يساعدك في إبقاء نظامك محدّثًا Comment[ca]=Us ajuda a mantenir el vostre sistema al dia Comment[ca@valencia]=Vos ajuda a mantindre el vostre sistema al dia Comment[cs]=Umožňuje vám zachovat systém aktuální Comment[da]=Hjælper dig med at holde dit system opdateret Comment[de]=Hilft Ihnen, Ihr System auf dem neusten Stand zu halten Comment[el]=Βοηθά να διατηρήσετε το σύστημά σας ενημερωμένο Comment[en_GB]=Helps you keep your system up to date Comment[es]=Le ayuda a mantener su sistema al día Comment[et]=Aitab hoida süsteemi värske Comment[eu]=Zure sistema egunean mantentzen laguntzen dizu Comment[fi]=Auttaa pitämään järjestelmäsi ajan tasalla Comment[fr]=Vous aide à maintenir le système à jour Comment[gl]=Axúdao a manter o seu sistema actualizado. Comment[he]=עוזר לך לשמור על המערכת מעודכנת Comment[hu]=Segít a rendszere naprakészen tartásában Comment[id]=Membantu kamu menjaga komputermu selalu terbaru Comment[it]=Ti aiuta a mantenere il sistema aggiornato Comment[ko]=시스템을 최신 상태로 유지합니다 Comment[nb]=Hjelper til å holde systemet oppdatert Comment[nl]=Helpt u om uw system bijgewerkt te houden Comment[nn]=Hjelper til å halda systemet oppdatert Comment[pa]=ਤੁਹਾਡੇ ਸਿਸਟਮ ਨੂੰ ਅੱਪਡੇਟ ਰੱਖਣ ਲਈ ਤੁਹਾਡੀ ਮਦਦ ਕਰਦਾ ਹੈ Comment[pl]=Pomaga utrzymać twój system aktualnym Comment[pt]=Ajuda-o a manter o seu sistema actualizado Comment[pt_BR]=Ajuda-o a manter seu sistema atualizado Comment[ru]=Помогает поддерживать систему в актуальном состоянии Comment[sk]=Pomôže vám udržiavať váš systém aktuálny Comment[sl]=Pomaga vam ohraniti posodobljen sistem Comment[sr]=Помаже вам да одржавате систем ажурним Comment[sr@ijekavian]=Помаже вам да одржавате систем ажурним Comment[sr@ijekavianlatin]=Pomaže vam da održavate sistem ažurnim Comment[sr@latin]=Pomaže vam da održavate sistem ažurnim Comment[sv]=Hjälper dig hålla systemet uppdaterat Comment[tr]=Sisteminizin güncel kalmasını sağlar Comment[uk]=Допомагає підтримувати актуальний стан вашої системи Comment[x-test]=xxHelps you keep your system up to datexx Comment[zh_CN]=帮助您保持系统更新 Comment[zh_TW]=協助您的系統更新到最新狀態 Encoding=UTF-8 Icon=update-none Type=Service X-KDE-ServiceTypes=Plasma/Applet X-KDE-PluginInfo-Author=The Plasma Team X-KDE-PluginInfo-Email=plasma-devel@kde.org X-KDE-PluginInfo-Name=org.kde.discovernotifier X-KDE-PluginInfo-Version=3.0 -X-KDE-PluginInfo-Website=http://plasma.kde.org/ +X-KDE-PluginInfo-Website=https://www.kde.org/plasma-desktop X-KDE-PluginInfo-Category=Online Services X-KDE-PluginInfo-Depends= X-KDE-PluginInfo-License=GPLv2+ X-KDE-PluginInfo-EnabledByDefault=true X-Plasma-API=declarativeappletscript X-Plasma-MainScript=ui/main.qml X-Plasma-NotificationArea=true X-Plasma-Requires-FileDialog=Unused X-Plasma-Requires-LaunchApp=Required