diff --git a/CMakeLists.txt b/CMakeLists.txt index 2f25d2db6c59c680f127c218a77f697940a01ba1..86912bc3eafbef3b68df35eb6b0df1ee19accbe5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -206,6 +206,7 @@ set(COMMON_SOURCES ${APP_SRC_DIR}/pluginadapter.cpp ${APP_SRC_DIR}/deviceitemlistmodel.cpp ${APP_SRC_DIR}/pluginlistmodel.cpp + ${APP_SRC_DIR}/pluginstorelistmodel.cpp ${APP_SRC_DIR}/pluginhandlerlistmodel.cpp ${APP_SRC_DIR}/preferenceitemlistmodel.cpp ${APP_SRC_DIR}/mediacodeclistmodel.cpp @@ -239,7 +240,8 @@ set(COMMON_SOURCES ${APP_SRC_DIR}/currentcall.cpp ${APP_SRC_DIR}/messageparser.cpp ${APP_SRC_DIR}/previewengine.cpp - ${APP_SRC_DIR}/imagedownloader.cpp) + ${APP_SRC_DIR}/imagedownloader.cpp + ${APP_SRC_DIR}/pluginversionmanager.cpp) set(COMMON_HEADERS ${APP_SRC_DIR}/avatarimageprovider.h @@ -267,6 +269,7 @@ set(COMMON_HEADERS ${APP_SRC_DIR}/pluginadapter.h ${APP_SRC_DIR}/deviceitemlistmodel.h ${APP_SRC_DIR}/pluginlistmodel.h + ${APP_SRC_DIR}/pluginstorelistmodel.h ${APP_SRC_DIR}/pluginhandlerlistmodel.h ${APP_SRC_DIR}/preferenceitemlistmodel.h ${APP_SRC_DIR}/mediacodeclistmodel.h @@ -303,7 +306,8 @@ set(COMMON_HEADERS ${APP_SRC_DIR}/currentcall.h ${APP_SRC_DIR}/messageparser.h ${APP_SRC_DIR}/htmlparser.h - ${APP_SRC_DIR}/imagedownloader.h) + ${APP_SRC_DIR}/imagedownloader.h + ${APP_SRC_DIR}/pluginversionmanager.h) # For libavutil/avframe. diff --git a/src/app/appsettingsmanager.h b/src/app/appsettingsmanager.h index 7397df79451f73495a7ffbdc7dfa4b89e11e9a47..901268b5bf8a2ed82c9989a22c3e62cadb740296 100644 --- a/src/app/appsettingsmanager.h +++ b/src/app/appsettingsmanager.h @@ -49,6 +49,7 @@ extern const QString defaultDownloadPath; X(HideSelf, false) \ X(HideSpectators, false) \ X(AutoUpdate, true) \ + X(PluginAutoUpdate, false) \ X(StartMinimized, false) \ X(ShowChatviewHorizontally, true) \ X(NeverShowMeAgain, false) \ diff --git a/src/app/appversionmanager.cpp b/src/app/appversionmanager.cpp index c3ac06c9c57595b54f227e29aaea33240026eebb..aead337ab942f3424971a2abc5c7e3197c9a8d4c 100644 --- a/src/app/appversionmanager.cpp +++ b/src/app/appversionmanager.cpp @@ -166,8 +166,8 @@ AppVersionManager::AppVersionManager(const QString& url, LRCInstance* instance, QObject* parent) : NetworkManager(cm, parent) - , pimpl_(std::make_unique<Impl>(url, instance, *this)) , replyId_(new int(0)) + , pimpl_(std::make_unique<Impl>(url, instance, *this)) {} AppVersionManager::~AppVersionManager() diff --git a/src/app/constant/JamiStrings.qml b/src/app/constant/JamiStrings.qml index c62fbd884d2b3b8cf1cff07b9f018a274006a10d..4278a99d32746aa5bf7f6e1bc41b44e14e16d323 100644 --- a/src/app/constant/JamiStrings.qml +++ b/src/app/constant/JamiStrings.qml @@ -661,6 +661,7 @@ Item { property string enable: qsTr("Enable") property string pluginPreferences: qsTr("Preferences") property string reset: qsTr("Reset") + property string disableAll: qsTr("Disable all") property string uninstall: qsTr("Uninstall") property string resetPreferences: qsTr("Reset Preferences") property string selectPluginInstall: qsTr("Select a plugin to install") diff --git a/src/app/lrcinstance.cpp b/src/app/lrcinstance.cpp index 323cab0b54daeac9df070b6580fb68ca01db50fe..02bafe4fdf91bc6cffb85a64aafa705ac1d3c432 100644 --- a/src/app/lrcinstance.cpp +++ b/src/app/lrcinstance.cpp @@ -19,6 +19,7 @@ */ #include "lrcinstance.h" +#include "connectivitymonitor.h" #include <QBuffer> #include <QMutex> @@ -35,6 +36,7 @@ LRCInstance::LRCInstance(migrateCallback willMigrateCb, bool muteDaemon) : lrc_(std::make_unique<Lrc>(willMigrateCb, didMigrateCb, !debugMode || muteDaemon)) , updateManager_(std::make_unique<AppVersionManager>(updateUrl, connectivityMonitor, this)) + , connectivityMonitor_(*connectivityMonitor) , threadPool_(new QThreadPool(this)) { debugMode_ = debugMode; @@ -111,6 +113,12 @@ LRCInstance::pluginModel() return lrc_->getPluginModel(); } +ConnectivityMonitor& +LRCInstance::connectivityMonitor() +{ + return connectivityMonitor_; +} + bool LRCInstance::isConnected() { diff --git a/src/app/lrcinstance.h b/src/app/lrcinstance.h index 0da5abee14ca57e9b0b248988ac094fa3a63c329..08075930d35e241a3e1b351bc3152b2c958891ef 100644 --- a/src/app/lrcinstance.h +++ b/src/app/lrcinstance.h @@ -79,6 +79,7 @@ public: ContactModel* getCurrentContactModel(); AVModel& avModel(); PluginModel& pluginModel(); + ConnectivityMonitor& connectivityMonitor(); BehaviorController& behaviorController(); void subscribeToDebugReceived(); @@ -147,6 +148,8 @@ private: std::unique_ptr<Lrc> lrc_; std::unique_ptr<AppVersionManager> updateManager_; + ConnectivityMonitor& connectivityMonitor_; + QString selectedConvUid_; MapStringString contentDrafts_; MapStringString lastConferences_; diff --git a/src/app/networkmanager.cpp b/src/app/networkmanager.cpp index 0af5f216e8e5e1a87eecd431aedaf0100f2efa15..7bad5e6c6bbc8a01952f5bd49e226175fb138580 100644 --- a/src/app/networkmanager.cpp +++ b/src/app/networkmanager.cpp @@ -72,9 +72,10 @@ NetworkManager::sendGetRequest(const QUrl& url, int NetworkManager::downloadFile(const QUrl& url, - unsigned int replyId, + int replyId, std::function<void(bool, const QString&)>&& onDoneCallback, - const QString& filePath) + const QString& filePath, + const QString& extension) { // If there is already a download in progress, return. if ((downloadReplies_.value(replyId) != NULL || !(replyId == 0)) @@ -111,7 +112,7 @@ NetworkManager::downloadFile(const QUrl& url, const QFileInfo fileInfo(url.path()); const QString fileName = fileInfo.fileName(); auto& file = files_[uuid]; - file = new QFile(filePath + fileName + ".jpl"); + file = new QFile(filePath + fileName + extension); if (!file->open(QIODevice::WriteOnly)) { Q_EMIT errorOccurred(GetError::ACCESS_DENIED); files_.remove(uuid); @@ -122,8 +123,8 @@ NetworkManager::downloadFile(const QUrl& url, // Start the download. const QNetworkRequest request(url); - downloadReplies_[uuid] = manager_->get(request); - auto* const reply = downloadReplies_[uuid]; + auto* const reply = manager_->get(request); + downloadReplies_[uuid] = reply; connect(reply, &QNetworkReply::readyRead, this, [file, reply]() { if (file && file->isOpen()) { file->write(reply->readAll()); @@ -148,8 +149,10 @@ NetworkManager::downloadFile(const QUrl& url, Q_EMIT errorOccurred(GetError::NETWORK_ERROR); }); - connect(reply, &QNetworkReply::finished, this, [this, uuid, onDoneCallback, reply]() { + connect(reply, &QNetworkReply::finished, this, [this, uuid, onDoneCallback, reply, file]() { bool success = false; + file->close(); + reply->deleteLater(); QString errorMessage; if (reply->error() == QNetworkReply::NoError) { resetDownload(uuid); diff --git a/src/app/networkmanager.h b/src/app/networkmanager.h index 67dacb237dbf14bdf820f5c1e04ac03e5cc5f2ef..daa2196be02496c89dd6a351e4aa54e6e9f500d3 100644 --- a/src/app/networkmanager.h +++ b/src/app/networkmanager.h @@ -41,9 +41,10 @@ public: void sendGetRequest(const QUrl& url, std::function<void(const QByteArray&)>&& onDoneCallback); int downloadFile(const QUrl& url, - unsigned int replyId, + int replyId, std::function<void(bool, const QString&)>&& onDoneCallback, - const QString& filePath); + const QString& filePath, + const QString& extension = ""); void resetDownload(int replyId); void cancelDownload(int replyId); Q_SIGNALS: diff --git a/src/app/pluginadapter.cpp b/src/app/pluginadapter.cpp index 6c5a97cf08cab2d3d17f576af08bcc8578dc7a58..8b2a464fadb6dfc15ab88b8530daded3c8453074 100644 --- a/src/app/pluginadapter.cpp +++ b/src/app/pluginadapter.cpp @@ -20,9 +20,22 @@ #include "networkmanager.h" #include "lrcinstance.h" +#include "utilsadapter.h" -PluginAdapter::PluginAdapter(LRCInstance* instance, QObject* parent) +#include <QJsonArray> +#include <QJsonDocument> +#include <QJsonObject> +#include <QDir> +#include <QString> + +PluginAdapter::PluginAdapter(LRCInstance* instance, QObject* parent, QString baseUrl) : QmlAdapterBase(instance, parent) + , pluginStoreListModel_(new PluginStoreListModel(this)) + , pluginVersionManager_(new PluginVersionManager(instance, baseUrl, this)) + , pluginListModel_(new PluginListModel(this)) + , lrcInstance_(instance) + , tempPath_(QDir::tempPath()) + , baseUrl_(baseUrl) { set_isEnabled(lrcInstance_->pluginModel().getPluginsEnabled()); @@ -32,6 +45,73 @@ PluginAdapter::PluginAdapter(LRCInstance* instance, QObject* parent) this, &PluginAdapter::updateHandlersListCount); connect(this, &PluginAdapter::isEnabledChanged, this, &PluginAdapter::updateHandlersListCount); + connect(pluginVersionManager_, + &PluginVersionManager::versionStatusChanged, + pluginListModel_, + &PluginListModel::onVersionStatusChanged); + connect(pluginVersionManager_, + &PluginVersionManager::versionStatusChanged, + pluginStoreListModel_, + &PluginStoreListModel::onVersionStatusChanged); + connect(pluginStoreListModel_, + &PluginStoreListModel::pluginAdded, + this, + &PluginAdapter::getPluginDetails); + connect(pluginListModel_, + &PluginListModel::versionCheckRequested, + pluginVersionManager_, + &PluginVersionManager::checkVersionStatus); + connect(pluginListModel_, + &PluginListModel::autoUpdateChanged, + pluginVersionManager_, + &PluginVersionManager::setAutoUpdate); + connect(pluginListModel_, + &PluginListModel::setVersionStatus, + pluginStoreListModel_, + &PluginStoreListModel::onVersionStatusChanged); + getPluginsFromStore(); +} + +void +PluginAdapter::getPluginsFromStore() +{ + pluginVersionManager_->sendGetRequest(QUrl(baseUrl_), [this](const QByteArray& data) { + auto result = QJsonDocument::fromJson(data).array(); + auto pluginsInstalled = lrcInstance_->pluginModel().getPluginsId(); + QList<QVariantMap> plugins; + for (const auto& plugin : result) { + auto qPlugin = plugin.toVariant().toMap(); + if (!pluginsInstalled.contains(qPlugin["id"].toString())) { + plugins.append(qPlugin); + } + } + pluginStoreListModel_->setPlugins(plugins); + }); +} + +void +PluginAdapter::getPluginDetails(const QString& pluginId) +{ + pluginVersionManager_->sendGetRequest(QUrl(baseUrl_ + "/details/" + pluginId), + [this](const QByteArray& data) { + auto result = QJsonDocument::fromJson(data).object(); + // my response is a json object and I want to convert + // it to a QVariantMap + pluginStoreListModel_->addPlugin( + result.toVariantMap()); + }); +} + +void +PluginAdapter::installRemotePlugin(const QString& pluginId) +{ + pluginVersionManager_->installRemotePlugin(pluginId); +} + +bool +PluginAdapter::isAutoUpdaterEnabled() +{ + return pluginVersionManager_->isAutoUpdaterEnabled(); } QVariant @@ -77,3 +157,15 @@ PluginAdapter::updateHandlersListCount() set_chatHandlersListCount(0); } } + +void +PluginAdapter::checkVersionStatus(const QString& pluginId) +{ + pluginVersionManager_->checkVersionStatus(pluginId); +} + +QString +PluginAdapter::baseUrl() const +{ + return baseUrl_; +} diff --git a/src/app/pluginadapter.h b/src/app/pluginadapter.h index cb640100cf78607fb7d700295d117922d55616e8..f994b089f145dd5c4db820190e4cb0a949bc9362 100644 --- a/src/app/pluginadapter.h +++ b/src/app/pluginadapter.h @@ -22,6 +22,8 @@ #include "pluginlistmodel.h" #include "pluginhandlerlistmodel.h" #include "pluginlistpreferencemodel.h" +#include "pluginversionmanager.h" +#include "pluginstorelistmodel.h" #include "preferenceitemlistmodel.h" #include <QObject> @@ -36,9 +38,18 @@ class PluginAdapter final : public QmlAdapterBase QML_PROPERTY(bool, isEnabled) public: - explicit PluginAdapter(LRCInstance* instance, QObject* parent = nullptr); + explicit PluginAdapter(LRCInstance* instance, + QObject* parent = nullptr, + QString baseUrl = "https://plugins.jami.net"); ~PluginAdapter() = default; + Q_INVOKABLE void getPluginsFromStore(); + Q_INVOKABLE void getPluginDetails(const QString& pluginId); + Q_INVOKABLE void installRemotePlugin(const QString& pluginId); + Q_INVOKABLE QString baseUrl() const; + Q_INVOKABLE void checkVersionStatus(const QString& pluginId); + Q_INVOKABLE bool isAutoUpdaterEnabled(); + protected: Q_INVOKABLE QVariant getMediaHandlerSelectableModel(const QString& callId); Q_INVOKABLE QVariant getChatHandlerSelectableModel(const QString& accountId, @@ -51,6 +62,11 @@ private: void updateHandlersListCount(); std::unique_ptr<PluginHandlerListModel> pluginHandlerListModel_; - + PluginStoreListModel* pluginStoreListModel_; + PluginVersionManager* pluginVersionManager_; + PluginListModel* pluginListModel_; + LRCInstance* lrcInstance_; std::mutex mtx_; + QString tempPath_; + QString baseUrl_; }; diff --git a/src/app/pluginlistmodel.cpp b/src/app/pluginlistmodel.cpp index 6f6a16c3bd99d27d35bc8183772fe260f11a0924..3dc41db43524e86e33a9b07f5a54cc8e27169f40 100644 --- a/src/app/pluginlistmodel.cpp +++ b/src/app/pluginlistmodel.cpp @@ -142,3 +142,39 @@ PluginListModel::filterPlugins(VectorString& list) const }), list.cend()); } + +void +PluginListModel::onVersionStatusChanged(const QString& pluginId, PluginStatus::Role status) +{ + auto pluginIndex = -1; + for (auto& p : installedPlugins_) { + auto details = lrcInstance_->pluginModel().getPluginDetails(p); + if (details.name == pluginId) { + pluginIndex = installedPlugins_.indexOf(p, -1); + break; + } + } + switch (status) { + case PluginStatus::INSTALLED: + addPlugin(); + break; + default: + break; + } + + if (pluginIndex == -1) { + return; + } + + switch (status) { + case PluginStatus::INSTALLABLE: + removePlugin(pluginIndex); + break; + case PluginStatus::FAILED: + qWarning() << "Failed to install plugin" << pluginId; + break; + default: + break; + } + return; +} diff --git a/src/app/pluginlistmodel.h b/src/app/pluginlistmodel.h index b0bbbd9fa6a1bf3d088f9bd47d05f0cf02888b3f..d5c445183aea3eadef84c21ab649d7f26effbfeb 100644 --- a/src/app/pluginlistmodel.h +++ b/src/app/pluginlistmodel.h @@ -19,6 +19,7 @@ #pragma once #include "abstractlistmodelbase.h" +#include "pluginversionmanager.h" class LRCInstance; @@ -52,6 +53,14 @@ public: Q_INVOKABLE void pluginChanged(int index); Q_INVOKABLE void addPlugin(); +Q_SIGNALS: + void versionCheckRequested(const QString& pluginId); + void setVersionStatus(const QString& pluginId, PluginStatus::Role status); + void autoUpdateChanged(bool state); + +public Q_SLOTS: + void onVersionStatusChanged(const QString& pluginId, PluginStatus::Role status); + private: void filterPlugins(VectorString& list) const; VectorString installedPlugins_ {}; diff --git a/src/app/pluginstorelistmodel.cpp b/src/app/pluginstorelistmodel.cpp new file mode 100644 index 0000000000000000000000000000000000000000..a90b7982860f1839c1da45cbc9d2168d698cc393 --- /dev/null +++ b/src/app/pluginstorelistmodel.cpp @@ -0,0 +1,196 @@ +/** + * Copyright (C) 2023 Savoir-faire Linux Inc. + * + * 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 3 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 <http://www.gnu.org/licenses/>. + */ + +#include "pluginstorelistmodel.h" + +#include <QUrl> + +PluginStoreListModel::PluginStoreListModel(QObject* parent) + : AbstractListModelBase(parent) +{} + +int +PluginStoreListModel::rowCount(const QModelIndex& parent) const +{ + if (!parent.isValid()) { + return plugins_.size(); + } + /// A valid QModelIndex returns 0 as no entry has sub-elements. + return 0; +} + +QVariant +PluginStoreListModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + auto plugin = plugins_.at(index.row()); + switch (role) { + case Role::Id: + return QVariant(plugin["id"].toString()); + case Role::Title: + return QVariant(plugin["name"].toString()); + case Role::IconPath: + return QVariant(plugin["iconPath"].toString()); + case Role::Background: + return QVariant(plugin["background"].toString()); + case Role::Description: + return QVariant(plugin["description"].toString()); + case Role::Author: + return QVariant(plugin["author"].toString()); + case Role::Status: + return QVariant(plugin.value("status", PluginStatus::INSTALLABLE).toString()); + } + return QVariant(); +} + +QHash<int, QByteArray> +PluginStoreListModel::roleNames() const +{ + using namespace PluginStoreList; + QHash<int, QByteArray> roles; +#define X(role) roles[role] = #role; + PLUGINSTORE_ROLES +#undef X + return roles; +} + +void +PluginStoreListModel::reset() +{ + beginResetModel(); + plugins_.clear(); + endResetModel(); +} + +void +PluginStoreListModel::addPlugin(const QVariantMap& plugin) +{ + beginInsertRows(QModelIndex(), plugins_.size(), plugins_.size()); + plugins_.append(plugin); + endInsertRows(); +} + +void +PluginStoreListModel::setPlugins(const QList<QVariantMap>& plugins) +{ + beginResetModel(); + plugins_ = plugins; + endResetModel(); +} + +void +PluginStoreListModel::removePlugin(const QString& pluginId) +{ + auto index = 0; + for (auto& plugin : plugins_) { + if (plugin["id"].toString() == pluginId) { + beginRemoveRows(QModelIndex(), index, index); + plugins_.removeAt(index); + endRemoveRows(); + return; + } + index++; + } +} + +void +PluginStoreListModel::updatePlugin(const QVariantMap& plugin) +{ + auto index = 0; + for (auto& p : plugins_) { + if (p["id"].toString() == plugin["id"].toString()) { + p = plugin; + Q_EMIT dataChanged(createIndex(index, 0), createIndex(index, 0)); + return; + } + index++; + } +} + +QColor +PluginStoreListModel::computeAverageColorOfImage(const QString& file) +{ + auto fileUrl = QUrl(file); + // Return an invalid color if the file URL is invalid. + if (!fileUrl.isValid()) { + return QColor(); + } + // Load the image. + QImage image(fileUrl.toLocalFile()); + // If the image is valid... + if (!image.isNull()) { + static const QSize size(3, 3); + static const int nPixels = size.width() * size.height(); + // Scale the image to 3x3 pixels. + image = image.scaled(size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + // Return the average color of the image's pixels. + double red = 0; + double green = 0; + double blue = 0; + for (int i = 0; i < size.width(); i++) { + for (int j = 0; j < size.height(); j++) { + auto pixelColor = image.pixelColor(i, j); + red += pixelColor.red(); + green += pixelColor.green(); + blue += pixelColor.blue(); + } + } + return QColor(red / nPixels, green / nPixels, blue / nPixels, 70); + } else { + // Return an invalid color. + return QColor(); + } +} + +void +PluginStoreListModel::onVersionStatusChanged(const QString& pluginId, PluginStatus::Role status) +{ + auto plugin = QVariantMap(); + for (auto& p : plugins_) { + if (p["id"].toString() == pluginId) { + plugin = p; + break; + } + } + switch (status) { + case PluginStatus::INSTALLABLE: + if (!plugin.isEmpty()) + break; + pluginAdded(pluginId); + break; + + default: + break; + } + if (plugin.isEmpty()) { + return; + } + plugin["status"] = status; + + switch (status) { + case PluginStatus::INSTALLED: + removePlugin(pluginId); + break; + case PluginStatus::FAILED: + qWarning() << "Failed to install plugin" << pluginId; + break; + default: + break; + } +} \ No newline at end of file diff --git a/src/app/pluginstorelistmodel.h b/src/app/pluginstorelistmodel.h new file mode 100644 index 0000000000000000000000000000000000000000..7a603c5103073234dd39d4ab45efc3bfbcb142fd --- /dev/null +++ b/src/app/pluginstorelistmodel.h @@ -0,0 +1,74 @@ +/** + * Copyright (C) 2019-2023 Savoir-faire Linux Inc. + * Author: Xavier Jouslin de Noray <xavier.jouslindenoray@savoirfairelinux.com> + * + * 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 3 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 <http://www.gnu.org/licenses/>. + */ +#pragma once + +#include "abstractlistmodelbase.h" +#include "pluginversionmanager.h" + +class QColor; +class QString; + +#define PLUGINSTORE_ROLES \ + X(Id) \ + X(Title) \ + X(IconPath) \ + X(Background) \ + X(Description) \ + X(Status) \ + X(Author) + +namespace PluginStoreList { +Q_NAMESPACE +enum Role { + DummyRole = Qt::UserRole + 1, +#define X(role) role, + PLUGINSTORE_ROLES +#undef X +}; +Q_ENUM_NS(Role) +} // namespace PluginStoreList + +class PluginStoreListModel : public AbstractListModelBase +{ + Q_OBJECT + +public: + explicit PluginStoreListModel(QObject* parent = nullptr); + + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + QHash<int, QByteArray> roleNames() const override; + + Q_INVOKABLE void reset(); + + void addPlugin(const QVariantMap& plugin); + void setPlugins(const QList<QVariantMap>& plugins); + void removePlugin(const QString& pluginId); + void updatePlugin(const QVariantMap& plugin); + Q_INVOKABLE QColor computeAverageColorOfImage(const QString& fileUrl); + +Q_SIGNALS: + void pluginAdded(const QString& pluginId); + +public Q_SLOTS: + void onVersionStatusChanged(const QString& pluginId, PluginStatus::Role status); + +private: + using Role = PluginStoreList::Role; + QList<QVariantMap> plugins_; +}; diff --git a/src/app/pluginversionmanager.cpp b/src/app/pluginversionmanager.cpp new file mode 100644 index 0000000000000000000000000000000000000000..28217891fcab1def243209a15ab652c30462a322 --- /dev/null +++ b/src/app/pluginversionmanager.cpp @@ -0,0 +1,221 @@ +/** + * Copyright (C) 2023 Savoir-faire Linux Inc. + * + * 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 3 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 <http://www.gnu.org/licenses/>. + */ + +#include "pluginversionmanager.h" +#include "networkmanager.h" +#include "appsettingsmanager.h" +#include "lrcinstance.h" +#include "api/pluginmodel.h" + +#include <QMap> +#include <QTimer> +#include <QDir> + +static constexpr int updatePeriod = 1000 * 60 * 60 * 24; // one day in millis + +struct PluginVersionManager::Impl : public QObject +{ +public: + Impl(LRCInstance* instance, PluginVersionManager& parent) + : QObject(nullptr) + , parent_(parent) + , appSettingsManager_(new AppSettingsManager(this)) + , lrcInstance_(instance) + , tempPath_(QDir::tempPath()) + , updateTimer_(new QTimer(this)) + { + connect(updateTimer_, &QTimer::timeout, this, [this] { checkForUpdates(); }); + connect(&parent_, &NetworkManager::downloadFinished, this, [this](int replyId) { + auto pluginsId = parent_.pluginRepliesId.keys(replyId); + if (pluginsId.size() == 0) { + return; + } + for (const auto& pluginId : qAsConst(pluginsId)) { + Q_EMIT parent_.versionStatusChanged(pluginId, PluginStatus::Role::INSTALLING); + parent_.pluginRepliesId.remove(pluginId); + } + }); + checkForUpdates(); + setAutoUpdateCheck(true); + } + + ~Impl() + { + setAutoUpdateCheck(false); + } + + void checkForUpdates() + { + if (!lrcInstance_) { + return; + } + for (const auto& plugin : lrcInstance_->pluginModel().getInstalledPlugins()) { + checkVersionStatusFromPath(plugin); + } + } + + void cancelUpdate(const QString& pluginId) + { + if (!parent_.pluginRepliesId.contains(pluginId)) { + return; + } + parent_.cancelDownload(parent_.pluginRepliesId[pluginId]); + }; + + bool isAutoUpdaterEnabled() + { + return appSettingsManager_->getValue(Settings::Key::PluginAutoUpdate).toBool(); + } + + void setAutoUpdate(bool state) + { + appSettingsManager_->setValue(Settings::Key::PluginAutoUpdate, state); + } + + void checkVersionStatus(const QString& pluginId) + { + checkVersionStatusFromPath(lrcInstance_->pluginModel().getPluginPath(pluginId)); + } + + void checkVersionStatusFromPath(const QString& pluginPath) + { + if (!lrcInstance_) { + return; + } + + auto plugin = lrcInstance_->pluginModel().getPluginDetails(pluginPath); + if (plugin.version == "" || plugin.id == "") { + Q_EMIT parent_.versionStatusChanged(plugin.id, PluginStatus::Role::FAILED); + return; + } + + parent_.sendGetRequest(QUrl(parent_.baseUrl + "/versions/" + plugin.id), + [this, plugin](const QByteArray& data) { + // `data` represents the version in this case. + if (plugin.version < data) { + if (isAutoUpdaterEnabled()) { + installRemotePlugin(plugin.name); + return; + } + } + parent_.versionStatusChanged(plugin.id, + PluginStatus::Role::UPDATABLE); + }); + } + + void installRemotePlugin(const QString& pluginId) + { + parent_.downloadFile( + QUrl(parent_.baseUrl + "/download/" + pluginId), + pluginId, + 0, + [this, pluginId](bool success, const QString& error) { + if (!success) { + qDebug() << "Download Plugin error: " << error; + parent_.versionStatusChanged(pluginId, PluginStatus::Role::FAILED); + return; + } + auto res = lrcInstance_->pluginModel().installPlugin(QDir(tempPath_).filePath( + pluginId + ".jpl"), + true); + if (res) { + parent_.versionStatusChanged(pluginId, PluginStatus::Role::INSTALLED); + } else { + parent_.versionStatusChanged(pluginId, PluginStatus::Role::FAILED); + } + }, + tempPath_ + '/'); + Q_EMIT parent_.versionStatusChanged(pluginId, PluginStatus::Role::DOWNLOADING); + } + + void setAutoUpdateCheck(bool state) + { + // Quiet check for updates periodically, if set to. + if (!state) { + updateTimer_->stop(); + return; + } + updateTimer_->start(updatePeriod); + }; + + PluginVersionManager& parent_; + AppSettingsManager* appSettingsManager_ {nullptr}; + LRCInstance* lrcInstance_ {nullptr}; + QString tempPath_; + QTimer* updateTimer_; +}; + +PluginVersionManager::PluginVersionManager(LRCInstance* instance, QString& baseUrl, QObject* parent) + : NetworkManager(&instance->connectivityMonitor(), parent) + , baseUrl(baseUrl) + , pimpl_(std::make_unique<Impl>(instance, *this)) +{} + +PluginVersionManager::~PluginVersionManager() +{ + for (const auto& pluginReplyId : qAsConst(pluginRepliesId)) { + cancelDownload(pluginReplyId); + } + pluginRepliesId.clear(); +} + +void +PluginVersionManager::cancelUpdate(const QString& pluginId) +{ + pimpl_->cancelUpdate(pluginId); +} + +bool +PluginVersionManager::isAutoUpdaterEnabled() +{ + return pimpl_->isAutoUpdaterEnabled(); +} + +void +PluginVersionManager::setAutoUpdate(bool state) +{ + pimpl_->setAutoUpdate(state); +} + +int +PluginVersionManager::downloadFile(const QUrl& url, + const QString& pluginId, + int replyId, + std::function<void(bool, const QString&)>&& onDoneCallback, + const QString& filePath, + const QString& extension) +{ + auto reply = NetworkManager::downloadFile(url, + replyId, + std::move(onDoneCallback), + filePath, + extension); + pluginRepliesId[pluginId] = reply; + return reply; +} + +void +PluginVersionManager::checkVersionStatus(const QString& pluginId) +{ + pimpl_->checkVersionStatus(pluginId); +} + +void +PluginVersionManager::installRemotePlugin(const QString& pluginId) +{ + pimpl_->installRemotePlugin(pluginId); +} diff --git a/src/app/pluginversionmanager.h b/src/app/pluginversionmanager.h new file mode 100644 index 0000000000000000000000000000000000000000..365006bbe784c3ad236785d4bf751484a761319f --- /dev/null +++ b/src/app/pluginversionmanager.h @@ -0,0 +1,80 @@ +/** + * Copyright (C) 2023 Savoir-faire Linux Inc. + * + * 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 3 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <memory> +#include "networkmanager.h" + +class QString; +class LRCInstance; + +#define PLUGIN_STATUS_ROLES \ + X(INSTALLABLE) \ + X(DOWNLOADING) \ + X(INSTALLING) \ + X(INSTALLED) \ + X(FAILED) \ + X(UPDATABLE) + +namespace PluginStatus { +Q_NAMESPACE +enum Role { + DummyRole = Qt::UserRole + 1, +#define X(role) role, + PLUGIN_STATUS_ROLES +#undef X +}; +Q_ENUM_NS(Role) +} // namespace PluginStatus + +class PluginVersionManager final : public NetworkManager +{ + Q_OBJECT +public: + explicit PluginVersionManager(LRCInstance* instance, + QString& baseUrl, + QObject* parent = nullptr); + ~PluginVersionManager(); + + Q_INVOKABLE bool isAutoUpdaterEnabled(); + + Q_INVOKABLE void cancelUpdate(const QString& pluginId); + int downloadFile(const QUrl& url, + const QString& pluginId, + int replyId, + std::function<void(bool, const QString&)>&& onDoneCallback, + const QString& filePath, + const QString& extension = ".jpl"); + void installRemotePlugin(const QString& pluginId); + +public Q_SLOTS: + void checkVersionStatus(const QString& pluginId); + void setAutoUpdate(bool state); + +Q_SIGNALS: + void versionStatusChanged(const QString& pluginId, PluginStatus::Role status); + +private: + QString baseUrl; + bool autoUpdateCheck = false; + QMap<QString, unsigned int> pluginRepliesId {}; + struct Impl; + friend struct Impl; + std::unique_ptr<Impl> pimpl_; +}; +Q_DECLARE_METATYPE(PluginVersionManager*) diff --git a/src/app/qmlregister.cpp b/src/app/qmlregister.cpp index 5c5d677b1d212e5526bc7c871590dbccbc1082a5..4ffec74d07667d752e6431f2d6347b00162b9e06 100644 --- a/src/app/qmlregister.cpp +++ b/src/app/qmlregister.cpp @@ -56,6 +56,7 @@ #include "mainapplication.h" #include "namedirectory.h" #include "pluginlistmodel.h" +#include "pluginversionmanager.h" #include "appversionmanager.h" #include "pluginlistpreferencemodel.h" #include "preferenceitemlistmodel.h" @@ -188,6 +189,7 @@ registerTypes(QQmlEngine* engine, QML_REGISTERNAMESPACE(NS_MODELS, ContactList::staticMetaObject, "ContactList"); QML_REGISTERNAMESPACE(NS_MODELS, FilesToSend::staticMetaObject, "FilesToSend"); QML_REGISTERNAMESPACE(NS_MODELS, MessageList::staticMetaObject, "MessageList"); + QML_REGISTERNAMESPACE(NS_MODELS, PluginStatus::staticMetaObject, "PluginStatus"); // Qml singleton components QML_REGISTERSINGLETONTYPE_URL(NS_CONSTANTS, "qrc:/constant/JamiTheme.qml", JamiTheme); diff --git a/src/app/settingsview/components/PluginAvailableDelagate.qml b/src/app/settingsview/components/PluginAvailableDelagate.qml new file mode 100644 index 0000000000000000000000000000000000000000..0d66d5d9846fab349b550b64a510473e0ed0f1e7 --- /dev/null +++ b/src/app/settingsview/components/PluginAvailableDelagate.qml @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2023 Savoir-faire Linux Inc. + * Author: Xavier Jouslin de Noray <xjouslindenoray@savoirfairelinux.com> + * + * 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 3 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 <http://www.gnu.org/licenses/>. + */ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import net.jami.Models 1.1 +import net.jami.Adapters 1.1 +import Qt5Compat.GraphicalEffects +import net.jami.Constants 1.1 +import "../../commoncomponents" +import "../../mainview/components" + +ItemDelegate { + id: root + property string pluginId + property string pluginTitle + property string pluginIcon + property string pluginBackground + property string pluginDescription + property string pluginAuthor + property string pluginShortDescription + property int pluginStatus + + Rectangle { + id: rect + Scaffold { + } + color: Qt.rgba(0, 0, 0, 1) + anchors.fill: parent + radius: 15 + } + Page { + id: plugin + anchors.fill: parent + header: Control { + padding: 10 + background: Rectangle { + color: pluginBackground + } + contentItem: ColumnLayout { + RowLayout { + Layout.alignment: Qt.AlignTop | Qt.AlignRight + MaterialButton { + id: install + Layout.alignment: Qt.AlignRight + Layout.rightMargin: 8 + Layout.topMargin: 8 + Layout.preferredHeight: 20 + TextMetrics { + id: installTextSize + font.weight: Font.Black + font.pixelSize: JamiTheme.wizardViewButtonFontPixelSize + font.capitalization: Font.Medium + text: isDownloading() ? JamiStrings.cancel : JamiStrings.install + } + onClicked: installPlugin() + secondary: true + preferredWidth: installTextSize.width + JamiTheme.buttontextWizzardPadding + text: isDownloading() ? JamiStrings.cancel : JamiStrings.install + fontSize: 15 + } + } + RowLayout { + spacing: 10 + + CachedImage { + id: icon + Component.onCompleted: { + pluginBackground = PluginStoreListModel.computeAverageColorOfImage("file://" + UtilsAdapter.getCachePath() + '/plugins/' + pluginId + '.svg'); + } + width: 50 + height: 50 + downloadUrl: PluginAdapter.baseUrl + "/icons/" + pluginId + fileExtension: '.svg' + localPath: UtilsAdapter.getCachePath() + '/plugins/' + pluginId + '.svg' + } + ColumnLayout { + Label { + text: pluginTitle + font.kerning: true + color: JamiTheme.textColor + font.pointSize: JamiTheme.settingsFontSize + verticalAlignment: Text.AlignVCenter + } + Label { + color: JamiTheme.textColor + text: pluginShortDescription + font.kerning: true + font.pointSize: JamiTheme.settingsFontSize + verticalAlignment: Text.AlignVCenter + } + } + } + } + } + Rectangle { + anchors.fill: parent + color: JamiTheme.pluginViewBackgroundColor + } + Flickable { + anchors.fill: parent + anchors.margins: 10 + contentWidth: description.width + contentHeight: description.height + clip: true + flickableDirection: Flickable.VerticalFlick + ScrollBar.vertical: ScrollBar { + id: scrollBar + policy: ScrollBar.AsNeeded + } + Text { + id: description + width: parent.width + color: JamiTheme.textColor + text: pluginDescription + wrapMode: Text.WordWrap + } + } + footer: Control { + padding: 10 + background: Rectangle { + color: JamiTheme.pluginViewBackgroundColor + } + contentItem: Text { + Layout.fillWidth: true + Layout.preferredHeight: implicitHeight + Layout.topMargin: 8 + Layout.leftMargin: 8 + color: JamiTheme.textColor + + font.pointSize: JamiTheme.settingsFontSize + font.kerning: true + text: "By " + pluginAuthor + verticalAlignment: Text.AlignVCenter + } + } + + DropShadow { + z: 2 + visible: hovered + width: root.width + height: root.height + radius: 16 + color: Qt.rgba(0, 0.34, 0.6, 0.16) + source: root + transparentBorder: true + samples: radius + 1 + cached: true + } + } + function installPlugin() { + if (isDownloading()) { + return; + } + PluginAdapter.installRemotePlugin(pluginId); + } + + function isDownloading() { + return pluginStatus === PluginStatus.DOWNLOADING; + } +} diff --git a/src/app/settingsview/components/PluginItemDelegate.qml b/src/app/settingsview/components/PluginItemDelegate.qml index 578808bb8307516122f744582f2ed5658d2bfcfb..e273d8772beaf2313ce764924c544109798ea64c 100644 --- a/src/app/settingsview/components/PluginItemDelegate.qml +++ b/src/app/settingsview/components/PluginItemDelegate.qml @@ -77,21 +77,7 @@ ItemDelegate { text: pluginName === "" ? pluginId : pluginName verticalAlignment: Text.AlignVCenter } - MaterialButton { - id: update - TextMetrics { - id: updateTextSize - font.weight: Font.Bold - font.pixelSize: JamiTheme.wizardViewButtonFontPixelSize - font.capitalization: Font.AllUppercase - text: JamiStrings.updatePlugin - } - visible: false - secondary: true - preferredWidth: updateTextSize.width - text: JamiStrings.updatePlugin - fontSize: 15 - } + ToggleSwitch { id: loadSwitch Layout.fillHeight: true diff --git a/src/app/settingsview/components/PluginListView.qml b/src/app/settingsview/components/PluginListView.qml index 487042c4d6edc10b02b26bb5d392589700db6506..1afa2d4cfac87fb6d263fe0fe79de0e69026bae1 100644 --- a/src/app/settingsview/components/PluginListView.qml +++ b/src/app/settingsview/components/PluginListView.qml @@ -29,7 +29,6 @@ Rectangle { property string activePlugin: "" - visible: false color: JamiTheme.secondaryBackgroundColor ColumnLayout { @@ -50,6 +49,21 @@ Rectangle { verticalAlignment: Text.AlignVCenter } + MaterialButton { + id: disableAll + TextMetrics { + id: disableTextSize + font.weight: Font.Bold + font.pixelSize: JamiTheme.wizardViewButtonFontPixelSize + font.capitalization: Font.AllUppercase + text: JamiStrings.disableAll + } + secondary: true + preferredWidth: disableTextSize.width + text: JamiStrings.disableAll + fontSize: 15 + } + MaterialButton { id: installButton @@ -88,7 +102,6 @@ Rectangle { id: pluginList Layout.fillWidth: true - Layout.minimumHeight: 0 Layout.bottomMargin: 10 Layout.preferredHeight: childrenRect.height clip: true diff --git a/src/app/settingsview/components/PluginPreferencesView.qml b/src/app/settingsview/components/PluginPreferencesView.qml index a1f188aa166688ee86454bbbfc575f092b09599b..f8c12e321d20f338df2fe3b1fa6b63b88fcf0a72 100644 --- a/src/app/settingsview/components/PluginPreferencesView.qml +++ b/src/app/settingsview/components/PluginPreferencesView.qml @@ -178,7 +178,9 @@ Rectangle { "buttonCallBacks": [function () { pluginPreferencesView.visible = false; PluginModel.uninstallPlugin(pluginId); - installedPluginsModel.removePlugin(index); + PluginListModel.removePlugin(index); + var pluginPath = pluginId.split('/'); + PluginListModel.setVersionStatus(pluginPath[pluginPath.length - 1], PluginStatus.INSTALLABLE); }] }) } diff --git a/src/app/settingsview/components/PluginSettingsPage.qml b/src/app/settingsview/components/PluginSettingsPage.qml index 6b177e94bb5a3a82cc8b8cd356978dca766c2513..6a3a28c67fa7e8bd91b7003291c0022f80cf82c8 100644 --- a/src/app/settingsview/components/PluginSettingsPage.qml +++ b/src/app/settingsview/components/PluginSettingsPage.qml @@ -41,12 +41,9 @@ SettingsPageBase { Layout.preferredWidth: root.width spacing: JamiTheme.settingsCategorySpacing } - // View of installed plugins PluginListView { id: pluginListView - visible: PluginAdapter.isEnabled - Layout.alignment: Qt.AlignTop | Qt.AlignHCenter Layout.preferredWidth: parent.width Layout.minimumHeight: 0 diff --git a/src/app/settingsview/components/PluginStoreListView.qml b/src/app/settingsview/components/PluginStoreListView.qml new file mode 100644 index 0000000000000000000000000000000000000000..a47adc8de82bc29e4d21b988269c9fa9b5233de5 --- /dev/null +++ b/src/app/settingsview/components/PluginStoreListView.qml @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2023 Savoir-faire Linux Inc. + * Author: Xavier Jouslin de Noray <xjouslindenoray@savoirfairelinux.com> + * + * 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 3 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 <http://www.gnu.org/licenses/>. + */ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt.labs.platform +import net.jami.Models 1.1 +import net.jami.Adapters 1.1 +import net.jami.Constants 1.1 +import "../../commoncomponents" + +ColumnLayout { + function installPlugin() { + var dlg = viewCoordinator.presentDialog(appWindow, "commoncomponents/JamiFileDialog.qml", { + "title": JamiStrings.selectPluginInstall, + "fileMode": JamiFileDialog.OpenFile, + "folder": StandardPaths.writableLocation(StandardPaths.DownloadLocation), + "nameFilters": [JamiStrings.pluginFiles, JamiStrings.allFiles] + }); + dlg.fileAccepted.connect(function (file) { + var url = UtilsAdapter.getAbsPath(file.toString()); + PluginModel.installPlugin(url, true); + PluginListModel.addPlugin(); + }); + } + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + Label { + Layout.fillWidth: true + Layout.preferredHeight: 25 + + text: JamiStrings.pluginStoreTitle + font.pointSize: JamiTheme.headerFontSize + font.kerning: true + color: JamiTheme.textColor + + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + } + RowLayout { + Layout.alignment: Qt.AlignRight + MaterialButton { + id: installManually + + TextMetrics { + id: installManuallyTextSize + font.weight: Font.Black + font.pixelSize: JamiTheme.wizardViewButtonFontPixelSize + font.capitalization: Font.Capitalize + text: JamiStrings.installManually + } + secondary: true + preferredWidth: installManuallyTextSize.width + text: JamiStrings.installManually + toolTipText: JamiStrings.installManually + fontSize: 15 + onClicked: installPlugin() + } + } + } + + Flow { + id: pluginStoreList + + Layout.fillWidth: true + spacing: 20 + Layout.preferredHeight: childrenRect.height + clip: true + Repeater { + model: PluginStoreListModel + + delegate: PluginAvailableDelagate { + id: pluginItemDelegate + + width: 350 + height: 400 + pluginId: Id + pluginTitle: Title + pluginIcon: IconPath + pluginBackground: Background === '' ? JamiTheme.backgroundColor : Background + pluginDescription: Description + pluginAuthor: Author + pluginShortDescription: "" + pluginStatus: Status + } + } + } +} diff --git a/src/libclient/api/pluginmodel.h b/src/libclient/api/pluginmodel.h index 7d58c7ebd55773aa68484a525274df004d5453dc..65fe7e375822bc51caf43e5e3ce9c9299e9afdff 100644 --- a/src/libclient/api/pluginmodel.h +++ b/src/libclient/api/pluginmodel.h @@ -38,8 +38,10 @@ namespace plugin { */ struct PluginDetails { + QString id = ""; QString name = ""; QString path = ""; + QString version = ""; QString iconPath = ""; bool loaded = false; }; @@ -102,6 +104,25 @@ public: */ Q_INVOKABLE bool uninstallPlugin(const QString& rootPath); + /** + * @brief get the plugin path + * @param pluginId + * @return plugin path + */ + QString getPluginPath(const QString& pluginId); + + /** + * @brief fetch all plugins path and id + * + */ + void setPluginsPath(); + + /** + * @brief get all plugins id + * @return plugins id + */ + VectorString getPluginsId(); + /** * Load plugin * @return true if plugin was succesfully loaded @@ -184,6 +205,9 @@ public: Q_SIGNALS: void chatHandlerStatusUpdated(bool isVisible); void modelUpdated(); + +private: + MapStringString pluginsPath_ = {}; }; } // namespace api diff --git a/src/libclient/pluginmodel.cpp b/src/libclient/pluginmodel.cpp index 7b11cd4701e5c887d0fe78cb115866d8a4677a1a..3b22a64aebfa4549394bb16374f10e1c830a12a4 100644 --- a/src/libclient/pluginmodel.cpp +++ b/src/libclient/pluginmodel.cpp @@ -38,13 +38,23 @@ // LRC #include "dbus/pluginmanager.h" +enum PluginInstallStatus { + SUCCESS = 0, + PLUGIN_ALREADY_INSTALLED = 100, + PLUGIN_OLD_VERSION = 200, + SIGNATURE_VERIFICATION_FAILED = 300, + CERTIFICATE_VERIFICATION_FAILED = 400, + INVALID_PLUGIN = 500, +} PluginInstallStatus; namespace lrc { using namespace api; PluginModel::PluginModel() : QObject() -{} +{ + setPluginsPath(); +} PluginModel::~PluginModel() {} @@ -87,11 +97,15 @@ PluginModel::getPluginDetails(const QString& path) MapStringString details = PluginManager::instance().getPluginDetails(path); plugin::PluginDetails result; if (!details.empty()) { + result.id = details["id"]; result.name = details["name"]; result.path = path; result.iconPath = details["iconPath"]; + result.version = details["version"]; + } + if (!pluginsPath_.contains(result.id)) { + pluginsPath_[result.id] = path; } - VectorString loadedPlugins = getLoadedPlugins(); if (std::find(loadedPlugins.begin(), loadedPlugins.end(), result.path) != loadedPlugins.end()) { result.loaded = true; @@ -106,7 +120,27 @@ PluginModel::installPlugin(const QString& jplPath, bool force) if (getPluginsEnabled()) { auto result = PluginManager::instance().installPlugin(jplPath, force); Q_EMIT modelUpdated(); - return result; + if (result != 0) { + switch (result) { + case PluginInstallStatus::PLUGIN_ALREADY_INSTALLED: + qWarning() << "Plugin already installed"; + break; + case PluginInstallStatus::PLUGIN_OLD_VERSION: + qWarning() << "Plugin already installed with a newer version"; + break; + case PluginInstallStatus::SIGNATURE_VERIFICATION_FAILED: + qWarning() << "Signature verification failed"; + break; + case PluginInstallStatus::CERTIFICATE_VERIFICATION_FAILED: + qWarning() << "Certificate verification failed"; + break; + case PluginInstallStatus::INVALID_PLUGIN: + qWarning() << "Invalid plugin"; + break; + } + } + pluginsPath_[getPluginDetails(jplPath).id] = jplPath; + return result == 0; } return false; } @@ -115,10 +149,37 @@ bool PluginModel::uninstallPlugin(const QString& rootPath) { auto result = PluginManager::instance().uninstallPlugin(rootPath); + for (auto plugin : pluginsPath_.keys(rootPath)) { + pluginsPath_.remove(plugin); + } Q_EMIT modelUpdated(); return result; } +QString +PluginModel::getPluginPath(const QString& pluginId) +{ + return pluginsPath_[pluginId]; +} + +void +PluginModel::setPluginsPath() +{ + for (auto plugin : getInstalledPlugins()) { + auto details = getPluginDetails(plugin); + pluginsPath_[details.name] = details.path; + } +} + +VectorString +PluginModel::getPluginsId() +{ + if (pluginsPath_.empty()) { + setPluginsPath(); + } + return pluginsPath_.keys(); +} + bool PluginModel::loadPlugin(const QString& path) {