From ec0feef74d1da0de950321cc281c2fb092a5461e Mon Sep 17 00:00:00 2001 From: Matheo Joseph <matheo.joseph@savoirfairelinux.com> Date: Mon, 26 Jun 2023 17:12:01 -0400 Subject: [PATCH] CachedImage: add icon downloader for pluginstore and welcome page Gitlab: #1228 Gitlab: #1166 Change-Id: I0117cecdb8a77ded8f3da3c2f25028012002a285 --- CMakeLists.txt | 7 +- src/app/MainApplicationWindow.qml | 2 +- src/app/imagedownloader.cpp | 66 ++++++++++ src/app/imagedownloader.h | 42 +++++++ src/app/mainapplication.cpp | 4 +- src/app/mainapplication.h | 4 +- src/app/mainview/components/CachedImage.qml | 132 ++++++++++++++++++++ src/app/qmlregister.cpp | 9 +- src/app/qmlregister.h | 3 +- src/app/utilsadapter.cpp | 67 +++++++++- src/app/utilsadapter.h | 9 ++ src/app/webengine/GeneralWebEngineView.qml | 4 +- tests/qml/main.cpp | 2 +- tests/qml/src/tst_CachedImage.qml | 86 +++++++++++++ 14 files changed, 421 insertions(+), 16 deletions(-) create mode 100644 src/app/imagedownloader.cpp create mode 100644 src/app/imagedownloader.h create mode 100644 src/app/mainview/components/CachedImage.qml create mode 100644 tests/qml/src/tst_CachedImage.qml diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f06f2cbf..4f936b48d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -238,7 +238,8 @@ set(COMMON_SOURCES ${APP_SRC_DIR}/positioning.cpp ${APP_SRC_DIR}/currentcall.cpp ${APP_SRC_DIR}/messageparser.cpp - ${APP_SRC_DIR}/previewengine.cpp) + ${APP_SRC_DIR}/previewengine.cpp + ${APP_SRC_DIR}/imagedownloader.cpp) set(COMMON_HEADERS ${APP_SRC_DIR}/avatarimageprovider.h @@ -301,7 +302,9 @@ set(COMMON_HEADERS ${APP_SRC_DIR}/positioning.h ${APP_SRC_DIR}/currentcall.h ${APP_SRC_DIR}/messageparser.h - ${APP_SRC_DIR}/htmlparser.h) + ${APP_SRC_DIR}/htmlparser.h + ${APP_SRC_DIR}/imagedownloader.h) + # For libavutil/avframe. set(LIBJAMI_CONTRIB_DIR "${DAEMON_DIR}/contrib") diff --git a/src/app/MainApplicationWindow.qml b/src/app/MainApplicationWindow.qml index 5da6ea9cb..6410f6a54 100644 --- a/src/app/MainApplicationWindow.qml +++ b/src/app/MainApplicationWindow.qml @@ -270,7 +270,7 @@ ApplicationWindow { Connections { target: UpdateManager - function onUpdateDownloadStarted() { + function onDownloadStarted() { viewCoordinator.presentDialog(appWindow, "settingsview/components/UpdateDownloadDialog.qml", { "title": JamiStrings.updateDialogTitle }); diff --git a/src/app/imagedownloader.cpp b/src/app/imagedownloader.cpp new file mode 100644 index 000000000..945a4e607 --- /dev/null +++ b/src/app/imagedownloader.cpp @@ -0,0 +1,66 @@ +/* + * 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 <https://www.gnu.org/licenses/>. + */ + +#include "imagedownloader.h" +#include <QDir> +#include <QLockFile> + +ImageDownloader::ImageDownloader(ConnectivityMonitor* cm, QObject* parent) + : NetworkManager(cm, parent) +{} + +void +ImageDownloader::downloadImage(const QUrl& url, const QString& localPath) +{ + Utils::oneShotConnect(this, &NetworkManager::errorOccured, this, [this, localPath]() { + onDownloadImageFinished({}, localPath); + }); + + sendGetRequest(url, [this, localPath](const QByteArray& imageData) { + onDownloadImageFinished(imageData, localPath); + }); +} + +void +ImageDownloader::onDownloadImageFinished(const QByteArray& data, const QString& localPath) +{ + if (!data.isEmpty()) { + // Check if the parent folders exist create them if not + QString dirPath = localPath.left(localPath.lastIndexOf('/')); + QDir dir; + dir.mkpath(dirPath); + + QLockFile lf(localPath + ".lock"); + QFile file(localPath); + + if (!lf.lock()) { + qWarning().noquote() << "Can't lock file for writing: " << file.fileName(); + return; + } + if (!file.open(QIODevice::WriteOnly)) { + qWarning().noquote() << "Can't open file for writing: " << file.fileName(); + return; + } + + file.write(data); + file.close(); + qWarning() << Q_FUNC_INFO; + Q_EMIT downloadImageSuccessful(localPath); + return; + } + Q_EMIT downloadImageFailed(localPath); +} diff --git a/src/app/imagedownloader.h b/src/app/imagedownloader.h new file mode 100644 index 000000000..ed8c1b129 --- /dev/null +++ b/src/app/imagedownloader.h @@ -0,0 +1,42 @@ +/* + * 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 <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "networkmanager.h" +#include "qtutils.h" + +class ImageDownloader : public NetworkManager +{ + Q_OBJECT + + QML_PROPERTY(QString, cachePath) + +public: + explicit ImageDownloader(ConnectivityMonitor* cm, QObject* parent = nullptr); + + // Download an image and call onDownloadImageFinished when done + Q_INVOKABLE void downloadImage(const QUrl& url, const QString& localPath); + +Q_SIGNALS: + void downloadImageSuccessful(const QString& localPath); + void downloadImageFailed(const QString& localPath); + +private Q_SLOTS: + // Saves the image to the localPath and emits the appropriate signal + void onDownloadImageFinished(const QByteArray& reply, const QString& localPath); +}; \ No newline at end of file diff --git a/src/app/mainapplication.cpp b/src/app/mainapplication.cpp index 6d2f4e4f1..7a858ca1a 100644 --- a/src/app/mainapplication.cpp +++ b/src/app/mainapplication.cpp @@ -25,7 +25,6 @@ #include "appsettingsmanager.h" #include "connectivitymonitor.h" #include "systemtray.h" -#include "previewengine.h" #include "videoprovider.h" #include <QAction> @@ -133,7 +132,6 @@ MainApplication::init() connectivityMonitor_.reset(new ConnectivityMonitor(this)); settingsManager_.reset(new AppSettingsManager(this)); systemTray_.reset(new SystemTray(settingsManager_.get(), this)); - previewEngine_.reset(new PreviewEngine(connectivityMonitor_.get(), this)); QObject::connect(settingsManager_.get(), &AppSettingsManager::retranslate, @@ -347,7 +345,7 @@ MainApplication::initQmlLayer() systemTray_.get(), lrcInstance_.get(), settingsManager_.get(), - previewEngine_.get(), + connectivityMonitor_.get(), &screenInfo_, this); diff --git a/src/app/mainapplication.h b/src/app/mainapplication.h index fa8c12b91..3e42b7744 100644 --- a/src/app/mainapplication.h +++ b/src/app/mainapplication.h @@ -20,6 +20,7 @@ #pragma once +#include "imagedownloader.h" #include "lrcinstance.h" #include "qtutils.h" @@ -36,7 +37,6 @@ class ConnectivityMonitor; class AppSettingsManager; class SystemTray; class CallAdapter; -class PreviewEngine; // Provides information about the screen the app is displayed on class ScreenInfo : public QObject @@ -118,7 +118,7 @@ private: QScopedPointer<ConnectivityMonitor> connectivityMonitor_; QScopedPointer<AppSettingsManager> settingsManager_; QScopedPointer<SystemTray> systemTray_; - QScopedPointer<PreviewEngine> previewEngine_; + QScopedPointer<ImageDownloader> imageDownloader_; ScreenInfo screenInfo_; }; diff --git a/src/app/mainview/components/CachedImage.qml b/src/app/mainview/components/CachedImage.qml new file mode 100644 index 000000000..d06063964 --- /dev/null +++ b/src/app/mainview/components/CachedImage.qml @@ -0,0 +1,132 @@ +/* + * 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 <https://www.gnu.org/licenses/>. + */ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import net.jami.Adapters 1.1 +import net.jami.Constants 1.1 +import net.jami.Helpers 1.1 +import net.jami.Models 1.1 +import "../../commoncomponents" + +Item { + id: cachedImage + property bool customLogo: false + property alias source: image.source + property string defaultImage: "" + property string downloadUrl: "" + property string fileExtension: downloadUrl.substring(downloadUrl.lastIndexOf("."), downloadUrl.length) + property string localPath: "" + property int imageFillMode: 0 + + Image { + id: image + objectName: "image" + anchors.fill: parent + fillMode: imageFillMode + smooth: true + antialiasing: true + property bool isSvg: getIsSvg(this) + + Image { + id: default_img + objectName: "default_img" + anchors.fill: parent + source: defaultImage + visible: image.status != Image.Ready + smooth: true + antialiasing: true + property bool isSvg: getIsSvg(this) + + Component.onCompleted: setSourceSize(default_img) + } + + Component.onCompleted: setSourceSize(image) + } + + function setSourceSize(img) { + img.sourceSize = undefined; + if (img.isSvg) { + img.sourceSize = Qt.size(cachedImage.width, cachedImage.height); + } + } + + function getIsSvg(img) { + if (img.source && img.source!=""){ + var localPath = img.source.toString() + if (localPath.startsWith("file://")) { + localPath = localPath.substring(7); + } + return UtilsAdapter.getMimeName(localPath).startsWith("image/svg+xml"); + } + return false + + } + + Connections { + target: ImageDownloader + function onDownloadImageSuccessful(localPath) { + if (localPath === cachedImage.localPath) { + image.source = "file://" + localPath; + } + } + function onDownloadImageFailed(localPath) { + print("Failed to download image: " + downloadUrl); + if (localPath === cachedImage.localPath) { + image.source = defaultImage; + } + } + } + + Connections { + target: cachedImage + function onDownloadUrlChanged() { + updateImageSource(downloadUrl, localPath, defaultImage); + setSourceSize(image); + setSourceSize(default_img); + } + } + + Component.onCompleted: { + updateImageSource(downloadUrl, localPath, defaultImage); + setSourceSize(image); + setSourceSize(default_img); + } + + Connections { + target: CurrentScreenInfo + + function onDevicePixelRatioChanged() { + setSourceSize(image); + setSourceSize(default_img); + } + } + + function updateImageSource(downloadUrl, localPath, defaultImage) { + if (downloadUrl === "") { + image.source = defaultImage; + return; + } + if (downloadUrl !== "" && localPath !== "") { + if (!UtilsAdapter.fileExists(localPath)) { + ImageDownloader.downloadImage(downloadUrl, localPath); + } else { + image.source = "file://" + localPath; + } + } + } +} diff --git a/src/app/qmlregister.cpp b/src/app/qmlregister.cpp index 8a9f68fb5..15bc67a9c 100644 --- a/src/app/qmlregister.cpp +++ b/src/app/qmlregister.cpp @@ -26,7 +26,9 @@ #include "messagesadapter.h" #include "positionmanager.h" #include "tipsmodel.h" +#include "connectivitymonitor.h" #include "previewengine.h" +#include "imagedownloader.h" #include "utilsadapter.h" #include "conversationsadapter.h" #include "currentcall.h" @@ -54,7 +56,6 @@ #include "mainapplication.h" #include "namedirectory.h" #include "updatemanager.h" -#include "pluginlistmodel.h" #include "pluginlistpreferencemodel.h" #include "preferenceitemlistmodel.h" #include "wizardviewstepmodel.h" @@ -105,12 +106,14 @@ registerTypes(QQmlEngine* engine, SystemTray* systemTray, LRCInstance* lrcInstance, AppSettingsManager* settingsManager, - PreviewEngine* previewEngine, + ConnectivityMonitor* connectivityMonitor, ScreenInfo* screenInfo, QObject* parent) { // setup the adapters (their lifetimes are that of MainApplication) auto callAdapter = new CallAdapter(systemTray, lrcInstance, parent); + auto previewEngine = new PreviewEngine(connectivityMonitor, parent); + auto imageDownloader = new ImageDownloader(connectivityMonitor, parent); auto messagesAdapter = new MessagesAdapter(settingsManager, previewEngine, lrcInstance, parent); auto positionManager = new PositionManager(settingsManager, systemTray, lrcInstance, parent); auto conversationsAdapter = new ConversationsAdapter(systemTray, lrcInstance, parent); @@ -147,6 +150,7 @@ registerTypes(QQmlEngine* engine, QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, currentAccountToMigrate, "CurrentAccountToMigrate") QML_REGISTERSINGLETONTYPE_POBJECT(NS_HELPERS, avatarRegistry, "AvatarRegistry"); QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, wizardViewStepModel, "WizardViewStepModel") + QML_REGISTERSINGLETONTYPE_POBJECT(NS_HELPERS, imageDownloader, "ImageDownloader") // TODO: remove these QML_REGISTERSINGLETONTYPE_CUSTOM(NS_MODELS, AVModel, &lrcInstance->avModel()) @@ -168,7 +172,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/qmlregister.h b/src/app/qmlregister.h index 4676fd32d..a50cfee79 100644 --- a/src/app/qmlregister.h +++ b/src/app/qmlregister.h @@ -36,6 +36,7 @@ class AppSettingsManager; class PreviewEngine; class ScreenInfo; class MainApplication; +class ConnectivityMonitor; // Hack for QtCreator autocomplete (part 1) // https://bugreports.qt.io/browse/QTCREATORBUG-20569 @@ -65,7 +66,7 @@ void registerTypes(QQmlEngine* engine, SystemTray* systemTray, LRCInstance* lrcInstance, AppSettingsManager* appSettingsManager, - PreviewEngine* previewEngine, + ConnectivityMonitor* connectivityMonitor, ScreenInfo* screenInfo, QObject* parent); } diff --git a/src/app/utilsadapter.cpp b/src/app/utilsadapter.cpp index 9c5bc6296..aee1138a5 100644 --- a/src/app/utilsadapter.cpp +++ b/src/app/utilsadapter.cpp @@ -35,6 +35,8 @@ #include <QClipboard> #include <QFileInfo> #include <QRegExp> +#include <QMimeData> +#include <QMimeDatabase> UtilsAdapter::UtilsAdapter(AppSettingsManager* settingsManager, SystemTray* systemTray, @@ -142,13 +144,21 @@ UtilsAdapter::getStyleSheet(const QString& name, const QString& source) } const QString -UtilsAdapter::getCachePath() +UtilsAdapter::getLocalDataPath() { QDir dataDir(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation)); dataDir.cdUp(); return dataDir.absolutePath() + "/jami"; } +const QString +UtilsAdapter::getCachePath() +{ + QDir dataDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); + dataDir.cdUp(); + return dataDir.absolutePath() + "/jami"; +} + QString UtilsAdapter::getDefaultRecordPath() const { @@ -825,3 +835,58 @@ UtilsAdapter::isSystemTrayIconVisible() return false; return systemTray_->geometry() != QRect(); } + +QString +UtilsAdapter::base64Encode(const QString& input) +{ + QByteArray byteArray = input.toUtf8(); + return byteArray.toBase64(); +} + +bool +UtilsAdapter::fileExists(const QString& filePath) +{ + return QFile::exists(filePath); +} + +QString +UtilsAdapter::getStandardTempLocation() +{ + return QStandardPaths::writableLocation( + static_cast<QStandardPaths::StandardLocation>(QStandardPaths::TempLocation)); +} + +QString +UtilsAdapter::getMimeName(const QString &filePath) const +{ + QMimeDatabase db; + QMimeType mime = db.mimeTypeForFile(filePath); + return mime.name(); +} + +#ifdef ENABLE_TESTS +//Must only be used for testing purposes +QString +UtilsAdapter::createDummyImage() const +{ + // Create an QImage + QImage image(256, 256, QImage::Format_ARGB32); + image.fill(QColor(255, 255, 255, 255)); + + QByteArray ba; + QBuffer bu(&ba); + image.save(&bu, "PNG"); + + // Save the image to a file + QFile file(QDir::tempPath() + "/dummy.png"); + if (file.open(QIODevice::WriteOnly)) { + file.write(ba); + file.close(); + qInfo() << "Dummy image created" << QDir::tempPath() + "/dummy.png"; + return QDir::tempPath() + "/dummy.png"; + } else { + qWarning() << "Could not create dummy image"; + return ""; + } +} +#endif diff --git a/src/app/utilsadapter.h b/src/app/utilsadapter.h index 3325e1f29..8aa5eea98 100644 --- a/src/app/utilsadapter.h +++ b/src/app/utilsadapter.h @@ -86,6 +86,7 @@ public: Q_INVOKABLE void setClipboardText(QString text); Q_INVOKABLE const QString qStringFromFile(const QString& filename); Q_INVOKABLE const QString getStyleSheet(const QString& name, const QString& source); + Q_INVOKABLE const QString getLocalDataPath(); Q_INVOKABLE const QString getCachePath(); Q_INVOKABLE bool createStartupLink(); Q_INVOKABLE QString GetRingtonePath(); @@ -152,6 +153,14 @@ public: Q_INVOKABLE bool isRTL(); Q_INVOKABLE bool isSystemTrayIconVisible(); + Q_INVOKABLE QString base64Encode(const QString& input); + Q_INVOKABLE bool fileExists(const QString& filePath); + Q_INVOKABLE QString getStandardTempLocation(); + Q_INVOKABLE QString getMimeName(const QString& filePath) const; + +#ifdef ENABLE_TESTS + Q_INVOKABLE QString createDummyImage() const; +#endif Q_SIGNALS: void debugMessageReceived(const QString& message); void changeFontSize(); diff --git a/src/app/webengine/GeneralWebEngineView.qml b/src/app/webengine/GeneralWebEngineView.qml index da8e2dfef..d89868128 100644 --- a/src/app/webengine/GeneralWebEngineView.qml +++ b/src/app/webengine/GeneralWebEngineView.qml @@ -62,8 +62,8 @@ WebEngineView { } Component.onCompleted: { - profile.cachePath = UtilsAdapter.getCachePath(); - profile.persistentStoragePath = UtilsAdapter.getCachePath(); + profile.cachePath = UtilsAdapter.getLocalDataPath(); + profile.persistentStoragePath = UtilsAdapter.getLocalDataPath(); profile.persistentCookiesPolicy = WebEngineProfile.NoPersistentCookies; profile.httpCacheType = WebEngineProfile.NoCache; profile.httpUserAgent = JamiStrings.httpUserAgentName; diff --git a/tests/qml/main.cpp b/tests/qml/main.cpp index 0e92ed184..23edaa66a 100644 --- a/tests/qml/main.cpp +++ b/tests/qml/main.cpp @@ -90,7 +90,7 @@ public Q_SLOTS: systemTray_.get(), lrcInstance_.get(), settingsManager_.get(), - previewEngine_.get(), + connectivityMonitor_.get(), &screenInfo_, this); diff --git a/tests/qml/src/tst_CachedImage.qml b/tests/qml/src/tst_CachedImage.qml new file mode 100644 index 000000000..af04a7dac --- /dev/null +++ b/tests/qml/src/tst_CachedImage.qml @@ -0,0 +1,86 @@ +/* + * 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 <https://www.gnu.org/licenses/>. + */ + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import QtTest + +import net.jami.Adapters 1.1 +import net.jami.Models 1.1 +import net.jami.Constants 1.1 +import net.jami.Enums 1.1 +import net.jami.Helpers 1.1 + +import "../../../src/app/" +import "../../../src/app/mainview/components" + +Item { + + width: 800 + height: 600 + + CachedImage { + id: cachedImage + + TestCase { + name: "Test cachedImage" + when: windowShown + + SignalSpy { + id: spyDownloadSuccessful + target: ImageDownloader + signalName: "onDownloadImageSuccessful" + } + + SignalSpy { + id: spyDownloadFailed + target: ImageDownloader + signalName: "onDownloadImageFailed" + } + + function test_goodDownLoad() { + + var localPath = UtilsAdapter.getStandardTempLocation()+"/"+Math.random().toString(36).substring(7)+".svg" + + cachedImage.localPath = localPath + cachedImage.downloadUrl= "File://"+UtilsAdapter.createDummyImage() + + spyDownloadSuccessful.wait() + + compare(findChild(cachedImage,"image").source, Qt.url("file://"+localPath), "image source") + compare(findChild(cachedImage,"default_img").visible,false, "default_img visible") + + } + + function test_failedDownLoad() { + var imageUrl = "File:///dummy" + + var localPath = UtilsAdapter.getStandardTempLocation()+"/"+Math.random().toString(36).substring(7)+".svg" + + cachedImage.localPath = localPath + cachedImage.downloadUrl= imageUrl + + spyDownloadFailed.wait() + + compare(findChild(cachedImage,"image").source,"", "image source") + compare(findChild(cachedImage,"image").visible,true, "default_img visible") + } + } + } +} -- GitLab