From 5530649f073ca39a19c518b151c8ba6a56a376ab Mon Sep 17 00:00:00 2001 From: Xavier Jouslin de Noray <xavier.jouslindenoray@savoirfairelinux.com> Date: Wed, 28 Jun 2023 10:47:36 -0400 Subject: [PATCH] PluginStore: add view for plugin store Gitlab: #1163 Change-Id: If9d9a27a296c5810b9f99126bed6453cc6ab6852 --- resources/icons/plugins_default_icon.svg | 35 ++ .../commoncomponents/HeaderToggleSwitch.qml | 75 ++++ src/app/commoncomponents/ResponsiveImage.qml | 1 + .../commoncomponents/SpinningAnimation.qml | 7 +- src/app/constant/JamiStrings.qml | 15 +- src/app/constant/JamiTheme.qml | 4 + src/app/mainview/components/CachedImage.qml | 22 +- src/app/pluginadapter.cpp | 64 +++- src/app/pluginadapter.h | 12 +- src/app/pluginlistmodel.cpp | 40 ++- src/app/pluginlistmodel.h | 16 +- src/app/pluginstorelistmodel.cpp | 103 ++++-- src/app/pluginstorelistmodel.h | 9 +- src/app/pluginversionmanager.cpp | 28 +- src/app/pluginversionmanager.h | 2 +- src/app/qmlregister.cpp | 1 - src/app/settingsview/SettingsView.qml | 4 +- .../components/InstallManuallyView.qml | 78 +++++ .../components/PluginAvailableDelagate.qml | 211 ++++++++---- .../components/PluginItemDelegate.qml | 154 +++++---- .../components/PluginListView.qml | 181 +++++----- .../components/PluginPreferencesListView.qml | 12 +- .../components/PluginPreferencesView.qml | 322 +++++++++--------- .../components/PluginSettingsPage.qml | 89 ++++- .../components/PluginStoreListView.qml | 115 +++---- .../components/SettingsPageBase.qml | 61 ++-- src/app/utils.cpp | 6 + src/app/utils.h | 1 + src/libclient/api/pluginmodel.h | 2 +- src/libclient/pluginmodel.cpp | 11 +- 30 files changed, 1071 insertions(+), 610 deletions(-) create mode 100644 resources/icons/plugins_default_icon.svg create mode 100644 src/app/commoncomponents/HeaderToggleSwitch.qml create mode 100644 src/app/settingsview/components/InstallManuallyView.qml diff --git a/resources/icons/plugins_default_icon.svg b/resources/icons/plugins_default_icon.svg new file mode 100644 index 000000000..cf331309b --- /dev/null +++ b/resources/icons/plugins_default_icon.svg @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 428 428" style="enable-background:new 0 0 428 428;" xml:space="preserve"> +<style type="text/css"> + .st0{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_1_);} + .st1{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_2_);} + .st2{fill:url(#SVGID_3_);} + .st3{fill:url(#SVGID_4_);} +</style> +<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="-12.6549" y1="165.4979" x2="165.4206" y2="343.5734"> + <stop offset="0" style="stop-color:#3A3A3A"/> + <stop offset="1" style="stop-color:#818181"/> +</linearGradient> +<path class="st0" d="M1.6,154v21.5v1.4c0.2,0,0.4,0,0.6,0c21.8,0,39.5,17.7,39.5,39.5c0,21.8-17.7,39.5-39.5,39.5 + c-0.2,0-0.4,0-0.6,0V408H101V152c0.2,0.1,0.3,0.1,0.5,0.2V51.4L1.6,154z"/> +<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="227.3426" y1="84.8295" x2="405.4313" y2="262.9182"> + <stop offset="0" style="stop-color:#3A3A3A"/> + <stop offset="1" style="stop-color:#717171"/> +</linearGradient> +<path class="st1" d="M386.9,176.8L386.9,176.8l0-157.8h-93.7v256c-0.1-0.1-0.3-0.1-0.4-0.2v100.8l94.2-96.7v-23.2h0 + c21.8,0,39.5-17.7,39.5-39.5C426.4,194.5,408.7,176.8,386.9,176.8z"/> +<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="1.6097" y1="86.4966" x2="395.2845" y2="86.4966"> + <stop offset="0" style="stop-color:#818181"/> + <stop offset="1" style="stop-color:#3A3A3A"/> +</linearGradient> +<path class="st2" d="M181.3,19C126.4,19,1.6,31.5,1.6,154c0,0,49.3-42.7,136.9-25s160.9-0.7,207.6-49.1 + c18.6-19.3,32.4-39.6,40.2-60.9H181.3z"/> +<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="-553.2536" y1="-1072.2665" x2="-159.5788" y2="-1072.2665" gradientTransform="matrix(-1 0 0 -1 -166.3346 -728.8083)"> + <stop offset="0" style="stop-color:#818181"/> + <stop offset="1" style="stop-color:#3A3A3A"/> +</linearGradient> +<path class="st3" d="M207.2,408c54.9,0,179.7-11.9,179.7-129.1c0,0-49.3,40.8-136.9,23.9c-87.6-16.9-160.9,0.7-207.6,47 + C23.8,368.3,10.1,387.6,2.2,408H207.2z"/> +</svg> \ No newline at end of file diff --git a/src/app/commoncomponents/HeaderToggleSwitch.qml b/src/app/commoncomponents/HeaderToggleSwitch.qml new file mode 100644 index 000000000..938e10430 --- /dev/null +++ b/src/app/commoncomponents/HeaderToggleSwitch.qml @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2022-2023 Savoir-faire Linux Inc. + * Author: Xavier Jouslin <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 <https://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 + +RowLayout { + id: root + property string labelText: "" + property int widthOfSwitch: 50 + property int heightOfSwitch: 10 + + property string tooltipText: "" + + property alias toggleSwitch: autoupdate + property alias checked: autoupdate.checked + + signal switchToggled + Layout.alignment: Qt.AlignRight + JamiSwitch { + id: autoupdate + Layout.alignment: Qt.AlignLeft + + Layout.preferredWidth: widthOfSwitch + + hoverEnabled: true + toolTipText: tooltipText + + Accessible.role: Accessible.Button + Accessible.name: JamiStrings.autoUpdate + Accessible.description: root.tooltipText + + onToggled: switchToggled() + } + Text { + id: description + Layout.rightMargin: JamiTheme.preferredMarginSize + text: JamiStrings.autoUpdate + font.pixelSize: JamiTheme.wizardViewButtonFontPixelSize + visible: labelText !== "" + font.kerning: true + wrapMode: Text.WordWrap + verticalAlignment: Text.AlignVCenter + + color: JamiTheme.textColor + } + TapHandler { + target: parent + enabled: parent.visible + onTapped: function onTapped(eventPoint) { + // switchToggled should be emitted as onToggled is not called (because it's only called if the user click on the switch) + autoupdate.toggle(); + switchToggled(); + } + } +} diff --git a/src/app/commoncomponents/ResponsiveImage.qml b/src/app/commoncomponents/ResponsiveImage.qml index c1fcc63b4..4ba427322 100644 --- a/src/app/commoncomponents/ResponsiveImage.qml +++ b/src/app/commoncomponents/ResponsiveImage.qml @@ -68,6 +68,7 @@ Item { antialiasing: true asynchronous: true visible: false + mipmap: true function setSourceSize() { sourceSize = undefined; diff --git a/src/app/commoncomponents/SpinningAnimation.qml b/src/app/commoncomponents/SpinningAnimation.qml index 740df45d4..d450e0d5c 100644 --- a/src/app/commoncomponents/SpinningAnimation.qml +++ b/src/app/commoncomponents/SpinningAnimation.qml @@ -32,9 +32,8 @@ Item { property int spinningAnimationWidth: 4 property real outerCutRadius: root.height / 2 property int spinningAnimationDuration: 1000 - + property color color: "white" visible: mode !== SpinningAnimation.Mode.Disabled - ConicalGradient { id: conicalGradientOne @@ -48,7 +47,7 @@ Item { } GradientStop { position: 1.0 - color: "white" + color: mode === SpinningAnimation.Mode.Disabled ? "transparent" : root.color } } @@ -90,7 +89,7 @@ Item { } GradientStop { position: 1.0 - color: "white" + color: mode === SpinningAnimation.Mode.Disabled ? "transparent" : root.color } } diff --git a/src/app/constant/JamiStrings.qml b/src/app/constant/JamiStrings.qml index 4278a99d3..06122b9d5 100644 --- a/src/app/constant/JamiStrings.qml +++ b/src/app/constant/JamiStrings.qml @@ -166,7 +166,7 @@ Item { property string back: qsTr("Back") property string accountSettingsMenuTitle: qsTr("Account") property string generalSettingsTitle: qsTr("General") - property string pluginSettingsTitle: qsTr("Plugins") + property string pluginSettingsTitle: qsTr("Extensions") property string enableAccountSettingsTitle: qsTr("Enable account") property string manageAccountSettingsTitle: qsTr("Manage account") property string linkedDevicesSettingsTitle: qsTr("Linked devices") @@ -659,9 +659,19 @@ Item { // Plugins property string enable: qsTr("Enable") + property string autoUpdate: qsTr("Auto update") + property string disableAll: qsTr("Disable all") + property string installed: qsTr("Installed") + property string install: qsTr("Install") + property string installing: qsTr("Installing") + property string installManually: qsTr("Install manually") + property string installMannuallyDescription: qsTr("Install an extension directly from your device.") + property string pluginStoreTitle: qsTr("Available") + property string pluginStoreNotAvailable: qsTr("Plugins store is not available") property string pluginPreferences: qsTr("Preferences") + property string installationFailed: qsTr("Installation failed") + property string pluginInstallationFailed: qsTr("The installation of the plugin failed") 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") @@ -677,7 +687,6 @@ Item { property string chooseImageFile: qsTr("Choose image file") property string tipGeneralPluginSettingsDisplay: qsTr("Display or hide General plugin settings") property string tipAccountPluginSettingsDisplay: qsTr("Display or hide Account plugin settings") - property string installedPlugins: qsTr("Installed plugins") property string pluginFiles: qsTr("Plugin Files (*.jpl)") property string loadUnload: qsTr("Load/Unload") property string selectAnImage: qsTr("Select An Image to %1") diff --git a/src/app/constant/JamiTheme.qml b/src/app/constant/JamiTheme.qml index 3d39ed34a..4b0f5c8d4 100644 --- a/src/app/constant/JamiTheme.qml +++ b/src/app/constant/JamiTheme.qml @@ -383,6 +383,10 @@ Item { property real minimumMapWidth: 250 property real pluginHandlersPopupViewHeight: 200 property real pluginHandlersPopupViewDelegateHeight: 50 + property color pluginDefaultBackgroundColor: "#666666" + property real remotePluginWidthDelegate: 350 + property real remotePluginHeightDelegate: 400 + property color pluginViewBackgroundColor: darkTheme ? "#000000" : "#F0EFEF" property real secondaryDialogDimension: 500 property real lineEditContextMenuItemsHeight: 15 diff --git a/src/app/mainview/components/CachedImage.qml b/src/app/mainview/components/CachedImage.qml index 50382a042..4d770c0b5 100644 --- a/src/app/mainview/components/CachedImage.qml +++ b/src/app/mainview/components/CachedImage.qml @@ -42,15 +42,11 @@ Item { antialiasing: true property bool isGif: getIsGif(this) - Image { - id: default_img - objectName: "default_img" - anchors.fill: parent - source: defaultImage - visible: image.status != Image.Ready - smooth: true - antialiasing: true - property bool isGif: getIsGif(this) + source: defaultImage + onStatusChanged: { + if (status === Image.Error) { + source = defaultImage; + } } } @@ -70,13 +66,6 @@ Item { function onDownloadImageSuccessful(localPath) { if (localPath === cachedImage.localPath) { image.source = "file://" + localPath; - print("onDownloadImageSuccessful", localPath); - } - } - function onDownloadImageFailed(localPath) { - if (localPath === cachedImage.localPath) { - print("Failed to download image: " + downloadUrl); - image.source = defaultImage; } } } @@ -99,7 +88,6 @@ Item { } if (downloadUrl && downloadUrl !== "" && localPath !== "") { if (!UtilsAdapter.fileExists(localPath)) { - print("ImageDownloader.downloadImage", downloadUrl, localPath); ImageDownloader.downloadImage(downloadUrl, localPath); } else { image.source = "file://" + localPath; diff --git a/src/app/pluginadapter.cpp b/src/app/pluginadapter.cpp index 8b2a464fa..bbf5ee606 100644 --- a/src/app/pluginadapter.cpp +++ b/src/app/pluginadapter.cpp @@ -18,9 +18,13 @@ #include "pluginadapter.h" +#include "pluginversionmanager.h" +#include "pluginlistmodel.h" +#include "pluginstorelistmodel.h" #include "networkmanager.h" #include "lrcinstance.h" #include "utilsadapter.h" +#include "qmlregister.h" #include <QJsonArray> #include <QJsonDocument> @@ -30,14 +34,16 @@ PluginAdapter::PluginAdapter(LRCInstance* instance, QObject* parent, QString baseUrl) : QmlAdapterBase(instance, parent) - , pluginStoreListModel_(new PluginStoreListModel(this)) + , pluginStoreListModel_(new PluginStoreListModel(instance, this)) , pluginVersionManager_(new PluginVersionManager(instance, baseUrl, this)) - , pluginListModel_(new PluginListModel(this)) + , pluginListModel_(new PluginListModel(instance, this)) , lrcInstance_(instance) , tempPath_(QDir::tempPath()) , baseUrl_(baseUrl) { + QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, pluginStoreListModel_, "PluginStoreListModel"); + QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, pluginListModel_, "PluginListModel") set_isEnabled(lrcInstance_->pluginModel().getPluginsEnabled()); updateHandlersListCount(); connect(&lrcInstance_->pluginModel(), @@ -75,24 +81,34 @@ PluginAdapter::PluginAdapter(LRCInstance* instance, QObject* parent, QString bas 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); - }); + const auto& errorHandler = connect(pluginVersionManager_, + &PluginVersionManager::errorOccurred, + this, + [this](NetworkManager::GetError error, const QString& msg) { + Q_EMIT storeNotAvailable(); + }); + pluginVersionManager_ + ->sendGetRequest(QUrl(baseUrl_ + "?arch=" + Utils::getPlatformString()), + [this, errorHandler](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["name"].toString())) { + plugins.append(qPlugin); + } + } + pluginStoreListModel_->setPlugins(plugins); + disconnect(errorHandler); + }); } void PluginAdapter::getPluginDetails(const QString& pluginId) { - pluginVersionManager_->sendGetRequest(QUrl(baseUrl_ + "/details/" + pluginId), + pluginVersionManager_->sendGetRequest(QUrl(baseUrl_ + "/details/" + pluginId + + "?arch=" + Utils::getPlatformString()), [this](const QByteArray& data) { auto result = QJsonDocument::fromJson(data).object(); // my response is a json object and I want to convert @@ -114,6 +130,18 @@ PluginAdapter::isAutoUpdaterEnabled() return pluginVersionManager_->isAutoUpdaterEnabled(); } +void +PluginAdapter::setAutoUpdate(bool state) +{ + pluginVersionManager_->setAutoUpdate(state); +} + +void +PluginAdapter::cancelDownload(const QString& pluginId) +{ + pluginVersionManager_->cancelUpdate(pluginId); +} + QVariant PluginAdapter::getMediaHandlerSelectableModel(const QString& callId) { @@ -169,3 +197,9 @@ PluginAdapter::baseUrl() const { return baseUrl_; } + +QString +PluginAdapter::getIconUrl(const QString& pluginId) const +{ + return baseUrl_ + "/icons/" + pluginId + "?arch=" + Utils::getPlatformString(); +} diff --git a/src/app/pluginadapter.h b/src/app/pluginadapter.h index f994b089f..f168726f6 100644 --- a/src/app/pluginadapter.h +++ b/src/app/pluginadapter.h @@ -30,6 +30,9 @@ #include <QSortFilterProxyModel> #include <QString> +class PluginVersionManager; +class PluginStoreListModel; + class PluginAdapter final : public QmlAdapterBase { Q_OBJECT @@ -49,6 +52,9 @@ public: Q_INVOKABLE QString baseUrl() const; Q_INVOKABLE void checkVersionStatus(const QString& pluginId); Q_INVOKABLE bool isAutoUpdaterEnabled(); + Q_INVOKABLE void cancelDownload(const QString& pluginId); + Q_INVOKABLE void setAutoUpdate(bool state); + Q_INVOKABLE QString getIconUrl(const QString& pluginId) const; protected: Q_INVOKABLE QVariant getMediaHandlerSelectableModel(const QString& callId); @@ -57,14 +63,18 @@ protected: Q_INVOKABLE QVariant getPluginPreferencesCategories(const QString& pluginId, const QString& accountId, bool removeLast = false); +Q_SIGNALS: + void storeNotAvailable(); private: void updateHandlersListCount(); - std::unique_ptr<PluginHandlerListModel> pluginHandlerListModel_; PluginStoreListModel* pluginStoreListModel_; PluginVersionManager* pluginVersionManager_; PluginListModel* pluginListModel_; + + std::unique_ptr<PluginHandlerListModel> pluginHandlerListModel_; + LRCInstance* lrcInstance_; std::mutex mtx_; QString tempPath_; diff --git a/src/app/pluginlistmodel.cpp b/src/app/pluginlistmodel.cpp index 3dc41db43..670ee5c8c 100644 --- a/src/app/pluginlistmodel.cpp +++ b/src/app/pluginlistmodel.cpp @@ -22,9 +22,12 @@ #include "api/pluginmodel.h" -PluginListModel::PluginListModel(QObject* parent) +PluginListModel::PluginListModel(LRCInstance* lrcInstance, QObject* parent) : AbstractListModelBase(parent) -{} + , lrcInstance_(lrcInstance) +{ + reset(); +} PluginListModel::~PluginListModel() {} @@ -55,16 +58,20 @@ PluginListModel::data(const QModelIndex& index, int role) const } auto details = lrcInstance_->pluginModel().getPluginDetails(installedPlugins_.at(index.row())); - + installedPlugins_.at(index.row()); switch (role) { case Role::PluginName: return QVariant(details.name); + case Role::PluginDescription: + return QVariant(details.description); case Role::PluginId: return QVariant(installedPlugins_.at(index.row())); case Role::PluginIcon: return QVariant(details.iconPath); case Role::IsLoaded: return QVariant(details.loaded); + case Role::Status: + return QVariant(pluginStatus_.value(installedPlugins_.at(index.row()))); } return QVariant(); } @@ -77,7 +84,8 @@ PluginListModel::roleNames() const roles[PluginId] = "PluginId"; roles[PluginIcon] = "PluginIcon"; roles[IsLoaded] = "IsLoaded"; - + roles[Status] = "Status"; + roles[PluginDescription] = "PluginDescription"; return roles; } @@ -87,6 +95,9 @@ PluginListModel::reset() beginResetModel(); installedPlugins_.clear(); installedPlugins_ = lrcInstance_->pluginModel().getInstalledPlugins(); + for (auto plugin : installedPlugins_) { + pluginStatus_[plugin] = PluginStatus::INSTALLED; + } filterPlugins(installedPlugins_); endResetModel(); } @@ -97,6 +108,7 @@ PluginListModel::removePlugin(int index) beginRemoveRows(QModelIndex(), index, index); installedPlugins_.removeAt(index); endRemoveRows(); + reset(); } void @@ -120,9 +132,18 @@ PluginListModel::addPlugin() index++; } - beginInsertRows(QModelIndex(), index, index); - installedPlugins_ = newList; - endInsertRows(); + reset(); +} + +void +PluginListModel::disableAllPlugins() +{ + for (auto& plugin : installedPlugins_) { + auto& pluginModel = lrcInstance_->pluginModel(); + const auto& details = pluginModel.getPluginDetails(plugin); + pluginModel.unloadPlugin(details.path); + disabled(details.path); + } } void @@ -165,14 +186,11 @@ PluginListModel::onVersionStatusChanged(const QString& pluginId, PluginStatus::R if (pluginIndex == -1) { return; } - + pluginStatus_[pluginId] = status; switch (status) { case PluginStatus::INSTALLABLE: removePlugin(pluginIndex); break; - case PluginStatus::FAILED: - qWarning() << "Failed to install plugin" << pluginId; - break; default: break; } diff --git a/src/app/pluginlistmodel.h b/src/app/pluginlistmodel.h index d5c445183..84dd1c010 100644 --- a/src/app/pluginlistmodel.h +++ b/src/app/pluginlistmodel.h @@ -28,10 +28,17 @@ class PluginListModel : public AbstractListModelBase Q_OBJECT QML_PROPERTY(bool, filterAccount) public: - enum Role { PluginName = Qt::UserRole + 1, PluginId, PluginIcon, IsLoaded }; + enum Role { + PluginName = Qt::UserRole + 1, + PluginDescription, + PluginId, + PluginIcon, + IsLoaded, + Status + }; Q_ENUM(Role) - explicit PluginListModel(QObject* parent = nullptr); + explicit PluginListModel(LRCInstance* lrcInstance, QObject* parent = nullptr); ~PluginListModel(); /* @@ -52,16 +59,19 @@ public: Q_INVOKABLE void removePlugin(int index); Q_INVOKABLE void pluginChanged(int index); Q_INVOKABLE void addPlugin(); + Q_INVOKABLE void disableAllPlugins(); Q_SIGNALS: void versionCheckRequested(const QString& pluginId); void setVersionStatus(const QString& pluginId, PluginStatus::Role status); void autoUpdateChanged(bool state); - + void disabled(const QString& pluginId); public Q_SLOTS: void onVersionStatusChanged(const QString& pluginId, PluginStatus::Role status); private: + LRCInstance* lrcInstance_ = nullptr; void filterPlugins(VectorString& list) const; VectorString installedPlugins_ {}; + QMap<QString, PluginStatus::Role> pluginStatus_ {}; }; diff --git a/src/app/pluginstorelistmodel.cpp b/src/app/pluginstorelistmodel.cpp index a90b79828..769954a7d 100644 --- a/src/app/pluginstorelistmodel.cpp +++ b/src/app/pluginstorelistmodel.cpp @@ -17,10 +17,17 @@ #include "pluginstorelistmodel.h" +#include "lrcinstance.h" + +#include "api/pluginmodel.h" + #include <QUrl> -PluginStoreListModel::PluginStoreListModel(QObject* parent) +#include <algorithm> + +PluginStoreListModel::PluginStoreListModel(LRCInstance* lrcInstance, QObject* parent) : AbstractListModelBase(parent) + , lrcInstance_(lrcInstance) {} int @@ -41,20 +48,16 @@ PluginStoreListModel::data(const QModelIndex& index, int role) const } auto plugin = plugins_.at(index.row()); switch (role) { - case Role::Id: - return QVariant(plugin["id"].toString()); - case Role::Title: + case Role::Name: 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(plugin.value("status", PluginStatus::INSTALLABLE)); } return QVariant(); } @@ -84,14 +87,16 @@ PluginStoreListModel::addPlugin(const QVariantMap& plugin) beginInsertRows(QModelIndex(), plugins_.size(), plugins_.size()); plugins_.append(plugin); endInsertRows(); + sort(); } void PluginStoreListModel::setPlugins(const QList<QVariantMap>& plugins) { beginResetModel(); - plugins_ = plugins; + plugins_ = filterPlugins(plugins); endResetModel(); + sort(); } void @@ -99,7 +104,7 @@ PluginStoreListModel::removePlugin(const QString& pluginId) { auto index = 0; for (auto& plugin : plugins_) { - if (plugin["id"].toString() == pluginId) { + if (plugin["name"].toString() == pluginId) { beginRemoveRows(QModelIndex(), index, index); plugins_.removeAt(index); endRemoveRows(); @@ -107,6 +112,7 @@ PluginStoreListModel::removePlugin(const QString& pluginId) } index++; } + sort(); } void @@ -114,13 +120,14 @@ PluginStoreListModel::updatePlugin(const QVariantMap& plugin) { auto index = 0; for (auto& p : plugins_) { - if (p["id"].toString() == plugin["id"].toString()) { + if (p["name"].toString() == plugin["name"].toString()) { p = plugin; Q_EMIT dataChanged(createIndex(index, 0), createIndex(index, 0)); return; } index++; } + sort(); } QColor @@ -151,7 +158,7 @@ PluginStoreListModel::computeAverageColorOfImage(const QString& file) blue += pixelColor.blue(); } } - return QColor(red / nPixels, green / nPixels, blue / nPixels, 70); + return QColor(red / nPixels, green / nPixels, blue / nPixels); } else { // Return an invalid color. return QColor(); @@ -161,36 +168,80 @@ PluginStoreListModel::computeAverageColorOfImage(const QString& file) void PluginStoreListModel::onVersionStatusChanged(const QString& pluginId, PluginStatus::Role status) { - auto plugin = QVariantMap(); - for (auto& p : plugins_) { - if (p["id"].toString() == pluginId) { - plugin = p; - break; - } - } + auto it = std::find_if(plugins_.begin(), plugins_.end(), [&pluginId](const QVariantMap& p) { + return p["name"].toString() == pluginId; + }); + switch (status) { case PluginStatus::INSTALLABLE: - if (!plugin.isEmpty()) + if (it != plugins_.end()) { break; + } pluginAdded(pluginId); break; - default: break; } - if (plugin.isEmpty()) { + + if (it == plugins_.end()) { return; } - plugin["status"] = status; + auto& plugin = *it; + plugin["status"] = status; + auto index = createIndex(rowFromPluginId(pluginId), 0); + if (index.isValid()) { + Q_EMIT dataChanged(index, index, {PluginStoreList::Role::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 +} + +int +PluginStoreListModel::rowFromPluginId(const QString& pluginId) const +{ + const auto it = std::find_if(plugins_.begin(), + plugins_.end(), + [&pluginId](const QVariantMap& p) { + return p["name"].toString() == pluginId; + }); + if (it != plugins_.end()) { + return std::distance(plugins_.begin(), it); + } + return -1; +} + +void +PluginStoreListModel::sort() +{ + beginResetModel(); + std::sort(plugins_.begin(), plugins_.end(), [](const QVariantMap& a, const QVariantMap& b) { + return a["timestamp"].toString() < b["timestamp"].toString(); + }); + endResetModel(); +} + +QList<QVariantMap> +PluginStoreListModel::filterPlugins(const QList<QVariantMap>& plugins) +{ + auto& pluginModel = lrcInstance_->pluginModel(); + auto installedPlugins = pluginModel.getInstalledPlugins(); + QList<QVariantMap> filterPluginsNotInstalled; + for (auto& remotePlugin : plugins) { + if (std::find_if(installedPlugins.begin(), + installedPlugins.end(), + [remotePlugin, &pluginModel, this](const QString& installedPlugin) { + const auto& details = pluginModel.getPluginDetails(installedPlugin); + return remotePlugin["name"].toString() == details.name; + }) + == installedPlugins.end()) { + filterPluginsNotInstalled.append(remotePlugin); + } + } + return filterPluginsNotInstalled; +} diff --git a/src/app/pluginstorelistmodel.h b/src/app/pluginstorelistmodel.h index 7a603c510..53654feb3 100644 --- a/src/app/pluginstorelistmodel.h +++ b/src/app/pluginstorelistmodel.h @@ -24,8 +24,7 @@ class QColor; class QString; #define PLUGINSTORE_ROLES \ - X(Id) \ - X(Title) \ + X(Name) \ X(IconPath) \ X(Background) \ X(Description) \ @@ -48,7 +47,7 @@ class PluginStoreListModel : public AbstractListModelBase Q_OBJECT public: - explicit PluginStoreListModel(QObject* parent = nullptr); + explicit PluginStoreListModel(LRCInstance* lrcInstance, QObject* parent = nullptr); int rowCount(const QModelIndex& parent = QModelIndex()) const override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; @@ -69,6 +68,10 @@ public Q_SLOTS: void onVersionStatusChanged(const QString& pluginId, PluginStatus::Role status); private: + QList<QVariantMap> filterPlugins(const QList<QVariantMap>& plugins); + int rowFromPluginId(const QString& pluginId) const; + void sort(); using Role = PluginStoreList::Role; QList<QVariantMap> plugins_; + LRCInstance* lrcInstance_ {}; }; diff --git a/src/app/pluginversionmanager.cpp b/src/app/pluginversionmanager.cpp index 28217891f..e508402b9 100644 --- a/src/app/pluginversionmanager.cpp +++ b/src/app/pluginversionmanager.cpp @@ -45,7 +45,6 @@ public: return; } for (const auto& pluginId : qAsConst(pluginsId)) { - Q_EMIT parent_.versionStatusChanged(pluginId, PluginStatus::Role::INSTALLING); parent_.pluginRepliesId.remove(pluginId); } }); @@ -74,6 +73,7 @@ public: return; } parent_.cancelDownload(parent_.pluginRepliesId[pluginId]); + parent_.versionStatusChanged(pluginId, PluginStatus::Role::INSTALLABLE); }; bool isAutoUpdaterEnabled() @@ -103,7 +103,8 @@ public: return; } - parent_.sendGetRequest(QUrl(parent_.baseUrl + "/versions/" + plugin.id), + parent_.sendGetRequest(QUrl(parent_.baseUrl + "/versions/" + plugin.id + + "?arch=" + Utils::getPlatformString()), [this, plugin](const QByteArray& data) { // `data` represents the version in this case. if (plugin.version < data) { @@ -120,7 +121,7 @@ public: void installRemotePlugin(const QString& pluginId) { parent_.downloadFile( - QUrl(parent_.baseUrl + "/download/" + pluginId), + QUrl(parent_.baseUrl + "/download/" + Utils::getPlatformString() + "/" + pluginId), pluginId, 0, [this, pluginId](bool success, const QString& error) { @@ -129,14 +130,17 @@ public: 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); - } + QThreadPool::globalInstance()->start([this, pluginId] { + 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); + } + }); + parent_.versionStatusChanged(pluginId, PluginStatus::Role::INSTALLING); }, tempPath_ + '/'); Q_EMIT parent_.versionStatusChanged(pluginId, PluginStatus::Role::DOWNLOADING); @@ -167,7 +171,7 @@ PluginVersionManager::PluginVersionManager(LRCInstance* instance, QString& baseU PluginVersionManager::~PluginVersionManager() { - for (const auto& pluginReplyId : qAsConst(pluginRepliesId)) { + for (const auto& pluginReplyId : pluginRepliesId.values()) { cancelDownload(pluginReplyId); } pluginRepliesId.clear(); diff --git a/src/app/pluginversionmanager.h b/src/app/pluginversionmanager.h index 365006bbe..a56a2a079 100644 --- a/src/app/pluginversionmanager.h +++ b/src/app/pluginversionmanager.h @@ -72,7 +72,7 @@ Q_SIGNALS: private: QString baseUrl; bool autoUpdateCheck = false; - QMap<QString, unsigned int> pluginRepliesId {}; + QMap<QString, int> pluginRepliesId {}; struct Impl; friend struct Impl; std::unique_ptr<Impl> pimpl_; diff --git a/src/app/qmlregister.cpp b/src/app/qmlregister.cpp index 4ffec74d0..6e2f3377b 100644 --- a/src/app/qmlregister.cpp +++ b/src/app/qmlregister.cpp @@ -174,7 +174,6 @@ registerTypes(QQmlEngine* engine, QML_REGISTERTYPE(NS_MODELS, MediaCodecListModel); QML_REGISTERTYPE(NS_MODELS, AudioDeviceModel); QML_REGISTERTYPE(NS_MODELS, AudioManagerListModel); - QML_REGISTERTYPE(NS_MODELS, PluginListModel); QML_REGISTERTYPE(NS_MODELS, PreferenceItemListModel); QML_REGISTERTYPE(NS_MODELS, PluginListPreferenceModel); QML_REGISTERTYPE(NS_MODELS, FilesToSendListModel); diff --git a/src/app/settingsview/SettingsView.qml b/src/app/settingsview/SettingsView.qml index 3cc6a12da..375d92e67 100644 --- a/src/app/settingsview/SettingsView.qml +++ b/src/app/settingsview/SettingsView.qml @@ -59,14 +59,14 @@ ListSelectionView { leftPaneItem: viewCoordinator.getView("SettingsSidePanel") Component.onCompleted: { - leftPaneItem.updateModel() + leftPaneItem.updateModel(); } Connections { target: viewNode function onIsSinglePaneChanged() { - leftPaneItem.isSinglePane = viewNode.isSinglePane + leftPaneItem.isSinglePane = viewNode.isSinglePane; } } diff --git a/src/app/settingsview/components/InstallManuallyView.qml b/src/app/settingsview/components/InstallManuallyView.qml new file mode 100644 index 000000000..5c0d02eaf --- /dev/null +++ b/src/app/settingsview/components/InstallManuallyView.qml @@ -0,0 +1,78 @@ +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()); + var isInstall = PluginModel.installPlugin(url, true); + if (isInstall) { + PluginListModel.addPlugin(); + } else { + presentErrorMessage(); + } + }); + } + + function presentErrorMessage() { + viewCoordinator.presentDialog(appWindow, "commoncomponents/SimpleMessageDialog.qml", { + "title": JamiStrings.installationFailed, + "infoText": JamiStrings.pluginInstallationFailed, + "buttonStyles": [SimpleMessageDialog.ButtonStyle.TintedBlue], + "buttonTitles": [JamiStrings.optionOk], + "buttonCallBacks": [] + }); + } + + Label { + Layout.fillWidth: true + Layout.bottomMargin: 20 + + text: JamiStrings.installManually + font.pixelSize: JamiTheme.settingsTitlePixelSize + font.kerning: true + color: JamiTheme.textColor + + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + } + Text { + id: descriptionInstallManually + Layout.fillWidth: true + font.pixelSize: JamiTheme.popuptextSize + color: JamiTheme.textColor + text: JamiStrings.installMannuallyDescription + wrapMode: Text.WordWrap + horizontalAlignment: Qt.AlignLeft + lineHeight: 1.5 + textFormat: Text.PlainText + } + MaterialButton { + id: installManually + radius: JamiTheme.chatViewHeaderButtonRadius + TextMetrics { + id: textSize + font.weight: Font.Black + font.pixelSize: JamiTheme.wizardViewButtonFontPixelSize + font.capitalization: Font.AllUppercase + text: installManually.text + } + primary: true + preferredWidth: textSize.width + 2 * JamiTheme.buttontextWizzardPadding + text: JamiStrings.install + fontSize: JamiTheme.popuptextSize + onClicked: installPlugin() + } +} diff --git a/src/app/settingsview/components/PluginAvailableDelagate.qml b/src/app/settingsview/components/PluginAvailableDelagate.qml index 0d66d5d98..7907315bf 100644 --- a/src/app/settingsview/components/PluginAvailableDelagate.qml +++ b/src/app/settingsview/components/PluginAvailableDelagate.qml @@ -27,114 +27,195 @@ import "../../mainview/components" ItemDelegate { id: root - property string pluginId - property string pluginTitle + property string pluginName property string pluginIcon - property string pluginBackground + property string pluginBackground: JamiTheme.pluginDefaultBackgroundColor property string pluginDescription property string pluginAuthor property string pluginShortDescription property int pluginStatus + property string installButtonStatus: { + switch (pluginStatus) { + case PluginStatus.DOWNLOADING: + return JamiStrings.cancel; + case PluginStatus.INSTALLABLE: + return JamiStrings.install; + case PluginStatus.INSTALLING: + return JamiStrings.installing; + default: + return JamiStrings.install; + } + } + onPluginStatusChanged: { + if (pluginStatus === PluginStatus.FAILED) { + presentErrorMessage(); + } + } + + background: null + + function presentErrorMessage() { + viewCoordinator.presentDialog(appWindow, "commoncomponents/SimpleMessageDialog.qml", { + "title": JamiStrings.installationFailed, + "infoText": JamiStrings.pluginInstallationFailed, + "buttonStyles": [SimpleMessageDialog.ButtonStyle.TintedBlue], + "buttonTitles": [JamiStrings.optionOk], + "buttonCallBacks": [] + }); + } Rectangle { - id: rect - Scaffold { - } - color: Qt.rgba(0, 0, 0, 1) + id: mask anchors.fill: parent - radius: 15 + radius: 5 } Page { id: plugin anchors.fill: parent + layer { + enabled: true + effect: OpacityMask { + maskSource: mask + } + } header: Control { - padding: 10 + leftPadding: 20 + rightPadding: 5 + bottomPadding: 20 + topPadding: 5 background: Rectangle { - color: pluginBackground + id: headerBackground + color: hovered ? Qt.lighter(pluginBackground, 1.9) : Qt.lighter(pluginBackground, 2) } contentItem: ColumnLayout { - RowLayout { + SpinningAnimation { + id: buttonContainer + visible: true Layout.alignment: Qt.AlignTop | Qt.AlignRight + Layout.rightMargin: 8 + Layout.topMargin: 2 + Layout.preferredHeight: install.height + Layout.preferredWidth: install.width + color: "black" + outerCutRadius: install.radius + spinningAnimationDuration: 5000 + mode: { + if (pluginStatus === PluginStatus.INSTALLABLE || pluginStatus === PluginStatus.FAILED) { + SpinningAnimation.Mode.Disabled; + } else { + SpinningAnimation.Mode.Radial; + } + } + MaterialButton { id: install - Layout.alignment: Qt.AlignRight - Layout.rightMargin: 8 - Layout.topMargin: 8 - Layout.preferredHeight: 20 + hoverEnabled: pluginStatus !== PluginStatus.INSTALLING + secHoveredColor: Qt.darker(headerBackground.color, 1.1) + buttontextHeightMargin: 10.0 + radius: JamiTheme.chatViewHeaderButtonRadius TextMetrics { id: installTextSize font.weight: Font.Black font.pixelSize: JamiTheme.wizardViewButtonFontPixelSize font.capitalization: Font.Medium - text: isDownloading() ? JamiStrings.cancel : JamiStrings.install + text: install.text } + contentColorProvider: "black" onClicked: installPlugin() secondary: true preferredWidth: installTextSize.width + JamiTheme.buttontextWizzardPadding - text: isDownloading() ? JamiStrings.cancel : JamiStrings.install - fontSize: 15 + text: { + switch (pluginStatus) { + case PluginStatus.DOWNLOADING: + return JamiStrings.cancel; + case PluginStatus.INSTALLABLE: + return JamiStrings.install; + case PluginStatus.INSTALLING: + return JamiStrings.installing; + default: + return JamiStrings.install; + } + } } } RowLayout { spacing: 10 - CachedImage { id: icon - Component.onCompleted: { - pluginBackground = PluginStoreListModel.computeAverageColorOfImage("file://" + UtilsAdapter.getCachePath() + '/plugins/' + pluginId + '.svg'); + defaultImage: JamiResources.plugins_default_icon_svg + onSourceChanged: { + if (source == defaultImage) { + pluginBackground = JamiTheme.pluginDefaultBackgroundColor; + return; + } + pluginBackground = PluginStoreListModel.computeAverageColorOfImage(source); } - width: 50 - height: 50 - downloadUrl: PluginAdapter.baseUrl + "/icons/" + pluginId + width: 55 + height: 55 + downloadUrl: PluginAdapter.getIconUrl(pluginName) fileExtension: '.svg' - localPath: UtilsAdapter.getCachePath() + '/plugins/' + pluginId + '.svg' + localPath: UtilsAdapter.getCachePath() + '/plugins/' + pluginName + '.svg' } ColumnLayout { + width: parent.width Label { - text: pluginTitle + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + text: pluginName 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 + color: "black" + font.pointSize: JamiTheme.tinyCreditsTextSize + textFormat: Text.PlainText + wrapMode: Text.WrapAnywhere } + // Label { + // Layout.fillWidth: true + // color: "black" + // text: pluginShortDescription + // font.pointSize: JamiTheme.settingsFontSize + // textFormat: Text.PlainText + // wrapMode: Text.WordWrap + // } } } } } Rectangle { + id: contentContainer anchors.fill: parent - color: JamiTheme.pluginViewBackgroundColor + color: hovered ? JamiTheme.smartListHoveredColor : JamiTheme.pluginViewBackgroundColor } - Flickable { + JamiFlickable { anchors.fill: parent - anchors.margins: 10 - contentWidth: description.width + anchors.margins: 20 contentHeight: description.height clip: true flickableDirection: Flickable.VerticalFlick - ScrollBar.vertical: ScrollBar { + ScrollBar.vertical: JamiScrollBar { id: scrollBar policy: ScrollBar.AsNeeded } - Text { - id: description + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ColumnLayout { width: parent.width - color: JamiTheme.textColor - text: pluginDescription - wrapMode: Text.WordWrap + Text { + id: description + Layout.preferredWidth: contentContainer.width + font.pixelSize: JamiTheme.popuptextSize + color: JamiTheme.textColor + text: pluginDescription + wrapMode: Text.WordWrap + horizontalAlignment: Qt.AlignLeft + lineHeight: 1.5 + textFormat: Text.MarkdownText + rightPadding: 40 + } } } footer: Control { - padding: 10 + padding: 20 background: Rectangle { - color: JamiTheme.pluginViewBackgroundColor + color: hovered ? JamiTheme.smartListHoveredColor : JamiTheme.pluginViewBackgroundColor } contentItem: Text { Layout.fillWidth: true @@ -145,32 +226,26 @@ ItemDelegate { font.pointSize: JamiTheme.settingsFontSize font.kerning: true + font.italic: true text: "By " + pluginAuthor + wrapMode: Text.WordWrap 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; + switch (pluginStatus) { + case PluginStatus.DOWNLOADING: + PluginAdapter.cancelDownload(pluginName); + break; + case PluginStatus.INSTALLABLE: + PluginAdapter.installRemotePlugin(pluginName); + break; + case PluginStatus.FAILED: + PluginAdapter.installRemotePlugin(pluginName); + break; + case PluginStatus.INSTALLING: + break; } - 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 e273d8772..139a225cd 100644 --- a/src/app/settingsview/components/PluginItemDelegate.qml +++ b/src/app/settingsview/components/PluginItemDelegate.qml @@ -20,109 +20,133 @@ 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" ItemDelegate { id: root - property string pluginName: "" property string pluginId: "" property string pluginIcon: "" + property int pluginStatus property bool isLoaded: false - property string activeId: "" - height: pluginPreferencesView.visible ? implicitHeight + pluginPreferencesView.childrenRect.height : implicitHeight + height: implicitHeight + Connections { + target: PluginListModel + function onDisabled(id) { + if (root.pluginId === id) { + isLoaded = false; + loadSwitch.checked = false; + } + } + } - signal settingsClicked + onClicked: { + pluginListView.currentIndex = index; + } - onActiveIdChanged: pluginPreferencesView.visible = activeId != pluginId ? false : !pluginPreferencesView.visible + Rectangle { + id: mask + anchors.fill: parent + color: { + if (pluginHover.hovered && pluginListView.currentIndex !== index) { + return JamiTheme.smartListHoveredColor; + } else { + return JamiTheme.pluginViewBackgroundColor; + } + } + border.width: 2 + border.color: { + if (pluginListView.currentIndex === index) { + return JamiTheme.switchHandleCheckedBorderColor; + } + return "transparent"; + } + radius: 5 + } ColumnLayout { - width: parent.width - - RowLayout { + width: parent.width - 20 + height: parent.height + anchors.centerIn: parent + Item { Layout.fillWidth: true - Layout.preferredHeight: implicitHeight + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight: childrenRect.height Label { id: pluginImage - Layout.leftMargin: 8 - Layout.topMargin: 8 - Layout.alignment: Qt.AlignLeft | Qt.AlingVCenter + anchors.left: parent.left width: JamiTheme.preferredFieldHeight - Layout.fillHeight: true + height: parent.height background: Rectangle { color: "transparent" - Image { + ResponsiveImage { anchors.centerIn: parent source: "file:" + pluginIcon - sourceSize: Qt.size(256, 256) - mipmap: true - width: JamiTheme.preferredFieldHeight - height: JamiTheme.preferredFieldHeight + containerWidth: JamiTheme.preferredFieldHeight + containerHeight: JamiTheme.preferredFieldHeight } } } Label { - Layout.fillHeight: true - Layout.fillWidth: true - Layout.topMargin: 8 - Layout.leftMargin: 8 + width: contentWidth + height: parent.height + anchors.left: pluginImage.right + anchors.leftMargin: 8 color: JamiTheme.textColor - font.pointSize: JamiTheme.settingsFontSize + font.pointSize: JamiTheme.tinyCreditsTextSize font.kerning: true text: pluginName === "" ? pluginId : pluginName verticalAlignment: Text.AlignVCenter } - ToggleSwitch { - id: loadSwitch - Layout.fillHeight: true - property bool isHovering: false - Layout.topMargin: 8 - Layout.rightMargin: 8 - width: 20 - - tooltipText: JamiStrings.loadUnload - - checked: isLoaded - onSwitchToggled: { - if (isLoaded) - PluginModel.unloadPlugin(pluginId); - else - PluginModel.loadPlugin(pluginId); - installedPluginsModel.pluginChanged(index); + MaterialButton { + id: update + anchors.right: itemSwitch.left + buttontextHeightMargin: 10.0 + TextMetrics { + id: updateTextSize + font.weight: Font.Bold + font.pixelSize: JamiTheme.wizardViewButtonFontPixelSize + font.capitalization: Font.AllUppercase + text: JamiStrings.updateDialogTitle } + visible: pluginStatus === PluginStatus.UPDATABLE + secondary: true + preferredWidth: updateTextSize.width + text: JamiStrings.updateDialogTitle + fontSize: 15 } - - PushButton { - id: btnPreferencesPlugin - - Layout.alignment: Qt.AlingVCenter | Qt.AlignRight - Layout.topMargin: 8 - Layout.rightMargin: 8 - - source: JamiResources.round_settings_24dp_svg - normalColor: JamiTheme.primaryBackgroundColor - imageColor: JamiTheme.textColor - toolTipText: JamiStrings.showHidePrefs - - onClicked: settingsClicked() + Item { + id: itemSwitch + height: parent.height + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + width: 78 + ToggleSwitch { + id: loadSwitch + anchors.topMargin: parent.height / 2 + width: parent.width + height: parent.height + property bool isHovering: false + + tooltipText: JamiStrings.loadUnload + + checked: isLoaded + onSwitchToggled: { + if (isLoaded) + PluginModel.unloadPlugin(pluginId); + else + PluginModel.loadPlugin(pluginId); + PluginListModel.pluginChanged(index); + } + } } } - - PluginPreferencesView { - id: pluginPreferencesView - - pluginId: root.pluginId - - Layout.fillWidth: true - Layout.leftMargin: JamiTheme.preferredMarginSize - Layout.rightMargin: JamiTheme.preferredMarginSize - Layout.preferredHeight: pluginPreferencesView.childrenRect.height - } } } diff --git a/src/app/settingsview/components/PluginListView.qml b/src/app/settingsview/components/PluginListView.qml index 1afa2d4cf..081865430 100644 --- a/src/app/settingsview/components/PluginListView.qml +++ b/src/app/settingsview/components/PluginListView.qml @@ -26,114 +26,105 @@ import "../../commoncomponents" Rectangle { id: root - - property string activePlugin: "" - + property int count: pluginLoader.item !== undefined ? pluginLoader.item.count : 0 + property bool isAutoUpdate: PluginAdapter.isAutoUpdaterEnabled() + property int currentIndex: { + if (pluginLoader.item !== undefined) { + return -1; + } else { + if (pluginListView.currentIndex === null) { + return -1; + } + return pluginListView.currentIndex; + } + } + visible: PluginAdapter.isEnabled && count color: JamiTheme.secondaryBackgroundColor ColumnLayout { anchors.left: root.left anchors.right: root.right anchors.bottomMargin: 20 - - Label { + RowLayout { + Layout.preferredHeight: JamiTheme.settingsHeaderpreferredHeight Layout.fillWidth: true - Layout.preferredHeight: 25 - - text: JamiStrings.installedPlugins - font.pointSize: JamiTheme.headerFontSize - font.kerning: true - color: JamiTheme.textColor - - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - } - - MaterialButton { - id: disableAll - TextMetrics { - id: disableTextSize - font.weight: Font.Bold - font.pixelSize: JamiTheme.wizardViewButtonFontPixelSize - font.capitalization: Font.AllUppercase - text: JamiStrings.disableAll + Layout.bottomMargin: 20 + Layout.alignment: Qt.AlignRight + Label { + Layout.fillWidth: true + text: JamiStrings.installed + font.pixelSize: JamiTheme.settingsTitlePixelSize + font.kerning: true + color: JamiTheme.textColor + + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter } - secondary: true - preferredWidth: disableTextSize.width - text: JamiStrings.disableAll - fontSize: 15 - } - - MaterialButton { - id: installButton - - Layout.alignment: Qt.AlignCenter - Layout.topMargin: JamiTheme.preferredMarginSize / 2 - - preferredWidth: JamiTheme.preferredFieldWidth - buttontextHeightMargin: JamiTheme.buttontextHeightMargin - - color: JamiTheme.buttonTintedBlack - hoveredColor: JamiTheme.buttonTintedBlackHovered - pressedColor: JamiTheme.buttonTintedBlackPressed - secondary: true - toolTipText: JamiStrings.addNewPlugin - - iconSource: JamiResources.round_add_24dp_svg - - text: JamiStrings.installPlugin - - onClicked: { - 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); - installedPluginsModel.addPlugin(); - }); + HeaderToggleSwitch { + labelText: "auto update" + tooltipText: "auto update" + checked: isAutoUpdate + onSwitchToggled: { + isAutoUpdate = !isAutoUpdate; + PluginAdapter.setAutoUpdate(isAutoUpdate); + } + } + MaterialButton { + id: disableAll + radius: JamiTheme.chatViewHeaderButtonRadius + buttontextHeightMargin: 10.0 + TextMetrics { + id: disableTextSize + font.weight: Font.Bold + font.pixelSize: JamiTheme.wizardViewButtonFontPixelSize + font.capitalization: Font.AllUppercase + text: JamiStrings.disableAll + } + secondary: true + preferredWidth: disableTextSize.width + 2 + text: JamiStrings.disableAll + fontSize: JamiTheme.wizardViewButtonFontPixelSize + onClicked: PluginListModel.disableAllPlugins() } } - - ListView { - id: pluginList - + Loader { + id: pluginLoader Layout.fillWidth: true - Layout.bottomMargin: 10 - Layout.preferredHeight: childrenRect.height - clip: true - - model: PluginListModel { - id: installedPluginsModel - - lrcInstance: LRCInstance - onLrcInstanceChanged: { - this.reset(); + Layout.preferredHeight: pluginLoader.item.contentHeight + Layout.topMargin: 10 + active: true + asynchronous: true + + sourceComponent: ListView { + id: pluginListView + clip: true + model: PluginListModel + spacing: 10 + currentIndex: -1 + onCurrentIndexChanged: { + root.currentIndex = currentIndex; } - } - - delegate: PluginItemDelegate { - id: pluginItemDelegate - - width: pluginList.width - implicitHeight: 50 - - pluginName: PluginName - pluginId: PluginId - pluginIcon: PluginIcon - isLoaded: IsLoaded - activeId: root.activePlugin - - background: Rectangle { - anchors.fill: parent - color: "transparent" + delegate: PluginItemDelegate { + id: pluginItemDelegate + width: pluginLoader.width + implicitHeight: 50 + + pluginName: PluginName + pluginId: PluginId + pluginIcon: PluginIcon + pluginStatus: Status + isLoaded: IsLoaded + HoverHandler { + id: pluginHover + target: parent + enabled: true + } } - - onSettingsClicked: { - root.activePlugin = root.activePlugin === pluginId ? "" : pluginId; + Connections { + target: pluginPreferencesView + function onClosed() { + pluginListView.currentIndex = -1; + } } } } diff --git a/src/app/settingsview/components/PluginPreferencesListView.qml b/src/app/settingsview/components/PluginPreferencesListView.qml index ec5aaf823..456a08895 100644 --- a/src/app/settingsview/components/PluginPreferencesListView.qml +++ b/src/app/settingsview/components/PluginPreferencesListView.qml @@ -25,9 +25,9 @@ import "../../commoncomponents" Rectangle { id: root - property string accountId: "" required property string pluginId + width: parent.width property int count: pluginPreferenceView.count + pluginPreferenceViewCategory.count @@ -40,7 +40,6 @@ Rectangle { } color: "transparent" - Connections { target: LRCInstance @@ -56,8 +55,6 @@ Rectangle { property var categories: PluginAdapter.getPluginPreferencesCategories(pluginId, accountId) property string generalCategory: categories.length <= 1 ? "all" : "" - visible: false - function setPreference(pluginId, preferenceKey, preferenceNewValue) { PluginModel.setPluginPreference(pluginId, accountId, preferenceKey, preferenceNewValue); } @@ -66,7 +63,7 @@ Rectangle { anchors.left: root.left anchors.right: root.right - Rectangle { + Item { id: prefsByCategory visible: categories.length > 1 @@ -74,7 +71,6 @@ Rectangle { Layout.topMargin: 24 Layout.fillWidth: true implicitHeight: childrenRect.height - color: JamiTheme.backgroundColor ColumnLayout { anchors.left: parent.left @@ -263,7 +259,7 @@ Rectangle { MaterialButton { id: resetButton - + visible: count > 0 Layout.alignment: Qt.AlignCenter preferredWidth: JamiTheme.preferredFieldWidth @@ -274,8 +270,6 @@ Rectangle { pressedColor: JamiTheme.buttonTintedBlackPressed secondary: true - iconSource: JamiResources.settings_backup_restore_24dp_svg - text: JamiStrings.reset onClicked: viewCoordinator.presentDialog(appWindow, "commoncomponents/SimpleMessageDialog.qml", { diff --git a/src/app/settingsview/components/PluginPreferencesView.qml b/src/app/settingsview/components/PluginPreferencesView.qml index f8c12e321..82bf989c5 100644 --- a/src/app/settingsview/components/PluginPreferencesView.qml +++ b/src/app/settingsview/components/PluginPreferencesView.qml @@ -19,179 +19,181 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import net.jami.Adapters 1.1 +import SortFilterProxyModel 0.2 import net.jami.Models 1.1 import net.jami.Constants 1.1 import "../../commoncomponents" -Rectangle { +Item { id: root - - required property string pluginId - - color: "transparent" - - visible: false - - ColumnLayout { - anchors.left: root.left - anchors.right: root.right - anchors.bottomMargin: 10 - - Label { - Layout.topMargin: 34 - Layout.alignment: Qt.AlignHCenter - height: 64 - background: Rectangle { - Image { - anchors.centerIn: parent - source: pluginIcon === "" ? JamiResources.plugins_24dp_svg : "file:" + pluginIcon - sourceSize: Qt.size(256, 256) - height: 64 - width: 64 - mipmap: true + required property int currentIndex + signal closed + ListView { + id: pluginPreferenceListView + height: parent.height + width: parent.width + model: SortFilterProxyModel { + sourceModel: PluginListModel + filters: [ + ExpressionFilter { + expression: index === currentIndex + enabled: true } - } - } - - Label { - Layout.alignment: Qt.AlignHCenter - Layout.topMargin: 24 - height: JamiTheme.preferredFieldHeight - - text: "%1\n%2".arg(pluginName).arg(JamiStrings.pluginPreferences) - font.pointSize: JamiTheme.headerFontSize - font.kerning: true - color: JamiTheme.textColor - - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter + ] } - - RowLayout { - Layout.fillWidth: true - - Text { - Layout.fillWidth: true - Layout.preferredHeight: JamiTheme.preferredFieldHeight - - font.pointSize: JamiTheme.headerFontSize - font.kerning: true - - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - color: JamiTheme.textColor - - text: JamiStrings.generalSettingsTitle - elide: Text.ElideRight + delegate: Page { + id: settings + width: root.width + height: root.height + background: Rectangle { + color: JamiTheme.pluginViewBackgroundColor } - - PushButton { - Layout.preferredWidth: JamiTheme.preferredFieldHeight - Layout.preferredHeight: JamiTheme.preferredFieldHeight - Layout.alignment: Qt.AlignHCenter - - imageColor: JamiTheme.textColor - toolTipText: JamiStrings.tipGeneralPluginSettingsDisplay - - preferredSize: 32 - source: pluginGeneralSettingsView.visible ? JamiResources.expand_less_24dp_svg : JamiResources.expand_more_24dp_svg - - onClicked: { - pluginGeneralSettingsView.visible = !pluginGeneralSettingsView.visible; + header: Control { + padding: 10 + background: Rectangle { + color: JamiTheme.pluginViewBackgroundColor + } + contentItem: ColumnLayout { + width: parent.width + PushButton { + id: closeButton + normalColor: "transparent" + hoveredColor: JamiTheme.smartListHoveredColor + Layout.alignment: Qt.AlignRight + Layout.preferredWidth: JamiTheme.preferredFieldHeight + Layout.preferredHeight: childrenRect.height + + imageColor: JamiTheme.textColor + toolTipText: JamiStrings.closeSettings + + preferredSize: 32 + source: JamiResources.round_close_24dp_svg + onClicked: { + closed(); + } + } + + RowLayout { + Layout.preferredWidth: parent.width + ResponsiveImage { + Layout.bottomMargin: 10 + Layout.rightMargin: 10 + containerWidth: 64 + containerHeight: 64 + source: PluginIcon === "" ? JamiResources.plugins_default_icon_svg : "file:" + PluginIcon + } + Label { + text: PluginName + font.pixelSize: JamiTheme.settingsTitlePixelSize + font.kerning: true + color: JamiTheme.textColor + textFormat: Text.PlainText + } + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + MaterialButton { + id: update + anchors.right: parent.right + buttontextHeightMargin: 0.0 + TextMetrics { + id: updateTextSize + font.weight: Font.Bold + font.pixelSize: JamiTheme.wizardViewButtonFontPixelSize + font.capitalization: Font.AllUppercase + text: JamiStrings.updateDialogTitle + } + visible: Status === PluginStatus.UPDATABLE + secondary: true + preferredWidth: updateTextSize.width + text: JamiStrings.updateDialogTitle + fontSize: 15 + } + } + } + + JamiFlickable { + Layout.fillWidth: true + Layout.preferredHeight: childrenRect.height + Layout.minimumHeight: childrenRect.height + Layout.maximumHeight: 88 + contentWidth: description.width + contentHeight: description.height + clip: true + flickableDirection: Flickable.VerticalFlick + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical: ScrollBar { + id: scrollBar + policy: ScrollBar.AsNeeded + } + Text { + id: description + width: settings.width - 2 * scrollBar.width + text: PluginDescription + font.pixelSize: JamiTheme.popuptextSize + color: JamiTheme.textColor + wrapMode: Text.WordWrap + textFormat: Text.PlainText + } + } } } - } - - PluginPreferencesListView { - id: pluginGeneralSettingsView - visible: false - Layout.fillWidth: true - pluginId: root.pluginId - } - - RowLayout { - Layout.fillWidth: true - visible: pluginAccountSettingsView.count > 0 - - Text { - Layout.fillWidth: true - Layout.preferredHeight: JamiTheme.preferredFieldHeight - - font.pointSize: JamiTheme.headerFontSize - font.kerning: true - - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - color: JamiTheme.textColor - - text: JamiStrings.accountSettingsMenuTitle - elide: Text.ElideRight + Rectangle { + anchors.fill: parent + color: JamiTheme.primaryBackgroundColor } - - PushButton { - Layout.preferredWidth: JamiTheme.preferredFieldHeight - Layout.preferredHeight: JamiTheme.preferredFieldHeight - Layout.alignment: Qt.AlignHCenter - - imageColor: JamiTheme.textColor - toolTipText: JamiStrings.tipAccountPluginSettingsDisplay - - preferredSize: 32 - source: pluginAccountSettingsView.visible ? JamiResources.expand_less_24dp_svg : JamiResources.expand_more_24dp_svg - - onClicked: { - pluginAccountSettingsView.visible = !pluginAccountSettingsView.visible; + JamiFlickable { + anchors.fill: parent + contentHeight: contentItem.childrenRect.height + topMargin: JamiTheme.preferredSettingsBottomMarginSize + bottomMargin: JamiTheme.preferredSettingsBottomMarginSize + ScrollBar.horizontal.visible: false + contentItem.children: ColumnLayout { + width: root.width + PluginPreferencesListView { + id: pluginGeneralSettingsView + Layout.fillWidth: true + pluginId: PluginId + } + PluginPreferencesListView { + id: pluginAccountSettingsView + Layout.fillWidth: true + accountId: LRCInstance.currentAccountId + pluginId: PluginId + } + MaterialButton { + id: uninstallButton + + Layout.alignment: Qt.AlignCenter + + preferredWidth: JamiTheme.preferredFieldWidth + buttontextHeightMargin: JamiTheme.buttontextHeightMargin + contentColorProvider: JamiTheme.buttonTintedRed + color: JamiTheme.buttonTintedBlack + hoveredColor: JamiTheme.buttonTintedBlackHovered + pressedColor: JamiTheme.buttonTintedBlackPressed + tertiary: true + toolTipText: JamiStrings.pluginUninstallConfirmation.arg(PluginId) + + text: JamiStrings.uninstall + + onClicked: viewCoordinator.presentDialog(appWindow, "commoncomponents/SimpleMessageDialog.qml", { + "title": JamiStrings.uninstallPlugin, + "infoText": JamiStrings.pluginUninstallConfirmation.arg(PluginName), + "buttonTitles": [JamiStrings.optionOk, JamiStrings.optionCancel], + "buttonStyles": [SimpleMessageDialog.ButtonStyle.TintedBlue, SimpleMessageDialog.ButtonStyle.TintedBlack], + "buttonCallBacks": [function () { + PluginListModel.setVersionStatus(PluginName, PluginStatus.INSTALLABLE); + PluginModel.uninstallPlugin(PluginId); + PluginListModel.removePlugin(index); + // could not call root from here + settings.ListView.view.parent.closed(); + }] + }) + } } } } - - PluginPreferencesListView { - id: pluginAccountSettingsView - visible: false - Layout.fillWidth: true - accountId: LRCInstance.currentAccountId - pluginId: root.pluginId - } - - MaterialButton { - id: uninstallButton - - Layout.alignment: Qt.AlignCenter - - preferredWidth: JamiTheme.preferredFieldWidth - buttontextHeightMargin: JamiTheme.buttontextHeightMargin - - color: JamiTheme.buttonTintedBlack - hoveredColor: JamiTheme.buttonTintedBlackHovered - pressedColor: JamiTheme.buttonTintedBlackPressed - secondary: true - toolTipText: JamiStrings.pluginUninstallConfirmation.arg(pluginName) - iconSource: JamiResources.delete_24dp_svg - - text: JamiStrings.uninstall - - onClicked: viewCoordinator.presentDialog(appWindow, "commoncomponents/SimpleMessageDialog.qml", { - "title": JamiStrings.uninstallPlugin, - "infoText": JamiStrings.pluginUninstallConfirmation.arg(pluginName), - "buttonTitles": [JamiStrings.optionOk, JamiStrings.optionCancel], - "buttonStyles": [SimpleMessageDialog.ButtonStyle.TintedBlue, SimpleMessageDialog.ButtonStyle.TintedBlack], - "buttonCallBacks": [function () { - pluginPreferencesView.visible = false; - PluginModel.uninstallPlugin(pluginId); - PluginListModel.removePlugin(index); - var pluginPath = pluginId.split('/'); - PluginListModel.setVersionStatus(pluginPath[pluginPath.length - 1], PluginStatus.INSTALLABLE); - }] - }) - } - - Rectangle { - Layout.bottomMargin: 10 - height: 2 - Layout.fillWidth: true - color: "transparent" - border.width: 1 - border.color: JamiTheme.separationLine - } } } diff --git a/src/app/settingsview/components/PluginSettingsPage.qml b/src/app/settingsview/components/PluginSettingsPage.qml index 6a3a28c67..e9afdc7d1 100644 --- a/src/app/settingsview/components/PluginSettingsPage.qml +++ b/src/app/settingsview/components/PluginSettingsPage.qml @@ -25,29 +25,84 @@ import "../../commoncomponents" SettingsPageBase { id: root - contentFlickableWidth: Math.min(root.width, root.width - 2 * JamiTheme.preferredSettingsMarginSize) title: JamiStrings.pluginSettingsTitle - - flickableContent: ColumnLayout { - id: pluginSettingsColumnLayout - - width: contentFlickableWidth - spacing: JamiTheme.settingsBlockSpacing + onWidthChanged: resolvePanes() + flickableContent: RowLayout { + width: parent.width anchors.left: parent.left anchors.leftMargin: JamiTheme.preferredSettingsMarginSize - ColumnLayout { id: generalSettings - Layout.preferredWidth: root.width - spacing: JamiTheme.settingsCategorySpacing - } - PluginListView { - id: pluginListView - - Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.maximumWidth: 3 * (JamiTheme.remotePluginWidthDelegate + 20) Layout.preferredWidth: parent.width - Layout.minimumHeight: 0 - Layout.preferredHeight: childrenRect.height + Layout.rightMargin: 80 + spacing: JamiTheme.settingsBlockSpacing + // View of installed plugins + PluginListView { + id: pluginList + Layout.fillWidth: true + Layout.rightMargin: 20 + Layout.preferredHeight: childrenRect.height + Connections { + target: pluginPreferencesView + function onClosed() { + pluginList.currentIndex = -1; + } + } + } + // View of available plugins in the store + PluginStoreListView { + Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter + Layout.fillWidth: true + } + InstallManuallyView { + Layout.fillWidth: true + spacing: 10 + } } } + property real previousDetailsWidth: 500 + property real previousWidth: 500 + // This function governs the visibility of the plugin content and tracks the + // the width of the SplitView and the details panel. This function should be + // called when the width of the SplitView changes, when the SplitView is shown, + // and when the details panel is shown. When called with force=true, it is being + // called from a visibleChanged event, and we should not update the previous widths. + function resolvePanes(force = false) { + // If the details panel is not visible, then show the generalSettings. + if (!pluginPreferencesView.visible) { + pageContainer.visible = true; + return; + } + // Next we compute whether the SplitView is expanding or shrinking. + const isExpanding = width > previousWidth; + // width has a first bad state + const preferencePreferredWidth = pluginPreferencesView.width === 0 ? 500 : pluginPreferencesView.width; + // If the SplitView is not wide enough to show both the generalSettings + // and the details panel, then hide the generalSettings. + if (width < 522 + preferencePreferredWidth && (!isExpanding || force) && pageContainer.visible) { + if (!force) + previousDetailsWidth = pluginPreferencesView.width; + pageContainer.visible = false; + } else if (width >= JamiTheme.mainViewPaneMinWidth + previousDetailsWidth && (isExpanding || force) && !pageContainer.visible) { + pageContainer.visible = true; + } + if (!force) + previousWidth = width; + } + + onResizingChanged: if (pageContainer.visible) + pluginPreferencesView.previousWidth = pluginPreferencesView.width + + PluginPreferencesView { + id: pluginPreferencesView + SplitView.maximumWidth: root.width + SplitView.minimumWidth: 500 + SplitView.preferredWidth: 500 + SplitView.fillHeight: true + property int previousWidth: 500 + currentIndex: pluginList.currentIndex + visible: pluginList.currentIndex != -1 + onVisibleChanged: root.resolvePanes(true) + } } diff --git a/src/app/settingsview/components/PluginStoreListView.qml b/src/app/settingsview/components/PluginStoreListView.qml index a47adc8de..2a32087fc 100644 --- a/src/app/settingsview/components/PluginStoreListView.qml +++ b/src/app/settingsview/components/PluginStoreListView.qml @@ -25,80 +25,63 @@ 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(); - }); + property bool storeAvailable: true + Component.onCompleted: { + PluginAdapter.getPluginsFromStore(); } - 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 + Connections { + target: PluginAdapter + function onStoreNotAvailable() { + storeAvailable = false; } - RowLayout { - Layout.alignment: Qt.AlignRight - MaterialButton { - id: installManually + } + Label { + Layout.fillWidth: true + Layout.bottomMargin: 20 + text: JamiStrings.pluginStoreTitle + font.pixelSize: JamiTheme.settingsTitlePixelSize + font.kerning: true + color: JamiTheme.textColor + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + } + Loader { + active: storeAvailable + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + Layout.preferredHeight: active ? item.height : 0 + sourceComponent: Flow { + id: pluginStoreList + height: childrenRect.height + spacing: 20 + Repeater { + model: PluginStoreListModel - TextMetrics { - id: installManuallyTextSize - font.weight: Font.Black - font.pixelSize: JamiTheme.wizardViewButtonFontPixelSize - font.capitalization: Font.Capitalize - text: JamiStrings.installManually + delegate: PluginAvailableDelagate { + id: pluginItemDelegate + width: JamiTheme.remotePluginWidthDelegate + height: JamiTheme.remotePluginHeightDelegate + pluginName: Name + pluginIcon: IconPath + pluginDescription: Description + pluginAuthor: Author + pluginShortDescription: "" + pluginStatus: Status } - secondary: true - preferredWidth: installManuallyTextSize.width - text: JamiStrings.installManually - toolTipText: JamiStrings.installManually - fontSize: 15 - onClicked: installPlugin() } } } - - Flow { - id: pluginStoreList - + Loader { 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 - } + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop + Layout.preferredHeight: active ? JamiTheme.bigFontSize : 0 + active: !storeAvailable + sourceComponent: Text { + font.bold: true + color: JamiTheme.textColor + font.pixelSize: JamiTheme.bigFontSize + horizontalAlignment: Text.AlignHCenter + text: JamiStrings.pluginStoreNotAvailable } } } diff --git a/src/app/settingsview/components/SettingsPageBase.qml b/src/app/settingsview/components/SettingsPageBase.qml index e28a0cd30..69cab4ac7 100644 --- a/src/app/settingsview/components/SettingsPageBase.qml +++ b/src/app/settingsview/components/SettingsPageBase.qml @@ -23,41 +23,44 @@ import net.jami.Enums 1.1 import net.jami.Models 1.1 import "../../commoncomponents" -Page { +JamiSplitView { id: root - required property Item flickableContent - - property real contentFlickableWidth: Math.min(JamiTheme.maximumWidthSettingsView, root.width - 2 * JamiTheme.preferredSettingsMarginSize) - + property real contentFlickableWidth: Math.min(JamiTheme.maximumWidthSettingsView, settingsPage.width - 2 * JamiTheme.preferredSettingsMarginSize) + property alias title: settingsPage.title property color backgroundColor: JamiTheme.secondaryBackgroundColor + property alias pageContainer: settingsPage + Page { + id: settingsPage + SplitView.maximumWidth: root.width + SplitView.fillWidth: true + SplitView.minimumWidth: 500 + Rectangle { + width: parent.width + height: parent.height + color: backgroundColor + } + header: Rectangle { + height: JamiTheme.settingsHeaderpreferredHeight + width: root.preferredWidth + color: backgroundColor - Rectangle { - width: parent.width - height: parent.height - color: backgroundColor - } - - header: Rectangle { - height: JamiTheme.settingsHeaderpreferredHeight - width: root.preferredWidth - color: backgroundColor + SettingsHeader { + id: settingsHeader + title: root.title + anchors.fill: parent + onBackArrowClicked: viewNode.dismiss() + } + } - SettingsHeader { - id: settingsHeader - title: root.title + JamiFlickable { + id: flickable anchors.fill: parent - onBackArrowClicked: viewNode.dismiss() + contentHeight: contentItem.childrenRect.height + contentItem.children: [flickableContent] + topMargin: JamiTheme.preferredSettingsBottomMarginSize + bottomMargin: JamiTheme.preferredSettingsBottomMarginSize + ScrollBar.horizontal.visible: false } } - - JamiFlickable { - id: flickable - anchors.fill: parent - contentHeight: contentItem.childrenRect.height - contentItem.children: [flickableContent] - topMargin: JamiTheme.preferredSettingsBottomMarginSize - bottomMargin: JamiTheme.preferredSettingsBottomMarginSize - ScrollBar.horizontal.visible: false - } } diff --git a/src/app/utils.cpp b/src/app/utils.cpp index 1dfd8cf7e..5d664a85e 100644 --- a/src/app/utils.cpp +++ b/src/app/utils.cpp @@ -933,3 +933,9 @@ Utils::generateUid() { return QUuid::createUuid().toString(QUuid::Id128); } + +QString +Utils::getPlatformString() +{ + return "desktop"; +} \ No newline at end of file diff --git a/src/app/utils.h b/src/app/utils.h index 0084a9b7d..6f7bed819 100644 --- a/src/app/utils.h +++ b/src/app/utils.h @@ -127,4 +127,5 @@ QString generateUid(); QString humanFileSize(qint64 fileSize); QString getDebugFilePath(); +QString getPlatformString(); } // namespace Utils diff --git a/src/libclient/api/pluginmodel.h b/src/libclient/api/pluginmodel.h index 65fe7e375..ad2234fef 100644 --- a/src/libclient/api/pluginmodel.h +++ b/src/libclient/api/pluginmodel.h @@ -40,6 +40,7 @@ struct PluginDetails { QString id = ""; QString name = ""; + QString description = ""; QString path = ""; QString version = ""; QString iconPath = ""; @@ -201,7 +202,6 @@ public: * @return true if preference was succesfully reset */ Q_INVOKABLE bool resetPluginPreferencesValues(const QString& path, const QString& accountId); - Q_SIGNALS: void chatHandlerStatusUpdated(bool isVisible); void modelUpdated(); diff --git a/src/libclient/pluginmodel.cpp b/src/libclient/pluginmodel.cpp index 3b22a64ae..7c3232a0e 100644 --- a/src/libclient/pluginmodel.cpp +++ b/src/libclient/pluginmodel.cpp @@ -50,6 +50,15 @@ namespace lrc { using namespace api; +enum pluginInstallResult { + SUCCESS = 0, + PLUGIN_ALREADY_INSTALLED = 100, /* Plugin already installed with the same version */ + PLUGIN_OLD_VERSION = 200, /* Plugin already installed with a newer version */ + SIGNATURE_VERIFICATION_FAILED = 300, /* Signature verification failed */ + CERTIFICATE_VERIFICATION_FAILED = 400, + INVALID_PLUGIN = 500, +}; + PluginModel::PluginModel() : QObject() { @@ -99,6 +108,7 @@ PluginModel::getPluginDetails(const QString& path) if (!details.empty()) { result.id = details["id"]; result.name = details["name"]; + result.description = details["description"]; result.path = path; result.iconPath = details["iconPath"]; result.version = details["version"]; @@ -311,5 +321,4 @@ PluginModel::resetPluginPreferencesValues(const QString& path, const QString& ac Q_EMIT modelUpdated(); return result; } - } // namespace lrc -- GitLab