diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 6b3cf688e..3d1075a4b 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -195,10 +195,10 @@ set(client_SRCS tray/activitylistmodel.h tray/activitylistmodel.cpp tray/unifiedsearchresult.h + tray/asyncimageresponse.cpp tray/unifiedsearchresult.cpp - tray/unifiedsearchresultimageprovider.h - tray/unifiedsearchresultimageprovider.cpp tray/unifiedsearchresultslistmodel.h + tray/trayimageprovider.cpp tray/unifiedsearchresultslistmodel.cpp tray/usermodel.h tray/usermodel.cpp diff --git a/src/gui/systray.cpp b/src/gui/systray.cpp index 992343bd5..a5a352dd4 100644 --- a/src/gui/systray.cpp +++ b/src/gui/systray.cpp @@ -20,7 +20,7 @@ #include "tray/svgimageprovider.h" #include "tray/usermodel.h" #include "wheelhandler.h" -#include "tray/unifiedsearchresultimageprovider.h" +#include "tray/trayimageprovider.h" #include "configfile.h" #include "accessmanager.h" @@ -65,7 +65,7 @@ void Systray::setTrayEngine(QQmlApplicationEngine *trayEngine) _trayEngine->addImportPath("qrc:/qml/theme"); _trayEngine->addImageProvider("avatars", new ImageProvider); _trayEngine->addImageProvider(QLatin1String("svgimage-custom-color"), new OCC::Ui::SvgImageProvider); - _trayEngine->addImageProvider(QLatin1String("unified-search-result-icon"), new UnifiedSearchResultImageProvider); + _trayEngine->addImageProvider(QLatin1String("tray-image-provider"), new TrayImageProvider); } Systray::Systray() @@ -513,7 +513,11 @@ AccessManagerFactory::AccessManagerFactory() QNetworkAccessManager* AccessManagerFactory::create(QObject *parent) { - return new AccessManager(parent); + const auto am = new AccessManager(parent); + const auto diskCache = new QNetworkDiskCache(am); + diskCache->setCacheDirectory("cacheDir"); + am->setCache(diskCache); + return am; } } // namespace OCC diff --git a/src/gui/tray/ActivityItem.qml b/src/gui/tray/ActivityItem.qml index 54c272cfb..9c049529a 100644 --- a/src/gui/tray/ActivityItem.qml +++ b/src/gui/tray/ActivityItem.qml @@ -38,8 +38,8 @@ MouseArea { ColumnLayout { anchors.left: root.left anchors.right: root.right - anchors.leftMargin: 15 anchors.rightMargin: 10 + anchors.leftMargin: 10 spacing: 0 diff --git a/src/gui/tray/ActivityItemContent.qml b/src/gui/tray/ActivityItemContent.qml index fe0bcafe6..7cfc59edc 100644 --- a/src/gui/tray/ActivityItemContent.qml +++ b/src/gui/tray/ActivityItemContent.qml @@ -3,6 +3,7 @@ import QtQuick 2.15 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 import Style 1.0 +import QtGraphicalEffects 1.15 import com.nextcloud.desktopclient 1.0 RowLayout { @@ -19,19 +20,66 @@ RowLayout { signal dismissButtonClicked() signal shareButtonClicked() - spacing: 10 - - Image { - id: activityIcon + spacing: Style.trayHorizontalMargin + Item { Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter - Layout.preferredWidth: 32 - Layout.preferredHeight: 32 + Layout.preferredWidth: Style.trayListItemIconSize + Layout.preferredHeight: Style.trayListItemIconSize - verticalAlignment: Qt.AlignCenter - source: icon - sourceSize.height: 64 - sourceSize.width: 64 + Loader { + id: thumbnailImageLoader + anchors.fill: parent + active: model.thumbnail !== undefined + + sourceComponent: Item { + anchors.fill: parent + + Image { + id: thumbnailImage + width: model.thumbnail.isMimeTypeIcon ? parent.width * 0.85 : parent.width * 0.8 + height: model.thumbnail.isMimeTypeIcon ? parent.height * 0.85 : parent.height * 0.8 + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + cache: true + source: model.thumbnail.source + visible: false + sourceSize.height: 64 + sourceSize.width: 64 + } + + Rectangle { + id: mask + color: "white" + radius: 3 + anchors.fill: thumbnailImage + visible: false + width: thumbnailImage.paintedWidth + height: thumbnailImage.paintedHeight + } + + OpacityMask { + anchors.fill: thumbnailImage + source: thumbnailImage + maskSource: mask + visible: model.thumbnail !== undefined + } + } + } + + Image { + id: activityIcon + width: model.thumbnail !== undefined ? parent.width * 0.5 : parent.width * 0.85 + height: model.thumbnail !== undefined ? parent.height * 0.5 : parent.height * 0.85 + anchors.verticalCenter: if(model.thumbnail === undefined) parent.verticalCenter + anchors.left: if(model.thumbnail === undefined) parent.left + anchors.right: if(model.thumbnail !== undefined) parent.right + anchors.bottom: if(model.thumbnail !== undefined) parent.bottom + cache: true + source: icon + sourceSize.height: 64 + sourceSize.width: 64 + } } Column { diff --git a/src/gui/tray/SyncStatus.qml b/src/gui/tray/SyncStatus.qml index 7bf9a2d55..83a38cf4f 100644 --- a/src/gui/tray/SyncStatus.qml +++ b/src/gui/tray/SyncStatus.qml @@ -11,7 +11,7 @@ RowLayout { property alias model: syncStatus - spacing: 0 + spacing: Style.trayHorizontalMargin NC.SyncStatusSummary { id: syncStatus @@ -19,15 +19,18 @@ RowLayout { Image { id: syncIcon + Layout.preferredWidth: Style.trayListItemIconSize * 0.85 + Layout.preferredHeight: Style.trayListItemIconSize * 0.85 Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter Layout.topMargin: 16 + Layout.rightMargin: Style.trayListItemIconSize * 0.15 Layout.bottomMargin: 16 - Layout.leftMargin: 16 + Layout.leftMargin: Style.trayHorizontalMargin source: syncStatus.syncIcon - sourceSize.width: 32 - sourceSize.height: 32 + sourceSize.width: 64 + sourceSize.height: 64 rotation: syncStatus.syncing ? 0 : 0 } @@ -45,8 +48,7 @@ RowLayout { Layout.alignment: Qt.AlignVCenter Layout.topMargin: 8 - Layout.rightMargin: 16 - Layout.leftMargin: 10 + Layout.rightMargin: Style.trayHorizontalMargin Layout.bottomMargin: 8 Layout.fillWidth: true Layout.fillHeight: true @@ -65,7 +67,7 @@ RowLayout { Loader { Layout.fillWidth: true - active: syncStatus.syncing; + active: syncStatus.syncing visible: syncStatus.syncing sourceComponent: ProgressBar { diff --git a/src/gui/tray/UnifiedSearchInputContainer.qml b/src/gui/tray/UnifiedSearchInputContainer.qml index 7ca1a99c2..df99c6630 100644 --- a/src/gui/tray/UnifiedSearchInputContainer.qml +++ b/src/gui/tray/UnifiedSearchInputContainer.qml @@ -13,15 +13,15 @@ TextField { readonly property color textFieldIconsColor: Style.menuBorder - readonly property int textFieldIconsOffset: 10 + readonly property int textFieldIconsOffset: Style.trayHorizontalMargin readonly property double textFieldIconsScaleFactor: 0.6 - readonly property int textFieldHorizontalPaddingOffset: 14 + readonly property int textFieldHorizontalPaddingOffset: Style.trayHorizontalMargin signal clearText() - leftPadding: trayWindowUnifiedSearchTextFieldSearchIcon.width + trayWindowUnifiedSearchTextFieldSearchIcon.anchors.leftMargin + textFieldHorizontalPaddingOffset + leftPadding: trayWindowUnifiedSearchTextFieldSearchIcon.width + trayWindowUnifiedSearchTextFieldSearchIcon.anchors.leftMargin + textFieldHorizontalPaddingOffset - 1 rightPadding: trayWindowUnifiedSearchTextFieldClearTextButton.width + trayWindowUnifiedSearchTextFieldClearTextButton.anchors.rightMargin + textFieldHorizontalPaddingOffset placeholderText: qsTr("Search files, messages, events …") @@ -36,6 +36,9 @@ TextField { Image { id: trayWindowUnifiedSearchTextFieldSearchIcon + width: Style.trayListItemIconSize - anchors.leftMargin + fillMode: Image.PreserveAspectFit + horizontalAlignment: Image.AlignLeft anchors { left: parent.left diff --git a/src/gui/tray/UnifiedSearchResultItem.qml b/src/gui/tray/UnifiedSearchResultItem.qml index 0241ed28e..69daa19df 100644 --- a/src/gui/tray/UnifiedSearchResultItem.qml +++ b/src/gui/tray/UnifiedSearchResultItem.qml @@ -39,7 +39,7 @@ RowLayout { id: unifiedSearchResultThumbnail visible: false asynchronous: true - source: "image://unified-search-result-icon/" + icons + source: "image://tray-image-provider/" + icons cache: true sourceSize.width: imageData.width sourceSize.height: imageData.height diff --git a/src/gui/tray/Window.qml b/src/gui/tray/Window.qml index a17b3a0fb..a6f97a680 100644 --- a/src/gui/tray/Window.qml +++ b/src/gui/tray/Window.qml @@ -22,8 +22,8 @@ Window { color: "transparent" flags: Systray.useNormalWindow ? Qt.Window : Qt.Dialog | Qt.FramelessWindowHint - property int fileActivityDialogObjectId: -1 + readonly property int maxMenuHeight: Style.trayWindowHeight - Style.trayWindowHeaderHeight - 2 * Style.trayWindowBorderWidth function openFileActivityDialog(objectName, objectId) { @@ -345,7 +345,7 @@ Window { Image { id: currentAccountAvatar - Layout.leftMargin: 8 + Layout.leftMargin: Style.trayHorizontalMargin verticalAlignment: Qt.AlignCenter cache: false source: UserModel.currentUser.avatar != "" ? UserModel.currentUser.avatar : "image://avatars/fallbackWhite" @@ -601,9 +601,9 @@ Window { left: trayWindowBackground.left right: trayWindowBackground.right - margins: { - top: 10 - } + topMargin: Style.trayHorizontalMargin + controlRoot.padding + leftMargin: Style.trayHorizontalMargin + controlRoot.padding + rightMargin: Style.trayHorizontalMargin + controlRoot.padding } text: UserModel.currentUser.unifiedSearchResultsListModel.searchTerm @@ -623,7 +623,7 @@ Window { anchors.top: trayWindowUnifiedSearchInputContainer.bottom anchors.left: trayWindowBackground.left anchors.right: trayWindowBackground.right - anchors.margins: 10 + anchors.margins: Style.trayHorizontalMargin } UnifiedSearchResultNothingFound { @@ -632,7 +632,7 @@ Window { anchors.top: trayWindowUnifiedSearchInputContainer.bottom anchors.left: trayWindowBackground.left anchors.right: trayWindowBackground.right - anchors.topMargin: 10 + anchors.topMargin: Style.trayHorizontalMargin text: UserModel.currentUser.unifiedSearchResultsListModel.searchTerm diff --git a/src/gui/tray/activitydata.cpp b/src/gui/tray/activitydata.cpp index 16f1f1c6d..8a077fc28 100644 --- a/src/gui/tray/activitydata.cpp +++ b/src/gui/tray/activitydata.cpp @@ -15,6 +15,7 @@ #include #include "activitydata.h" +#include "folderman.h" namespace OCC { @@ -44,4 +45,99 @@ ActivityLink ActivityLink::createFomJsonObject(const QJsonObject &obj) return activityLink; } + +OCC::Activity Activity::fromActivityJson(const QJsonObject json, const AccountPtr account) +{ + const auto activityUser = json.value(QStringLiteral("user")).toString(); + + Activity activity; + activity._type = Activity::ActivityType; + activity._objectType = json.value(QStringLiteral("object_type")).toString(); + activity._objectId = json.value(QStringLiteral("object_id")).toInt(); + activity._objectName = json.value(QStringLiteral("object_name")).toString(); + activity._id = json.value(QStringLiteral("activity_id")).toInt(); + activity._fileAction = json.value(QStringLiteral("type")).toString(); + activity._accName = account->displayName(); + activity._subject = json.value(QStringLiteral("subject")).toString(); + activity._message = json.value(QStringLiteral("message")).toString(); + activity._file = json.value(QStringLiteral("object_name")).toString(); + activity._link = QUrl(json.value(QStringLiteral("link")).toString()); + activity._dateTime = QDateTime::fromString(json.value(QStringLiteral("datetime")).toString(), Qt::ISODate); + activity._icon = json.value(QStringLiteral("icon")).toString(); + activity._isCurrentUserFileActivity = activity._objectType == QStringLiteral("files") && activityUser == account->davUser(); + + auto richSubjectData = json.value(QStringLiteral("subject_rich")).toArray(); + + if(richSubjectData.size() > 1) { + activity._subjectRich = richSubjectData[0].toString(); + auto parameters = richSubjectData[1].toObject(); + const QRegularExpression subjectRichParameterRe(QStringLiteral("({[a-zA-Z0-9]*})")); + const QRegularExpression subjectRichParameterBracesRe(QStringLiteral("[{}]")); + + for (auto i = parameters.begin(); i != parameters.end(); ++i) { + const auto parameterJsonObject = i.value().toObject(); + + activity._subjectRichParameters[i.key()] = Activity::RichSubjectParameter { + parameterJsonObject.value(QStringLiteral("type")).toString(), + parameterJsonObject.value(QStringLiteral("id")).toString(), + parameterJsonObject.value(QStringLiteral("name")).toString(), + parameterJsonObject.contains(QStringLiteral("path")) ? parameterJsonObject.value(QStringLiteral("path")).toString() : QString(), + parameterJsonObject.contains(QStringLiteral("link")) ? QUrl(parameterJsonObject.value(QStringLiteral("link")).toString()) : QUrl(), + }; + } + + auto displayString = activity._subjectRich; + auto subjectRichParameterMatch = subjectRichParameterRe.globalMatch(displayString); + + while (subjectRichParameterMatch.hasNext()) { + const auto match = subjectRichParameterMatch.next(); + auto word = match.captured(1); + word.remove(subjectRichParameterBracesRe); + + Q_ASSERT(activity._subjectRichParameters.contains(word)); + displayString = displayString.replace(match.captured(1), activity._subjectRichParameters[word].name); + } + + activity._subjectDisplay = displayString; + } + + const auto previewsData = json.value(QStringLiteral("previews")).toArray(); + + for(const auto preview : previewsData) { + const auto jsonPreviewData = preview.toObject(); + + PreviewData data; + data._link = jsonPreviewData.value(QStringLiteral("link")).toString(); + data._mimeType = jsonPreviewData.value(QStringLiteral("mimeType")).toString(); + data._fileId = jsonPreviewData.value(QStringLiteral("fileId")).toInt(); + data._view = jsonPreviewData.value(QStringLiteral("view")).toString(); + data._filename = jsonPreviewData.value(QStringLiteral("filename")).toString(); + + if(data._mimeType.contains(QStringLiteral("text/"))) { + data._source = account->url().toString() + QStringLiteral("/index.php/apps/theming/img/core/filetypes/text.svg"); + data._isMimeTypeIcon = true; + } else if (data._mimeType.contains(QStringLiteral("/pdf"))) { + data._source = account->url().toString() + QStringLiteral("/index.php/apps/theming/img/core/filetypes/application-pdf.svg"); + data._isMimeTypeIcon = true; + } else { + data._source = jsonPreviewData.value(QStringLiteral("source")).toString(); + data._isMimeTypeIcon = jsonPreviewData.value(QStringLiteral("isMimeTypeIcon")).toBool(); + } + + activity._previews.append(data); + } + + if(!previewsData.isEmpty()) { + if(activity._icon.contains(QStringLiteral("add-color.svg"))) { + activity._icon = "qrc:///client/theme/colored/add-bordered.svg"; + } else if(activity._icon.contains(QStringLiteral("delete-color.svg"))) { + activity._icon = "qrc:///client/theme/colored/delete-bordered.svg"; + } else if(activity._icon.contains(QStringLiteral("change.svg"))) { + activity._icon = "qrc:///client/theme/colored/change-bordered.svg"; + } + } + + return activity; +} + } diff --git a/src/gui/tray/activitydata.h b/src/gui/tray/activitydata.h index 49ffd5f3b..20b278326 100644 --- a/src/gui/tray/activitydata.h +++ b/src/gui/tray/activitydata.h @@ -19,6 +19,10 @@ #include #include +#include "syncfileitem.h" +#include "folder.h" +#include "account.h" + namespace OCC { /** * @brief The ActivityLink class describes actions of an activity @@ -49,6 +53,32 @@ public: bool _primary; }; +/** + * @brief The PreviewData class describes the data about a file's preview. + */ + +class PreviewData +{ + Q_GADGET + + Q_PROPERTY(QString source MEMBER _source) + Q_PROPERTY(QString link MEMBER _link) + Q_PROPERTY(QString mimeType MEMBER _mimeType) + Q_PROPERTY(int fileId MEMBER _fileId) + Q_PROPERTY(QString view MEMBER _view) + Q_PROPERTY(bool isMimeTypeIcon MEMBER _isMimeTypeIcon) + Q_PROPERTY(QString filename MEMBER _filename) + +public: + QString _source; + QString _link; + QString _mimeType; + int _fileId; + QString _view; + bool _isMimeTypeIcon; + QString _filename; +}; + /* ==================================================================== */ /** * @brief Activity Structure @@ -69,6 +99,8 @@ public: SyncFileItemType }; + static Activity fromActivityJson(const QJsonObject json, const AccountPtr account); + struct RichSubjectParameter { QString type; // Required QString id; // Required @@ -97,6 +129,7 @@ public: QString _accName; QString _icon; bool _isCurrentUserFileActivity = false; + QVector _previews; // Stores information about the error int _status; @@ -127,5 +160,6 @@ using ActivityList = QList; Q_DECLARE_METATYPE(OCC::Activity::Type) Q_DECLARE_METATYPE(OCC::ActivityLink) +Q_DECLARE_METATYPE(OCC::PreviewData) #endif // ACTIVITYDATA_H diff --git a/src/gui/tray/activitylistmodel.cpp b/src/gui/tray/activitylistmodel.cpp index a67f8aa94..8cad1589e 100644 --- a/src/gui/tray/activitylistmodel.cpp +++ b/src/gui/tray/activitylistmodel.cpp @@ -74,6 +74,7 @@ QHash ActivityListModel::roleNames() const roles[DisplayActions] = "displayActions"; roles[ShareableRole] = "isShareable"; roles[IsCurrentUserFileActivityRole] = "isCurrentUserFileActivity"; + roles[ThumbnailRole] = "thumbnail"; return roles; } @@ -175,6 +176,18 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const return displayPath == "." || displayPath == "/" ? QString() : displayPath; }; + const auto generatePreviewMap = [](const PreviewData &preview) { + return(QVariantMap { + {QStringLiteral("source"), QStringLiteral("image://tray-image-provider/").append(preview._source)}, + {QStringLiteral("link"), preview._link}, + {QStringLiteral("mimeType"), preview._mimeType}, + {QStringLiteral("fileId"), preview._fileId}, + {QStringLiteral("view"), preview._view}, + {QStringLiteral("isMimeTypeIcon"), preview._isMimeTypeIcon}, + {QStringLiteral("filename"), preview._filename}, + }); + }; + switch (role) { case DisplayPathRole: return getDisplayPath(); @@ -220,11 +233,14 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const } else { // File sync successful if (a._fileAction == "file_created") { - return "qrc:///client/theme/colored/add.svg"; + return a._previews.empty() ? "qrc:///client/theme/colored/add.svg" + : "qrc:///client/theme/colored/add-bordered.svg"; } else if (a._fileAction == "file_deleted") { - return "qrc:///client/theme/colored/delete.svg"; + return a._previews.empty() ? "qrc:///client/theme/colored/delete.svg" + : "qrc:///client/theme/colored/delete-bordered.svg"; } else { - return "qrc:///client/theme/change.svg"; + return a._previews.empty() ? "qrc:///client/theme/change.svg" + : "qrc:///client/theme/colored/change-bordered.svg"; } } } else { @@ -286,6 +302,14 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const return !data(index, PathRole).toString().isEmpty() && a._objectType == QStringLiteral("files") && _displayActions && a._fileAction != "file_deleted" && a._status != SyncFileItem::FileIgnored; case IsCurrentUserFileActivityRole: return a._isCurrentUserFileActivity; + case ThumbnailRole: { + if(a._previews.empty()) { + return {}; + } + + const auto preview = a._previews[0]; + return(generatePreviewMap(preview)); + } default: return QVariant(); } @@ -324,6 +348,7 @@ void ActivityListModel::startFetchJob() this, &ActivityListModel::activitiesReceived); QUrlQuery params; + params.addQueryItem(QLatin1String("previews"), QLatin1String("true")); params.addQueryItem(QLatin1String("since"), QString::number(_currentItem)); params.addQueryItem(QLatin1String("limit"), QString::number(50)); job->addQueryParams(params); @@ -348,80 +373,17 @@ int ActivityListModel::currentItem() const return _currentItem; } -void ActivityListModel::activitiesReceived(const QJsonDocument &json, int statusCode) +void ActivityListModel::ingestActivities(const QJsonArray &activities) { - auto activities = json.object().value("ocs").toObject().value("data").toArray(); - ActivityList list; - auto ast = _accountState; - if (!ast) { - return; - } - - if (activities.size() == 0) { - _doneFetching = true; - } - - _currentlyFetching = false; QDateTime oldestDate = QDateTime::currentDateTime(); oldestDate = oldestDate.addDays(_maxActivitiesDays * -1); - foreach (auto activ, activities) { - auto json = activ.toObject(); + for (const auto &activ : activities) { + const auto json = activ.toObject(); - Activity a; - const auto activityUser = json.value(QStringLiteral("user")).toString(); - a._type = Activity::ActivityType; - a._objectType = json.value(QStringLiteral("object_type")).toString(); - a._objectId = json.value(QStringLiteral("object_id")).toInt(); - a._objectName = json.value(QStringLiteral("object_name")).toString(); - a._accName = ast->account()->displayName(); - a._id = json.value(QStringLiteral("activity_id")).toInt(); - a._fileAction = json.value(QStringLiteral("type")).toString(); - a._subject = json.value(QStringLiteral("subject")).toString(); - a._message = json.value(QStringLiteral("message")).toString(); - a._file = json.value(QStringLiteral("object_name")).toString(); - a._link = QUrl(json.value(QStringLiteral("link")).toString()); - a._dateTime = QDateTime::fromString(json.value(QStringLiteral("datetime")).toString(), Qt::ISODate); - a._icon = json.value(QStringLiteral("icon")).toString(); - a._isCurrentUserFileActivity = a._objectType == QStringLiteral("files") && activityUser == ast->account()->davUser(); - - auto richSubjectData = json.value(QStringLiteral("subject_rich")).toArray(); - - if(richSubjectData.size() > 1) { - a._subjectRich = richSubjectData[0].toString(); - auto parameters = richSubjectData[1].toObject(); - const QRegularExpression subjectRichParameterRe(QStringLiteral("({[a-zA-Z0-9]*})")); - const QRegularExpression subjectRichParameterBracesRe(QStringLiteral("[{}]")); - - for (auto i = parameters.begin(); i != parameters.end(); ++i) { - const auto parameterJsonObject = i.value().toObject(); - const Activity::RichSubjectParameter parameter = { - parameterJsonObject.value(QStringLiteral("type")).toString(), - parameterJsonObject.value(QStringLiteral("id")).toString(), - parameterJsonObject.value(QStringLiteral("name")).toString(), - parameterJsonObject.contains(QStringLiteral("path")) ? parameterJsonObject.value(QStringLiteral("path")).toString() : QString(), - parameterJsonObject.contains(QStringLiteral("link")) ? QUrl(parameterJsonObject.value(QStringLiteral("link")).toString()) : QUrl(), - }; - - a._subjectRichParameters[i.key()] = parameter; - } - - auto displayString = a._subjectRich; - auto i = subjectRichParameterRe.globalMatch(displayString); - - while (i.hasNext()) { - const auto match = i.next(); - auto word = match.captured(1); - word.remove(subjectRichParameterBracesRe); - - Q_ASSERT(a._subjectRichParameters.contains(word)); - displayString = displayString.replace(match.captured(1), a._subjectRichParameters[word].name); - } - - a._subjectDisplay = displayString; - } + const auto a = Activity::fromActivityJson(json, _accountState->account()); list.append(a); _currentItem = list.last()._id; @@ -436,6 +398,23 @@ void ActivityListModel::activitiesReceived(const QJsonDocument &json, int status } _activityLists.append(list); +} + +void ActivityListModel::activitiesReceived(const QJsonDocument &json, int statusCode) +{ + const auto activities = json.object().value(QStringLiteral("ocs")).toObject().value(QStringLiteral("data")).toArray(); + + if (!_accountState) { + return; + } + + if (activities.empty()) { + _doneFetching = true; + } + + _currentlyFetching = false; + + ingestActivities(activities); combineActivityLists(); diff --git a/src/gui/tray/activitylistmodel.h b/src/gui/tray/activitylistmodel.h index 126ecc28e..3a688a636 100644 --- a/src/gui/tray/activitylistmodel.h +++ b/src/gui/tray/activitylistmodel.h @@ -66,6 +66,7 @@ public: DisplayActions, ShareableRole, IsCurrentUserFileActivityRole, + ThumbnailRole, }; Q_ENUM(DataRole) @@ -136,6 +137,8 @@ private: void combineActivityLists(); bool canFetchActivities() const; + void ingestActivities(const QJsonArray &activities); + ActivityList _activityLists; ActivityList _syncFileItemLists; ActivityList _notificationLists; diff --git a/src/gui/tray/asyncimageresponse.cpp b/src/gui/tray/asyncimageresponse.cpp new file mode 100644 index 000000000..e484ab0de --- /dev/null +++ b/src/gui/tray/asyncimageresponse.cpp @@ -0,0 +1,109 @@ +/* + * Copyright (C) by Oleksandr Zolotov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) 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. + */ + +#include +#include +#include + +#include "asyncimageresponse.h" +#include "usermodel.h" + +AsyncImageResponse::AsyncImageResponse(const QString &id, const QSize &requestedSize) +{ + if (id.isEmpty()) { + setImageAndEmitFinished(); + return; + } + + _imagePaths = id.split(QLatin1Char(';'), Qt::SkipEmptyParts); + _requestedImageSize = requestedSize; + + if (_imagePaths.isEmpty()) { + setImageAndEmitFinished(); + } else { + processNextImage(); + } +} + +void AsyncImageResponse::setImageAndEmitFinished(const QImage &image) +{ + _image = image; + emit finished(); +} + +QQuickTextureFactory* AsyncImageResponse::textureFactory() const +{ + return QQuickTextureFactory::textureFactoryForImage(_image); +} + +void AsyncImageResponse::processNextImage() +{ + if (_index < 0 || _index >= _imagePaths.size()) { + setImageAndEmitFinished(); + return; + } + + if (_imagePaths.at(_index).startsWith(QStringLiteral(":/client"))) { + setImageAndEmitFinished(QIcon(_imagePaths.at(_index)).pixmap(_requestedImageSize).toImage()); + return; + } + + const auto currentUser = OCC::UserModel::instance()->currentUser(); + if (currentUser && currentUser->account()) { + const QUrl iconUrl(_imagePaths.at(_index)); + if (iconUrl.isValid() && !iconUrl.scheme().isEmpty()) { + // fetch the remote resource + const auto reply = currentUser->account()->sendRawRequest(QByteArrayLiteral("GET"), iconUrl); + connect(reply, &QNetworkReply::finished, this, &AsyncImageResponse::slotProcessNetworkReply); + ++_index; + return; + } + } + + setImageAndEmitFinished(); +} + +void AsyncImageResponse::slotProcessNetworkReply() +{ + const auto reply = qobject_cast(sender()); + if (!reply) { + setImageAndEmitFinished(); + return; + } + + const QByteArray imageData = reply->readAll(); + // server returns "[]" for some some file previews (have no idea why), so, we use another image + // from the list if available + if (imageData.isEmpty() || imageData == QByteArrayLiteral("[]")) { + processNextImage(); + } else { + if (imageData.startsWith(QByteArrayLiteral(" + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) 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. + */ + +#pragma once + +#include +#include + +class AsyncImageResponse : public QQuickImageResponse +{ +public: + AsyncImageResponse(const QString &id, const QSize &requestedSize); + void setImageAndEmitFinished(const QImage &image = {}); + QQuickTextureFactory *textureFactory() const override; + +private: + void processNextImage(); + +private slots: + void slotProcessNetworkReply(); + + QImage _image; + QStringList _imagePaths; + QSize _requestedImageSize; + int _index = 0; +}; diff --git a/src/gui/tray/trayimageprovider.cpp b/src/gui/tray/trayimageprovider.cpp new file mode 100644 index 000000000..b9a98b448 --- /dev/null +++ b/src/gui/tray/trayimageprovider.cpp @@ -0,0 +1,25 @@ +/* + * Copyright (C) by Oleksandr Zolotov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) 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. + */ + +#include "trayimageprovider.h" +#include "asyncimageresponse.h" + +namespace OCC { + +QQuickImageResponse *TrayImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize) +{ + return new AsyncImageResponse(id, requestedSize); +} + +} diff --git a/src/gui/tray/unifiedsearchresultimageprovider.h b/src/gui/tray/trayimageprovider.h similarity index 79% rename from src/gui/tray/unifiedsearchresultimageprovider.h rename to src/gui/tray/trayimageprovider.h index 0e35c9be7..fc2076a20 100644 --- a/src/gui/tray/unifiedsearchresultimageprovider.h +++ b/src/gui/tray/trayimageprovider.h @@ -20,12 +20,12 @@ namespace OCC { /** - * @brief The UnifiedSearchResultImageProvider + * @brief The TrayImageProvider * @ingroup gui - * Allows to fetch Unified Search result icon from the server or used a local resource + * Allows to fetch icon from the server or used a local resource */ -class UnifiedSearchResultImageProvider : public QQuickAsyncImageProvider +class TrayImageProvider : public QQuickAsyncImageProvider { public: QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override; diff --git a/src/gui/tray/unifiedsearchresultimageprovider.cpp b/src/gui/tray/unifiedsearchresultimageprovider.cpp deleted file mode 100644 index 97a57f519..000000000 --- a/src/gui/tray/unifiedsearchresultimageprovider.cpp +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright (C) by Oleksandr Zolotov - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) 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. - */ - -#include "unifiedsearchresultimageprovider.h" - -#include "usermodel.h" - -#include -#include -#include - -namespace { -class AsyncImageResponse : public QQuickImageResponse -{ -public: - AsyncImageResponse(const QString &id, const QSize &requestedSize) - { - if (id.isEmpty()) { - setImageAndEmitFinished(); - return; - } - - _imagePaths = id.split(QLatin1Char(';'), Qt::SkipEmptyParts); - _requestedImageSize = requestedSize; - - if (_imagePaths.isEmpty()) { - setImageAndEmitFinished(); - } else { - processNextImage(); - } - } - - void setImageAndEmitFinished(const QImage &image = {}) - { - _image = image; - emit finished(); - } - - QQuickTextureFactory *textureFactory() const override - { - return QQuickTextureFactory::textureFactoryForImage(_image); - } - -private: - void processNextImage() - { - if (_index < 0 || _index >= _imagePaths.size()) { - setImageAndEmitFinished(); - return; - } - - if (_imagePaths.at(_index).startsWith(QStringLiteral(":/client"))) { - setImageAndEmitFinished(QIcon(_imagePaths.at(_index)).pixmap(_requestedImageSize).toImage()); - return; - } - - const auto currentUser = OCC::UserModel::instance()->currentUser(); - if (currentUser && currentUser->account()) { - const QUrl iconUrl(_imagePaths.at(_index)); - if (iconUrl.isValid() && !iconUrl.scheme().isEmpty()) { - // fetch the remote resource - const auto reply = currentUser->account()->sendRawRequest(QByteArrayLiteral("GET"), iconUrl); - connect(reply, &QNetworkReply::finished, this, &AsyncImageResponse::slotProcessNetworkReply); - ++_index; - return; - } - } - - setImageAndEmitFinished(); - } - -private slots: - void slotProcessNetworkReply() - { - const auto reply = qobject_cast(sender()); - if (!reply) { - setImageAndEmitFinished(); - return; - } - - const QByteArray imageData = reply->readAll(); - // server returns "[]" for some some file previews (have no idea why), so, we use another image - // from the list if available - if (imageData.isEmpty() || imageData == QByteArrayLiteral("[]")) { - processNextImage(); - } else { - if (imageData.startsWith(QByteArrayLiteral(" #include @@ -499,50 +500,88 @@ bool User::isUnsolvableConflict(const SyncFileItemPtr &item) const void User::processCompletedSyncItem(const Folder *folder, const SyncFileItemPtr &item) { + const auto fileActionFromInstruction = [](const int instruction) { + if (instruction == CSYNC_INSTRUCTION_REMOVE) { + return QStringLiteral("file_deleted"); + } else if (instruction == CSYNC_INSTRUCTION_NEW) { + return QStringLiteral("file_created"); + } else if (instruction == CSYNC_INSTRUCTION_RENAME) { + return QStringLiteral("file_renamed"); + } else { + return QStringLiteral("file_changed"); + } + }; + + const auto messageFromFileAction = [](const QString &fileAction, const QString &fileName) { + if (fileAction == QStringLiteral("file_renamed")) { + return QObject::tr("You renamed %1").arg(fileName); + } else if (fileAction == QStringLiteral("file_deleted")) { + return QObject:: tr("You deleted %1").arg(fileName); + } else if (fileAction == QStringLiteral("file_created")) { + return QObject::tr("You created %1").arg(fileName); + } else { + return QObject::tr("You changed %1").arg(fileName); + } + }; + Activity activity; activity._type = Activity::SyncFileItemType; //client activity activity._status = item->_status; activity._dateTime = QDateTime::currentDateTime(); activity._message = item->_originalFile; - activity._link = folder->accountState()->account()->url(); - activity._accName = folder->accountState()->account()->displayName(); + activity._link = account()->url(); + activity._accName = account()->displayName(); activity._file = item->_file; activity._folder = folder->alias(); activity._fileAction = ""; - activity._objectId = item->_fileId.toInt(); - activity._objectName = item->_file; const auto fileName = QFileInfo(item->_originalFile).fileName(); - if (item->_instruction == CSYNC_INSTRUCTION_REMOVE) { - activity._fileAction = "file_deleted"; - } else if (item->_instruction == CSYNC_INSTRUCTION_NEW) { - activity._fileAction = "file_created"; - } else if (item->_instruction == CSYNC_INSTRUCTION_RENAME) { - activity._fileAction = "file_renamed"; - activity._renamedFile = item->_renameTarget; - } else { - activity._fileAction = "file_changed"; - } + activity._fileAction = fileActionFromInstruction(item->_instruction); if (item->_status == SyncFileItem::NoStatus || item->_status == SyncFileItem::Success) { qCWarning(lcActivity) << "Item " << item->_file << " retrieved successfully."; if (item->_direction != SyncFileItem::Up) { - activity._message = tr("Synced %1").arg(fileName); - } else if (activity._fileAction == "file_renamed") { - activity._message = tr("You renamed %1").arg(fileName); - } else if (activity._fileAction == "file_deleted") { - activity._message = tr("You deleted %1").arg(fileName); - } else if (activity._fileAction == "file_created") { - activity._message = tr("You created %1").arg(fileName); + activity._message = QObject::tr("Synced %1").arg(fileName); } else { - activity._message = tr("You changed %1").arg(fileName); + activity._message = messageFromFileAction(activity._fileAction, fileName); + } + + if(activity._fileAction != "file_deleted") { + auto remotePath = folder->remotePath(); + remotePath.append(activity._fileAction == "file_renamed" ? item->_renameTarget : activity._file); + + const auto localFiles = FolderMan::instance()->findFileInLocalFolders(item->_file, account()); + if (!localFiles.isEmpty()) { + const QMimeType mimeType = _mimeDb.mimeTypeForFile(QFileInfo(localFiles.constFirst())); + + // Set the preview data, though for now we can skip setting file ID, link, and view + PreviewData preview; + preview._mimeType = mimeType.name(); + preview._filename = fileName; + + if(item->isDirectory()) { + preview._source = account()->url().toString() + QStringLiteral("/index.php/apps/theming/img/core/filetypes/folder.svg"); + preview._isMimeTypeIcon = true; + } else if(mimeType.isValid() && mimeType.inherits("text/plain")) { + preview._source = account()->url().toString() + QStringLiteral("/index.php/apps/theming/img/core/filetypes/text.svg"); + preview._isMimeTypeIcon = true; + } else if (mimeType.isValid() && mimeType.inherits("application/pdf")) { + preview._source = account()->url().toString() + QStringLiteral("/index.php/apps/theming/img/core/filetypes/application-pdf.svg"); + preview._isMimeTypeIcon = true; + } else { + preview._source = account()->url().toString() + QStringLiteral("/index.php/apps/files/api/v1/thumbnail/150/150/") + remotePath; + preview._isMimeTypeIcon = false; + } + activity._previews.append(preview); + } } _activityModel->addSyncFileItemToActivityList(activity); } else { qCWarning(lcActivity) << "Item " << item->_file << " retrieved resulted in error " << item->_errorString; + activity._subject = item->_errorString; if (item->_status == SyncFileItem::Status::FileIgnored) { diff --git a/src/gui/tray/usermodel.h b/src/gui/tray/usermodel.h index 36beaa029..8844ef291 100644 --- a/src/gui/tray/usermodel.h +++ b/src/gui/tray/usermodel.h @@ -134,6 +134,7 @@ private: QElapsedTimer _guiLogTimer; NotificationCache _notificationCache; + QMimeDatabase _mimeDb; // number of currently running notification requests. If non zero, // no query for notifications is started. diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 011ad5580..ccecd5fce 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -62,6 +62,7 @@ nextcloud_add_test(NotificationCache) nextcloud_add_test(SetUserStatusDialog) nextcloud_add_test(UnifiedSearchListmodel) nextcloud_add_test(ActivityListModel) +nextcloud_add_test(ActivityData) if( UNIX AND NOT APPLE ) nextcloud_add_test(InotifyWatcher) diff --git a/test/testactivitydata.cpp b/test/testactivitydata.cpp new file mode 100644 index 000000000..28def5329 --- /dev/null +++ b/test/testactivitydata.cpp @@ -0,0 +1,184 @@ +/* + * Copyright (C) by Claudio Cambra + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) 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. + */ + +#include "gui/tray/activitydata.h" +#include "account.h" +#include "accountstate.h" +#include "configfile.h" +#include "syncenginetestutils.h" +#include "syncfileitem.h" +#include "folder.h" +#include "folderman.h" +#include "testhelper.h" + +#include + +class TestActivityData : public QObject +{ + Q_OBJECT + +public: + TestActivityData() = default; + + void createJsonSpecificFormatData(QString fileFormat, QString mimeType) + { + const auto objectType = QStringLiteral("files"); + const auto subject = QStringLiteral("You created path/test.").append(fileFormat); + const auto path = QStringLiteral("path/test.").append(fileFormat); + const auto fileName = QStringLiteral("test.").append(fileFormat); + const auto activityType = QStringLiteral("file"); + const auto activityId = 90000; + const auto message = QStringLiteral(); + const auto objectName = QStringLiteral("test.").append(fileFormat); + const auto link = account->url().toString().append(QStringLiteral("/f/")).append(activityId); + const auto datetime = QDateTime::currentDateTime().toString(Qt::ISODate); + const auto icon = account->url().toString().append(QStringLiteral("/apps/files/img/add-color.svg")); + + const QJsonObject richStringData({ + {QStringLiteral("type"), activityType}, + {QStringLiteral("id"), activityId}, + {QStringLiteral("link"), link}, + {QStringLiteral("name"), fileName}, + {QStringLiteral("path"), objectName} + }); + + const auto subjectRichString = QStringLiteral("You created {file1}"); + const auto subjectRichObj = QJsonObject({{QStringLiteral("file1"), richStringData}}); + const auto subjectRichData = QJsonArray({subjectRichString, subjectRichObj}); + + const auto previewUrl = account->url().toString().append(QStringLiteral("/index.php/core/preview.png?file=/")).append(path); + + // Text file previews should be replaced by mimetype icon + const QJsonObject previewData({ + {QStringLiteral("link"), link}, + {QStringLiteral("mimeType"), mimeType}, + {QStringLiteral("fileId"), activityId}, + {QStringLiteral("filename"), fileName}, + {QStringLiteral("view"), QStringLiteral("files")}, + {QStringLiteral("source"), previewUrl}, + {QStringLiteral("isMimeTypeIcon"), false}, + }); + + QJsonObject testData({ + {QStringLiteral("object_type"), objectType}, + {QStringLiteral("activity_id"), activityId}, + {QStringLiteral("type"), activityType}, + {QStringLiteral("subject"), subject}, + {QStringLiteral("message"), message}, + {QStringLiteral("object_name"), objectName}, + {QStringLiteral("link"), link}, + {QStringLiteral("datetime"), datetime}, + {QStringLiteral("icon"), icon}, + {QStringLiteral("subject_rich"), subjectRichData}, + {QStringLiteral("previews"), QJsonArray({previewData})}, + }); + + QTest::addRow("data") << testData << fileFormat << mimeType << objectType << subject << path << fileName << activityType << activityId << message << objectName << link << datetime << icon << subjectRichString << subjectRichData << previewUrl; + } + + QScopedPointer fakeQnam; + OCC::AccountPtr account; + +private slots: + void initTestCase() + { + account = OCC::Account::create(); + account->setCredentials(new FakeCredentials{fakeQnam.data()}); + account->setUrl(QUrl(("http://example.de"))); + auto *cred = new HttpCredentialsTest("testuser", "secret"); + account->setCredentials(cred); + } + + void testFromJson_data() + { + QTest::addColumn("activityJsonObject"); + QTest::addColumn("fileFormat"); + QTest::addColumn("mimeTypeExpected"); + QTest::addColumn("objectTypeExpected"); + QTest::addColumn("subjectExpected"); + QTest::addColumn("pathExpected"); + QTest::addColumn("fileNameExpected"); + QTest::addColumn("activityTypeExpected"); + QTest::addColumn("activityIdExpected"); + QTest::addColumn("messageExpected"); + QTest::addColumn("objectNameExpected"); + QTest::addColumn("linkExpected"); + QTest::addColumn("datetimeExpected"); + QTest::addColumn("iconExpected"); + QTest::addColumn("subjectRichStringExpected"); + QTest::addColumn("subjectRichDataExpected"); + QTest::addColumn("previewUrlExpected"); + + createJsonSpecificFormatData(QStringLiteral("jpg"), QStringLiteral("image/jpg")); + createJsonSpecificFormatData(QStringLiteral("txt"), QStringLiteral("text/plain")); + createJsonSpecificFormatData(QStringLiteral("pdf"), QStringLiteral("application/pdf")); + } + + void testFromJson() + { + QFETCH(QJsonObject, activityJsonObject); + QFETCH(QString, fileFormat); + QFETCH(QString, mimeTypeExpected); + QFETCH(QString, objectTypeExpected); + QFETCH(QString, subjectExpected); + QFETCH(QString, pathExpected); + QFETCH(QString, fileNameExpected); + QFETCH(QString, activityTypeExpected); + QFETCH(int, activityIdExpected); + QFETCH(QString, messageExpected); + QFETCH(QString, objectNameExpected); + QFETCH(QString, linkExpected); + QFETCH(QString, datetimeExpected); + QFETCH(QString, iconExpected); + QFETCH(QString, subjectRichStringExpected); + QFETCH(QJsonArray, subjectRichDataExpected); + QFETCH(QString, previewUrlExpected); + + OCC::Activity activity = OCC::Activity::fromActivityJson(activityJsonObject, account); + QCOMPARE(activity._type, OCC::Activity::ActivityType); + QCOMPARE(activity._objectType, objectTypeExpected); + QCOMPARE(activity._id, activityIdExpected); + QCOMPARE(activity._fileAction, activityTypeExpected); + QCOMPARE(activity._accName, account->displayName()); + QCOMPARE(activity._subject, subjectExpected); + QCOMPARE(activity._message, messageExpected); + QCOMPARE(activity._file, objectNameExpected); + QCOMPARE(activity._link, linkExpected); + QCOMPARE(activity._dateTime, QDateTime::fromString(datetimeExpected, Qt::ISODate)); + + QCOMPARE(activity._subjectRichParameters.count(), 1); + QCOMPARE(activity._subjectDisplay, QStringLiteral("You created ").append(fileNameExpected)); + + QCOMPARE(activity._previews.count(), 1); + // We want the different icon when we have a preview + //QCOMPARE(activity._icon, iconExpected); + + if(fileFormat == "txt") { + QCOMPARE(activity._previews[0]._source, account->url().toString().append(QStringLiteral("/index.php/apps/theming/img/core/filetypes/text.svg"))); + QCOMPARE(activity._previews[0]._isMimeTypeIcon, true); + QCOMPARE(activity._previews[0]._mimeType, mimeTypeExpected); + } else if(fileFormat == "pdf") { + QCOMPARE(activity._previews[0]._source, account->url().toString().append(QStringLiteral("/index.php/apps/theming/img/core/filetypes/application-pdf.svg"))); + QCOMPARE(activity._previews[0]._isMimeTypeIcon, true); + } else { + QCOMPARE(activity._previews[0]._source, previewUrlExpected); + QCOMPARE(activity._previews[0]._isMimeTypeIcon, false); + } + + QCOMPARE(activity._previews[0]._mimeType, mimeTypeExpected); + } +}; + +QTEST_MAIN(TestActivityData) +#include "testactivitydata.moc" diff --git a/theme.qrc.in b/theme.qrc.in index f927692f8..c2d84b95f 100644 --- a/theme.qrc.in +++ b/theme.qrc.in @@ -190,13 +190,16 @@ theme/copy.svg theme/more.svg theme/change.svg + theme/colored/change-bordered.svg theme/lock-http.svg theme/lock-https.svg theme/lock-broken.svg theme/network.svg theme/account.svg theme/colored/add.svg + theme/colored/add-bordered.svg theme/colored/delete.svg + theme/colored/delete-bordered.svg theme/colored/@APPLICATION_ICON_NAME@-icon.svg theme/add.svg theme/share.svg diff --git a/theme/Style/Style.qml b/theme/Style/Style.qml index 1425de189..d25f7c6f4 100644 --- a/theme/Style/Style.qml +++ b/theme/Style/Style.qml @@ -29,6 +29,8 @@ QtObject { property int trayWindowRadius: 10 property int trayWindowBorderWidth: 1 property int trayWindowHeaderHeight: variableSize(60) + property int trayHorizontalMargin: 10 + property int trayListItemIconSize: accountAvatarSize property int currentAccountButtonWidth: 220 property int currentAccountButtonRadius: 2 @@ -58,7 +60,7 @@ QtObject { property int roundedButtonBackgroundVerticalMargins: 5 property int userStatusEmojiSize: 8 - property int userStatusSpacing: 6 + property int userStatusSpacing: trayHorizontalMargin property int userStatusAnchorsMargin: 2 property int accountServerAnchorsMargin: 10 property int accountLabelsSpacing: 4 diff --git a/theme/colored/add-bordered.svg b/theme/colored/add-bordered.svg new file mode 100644 index 000000000..76b628467 --- /dev/null +++ b/theme/colored/add-bordered.svg @@ -0,0 +1,43 @@ + + + + + + + diff --git a/theme/colored/change-bordered.svg b/theme/colored/change-bordered.svg new file mode 100644 index 000000000..01b4b71bb --- /dev/null +++ b/theme/colored/change-bordered.svg @@ -0,0 +1,49 @@ + + + + + + + + + diff --git a/theme/colored/delete-bordered.svg b/theme/colored/delete-bordered.svg new file mode 100644 index 000000000..4a6d969e9 --- /dev/null +++ b/theme/colored/delete-bordered.svg @@ -0,0 +1,43 @@ + + + + + + +