1
0
Fork 0
mirror of https://git.jami.net/savoirfairelinux/jami-client-qt.git synced 2025-04-21 21:52:03 +02:00

updateManager: create a plugin store and a plugin manager

Change-Id: I57ebec72c1cb6e2f245af011def82f880bc9573f
This commit is contained in:
Xavier Jouslin de Noray 2023-06-28 17:48:55 -04:00 committed by Sébastien Blin
parent 7f2c98a594
commit 7581f9397a
25 changed files with 1148 additions and 38 deletions

View file

@ -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.

View file

@ -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) \

View file

@ -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()

View file

@ -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")

View file

@ -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()
{

View file

@ -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_;

View file

@ -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);

View file

@ -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:

View file

@ -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_;
}

View file

@ -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_;
};

View file

@ -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;
}

View file

@ -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_ {};

View file

@ -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;
}
}

View file

@ -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_;
};

View file

@ -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);
}

View file

@ -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*)

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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

View file

@ -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

View file

@ -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);
}]
})
}

View file

@ -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

View file

@ -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
}
}
}
}

View file

@ -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

View file

@ -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)
{