diff options
author | Claudio Cambra <claudio.cambra@gmail.com> | 2022-01-20 14:54:36 +0300 |
---|---|---|
committer | Claudio Cambra (Rebase PR Action) <claudio.cambra@gmail.com> | 2022-03-17 13:46:09 +0300 |
commit | 65f2bada3e3095eb88c2e9b494ff8b87a72b5227 (patch) | |
tree | e1086cd1cdc1e3b44e84450a62e5e93ba65306cf /src/gui | |
parent | 315ebb0462ac77745e4b0f74d58376f74f442405 (diff) |
Add thumbnails for files in the activity view
Signed-off-by: Claudio Cambra <claudio.cambra@gmail.com>
Diffstat (limited to 'src/gui')
-rw-r--r-- | src/gui/CMakeLists.txt | 4 | ||||
-rw-r--r-- | src/gui/systray.cpp | 10 | ||||
-rw-r--r-- | src/gui/tray/ActivityItem.qml | 2 | ||||
-rw-r--r-- | src/gui/tray/ActivityItemContent.qml | 68 | ||||
-rw-r--r-- | src/gui/tray/SyncStatus.qml | 16 | ||||
-rw-r--r-- | src/gui/tray/UnifiedSearchInputContainer.qml | 9 | ||||
-rw-r--r-- | src/gui/tray/UnifiedSearchResultItem.qml | 2 | ||||
-rw-r--r-- | src/gui/tray/Window.qml | 14 | ||||
-rw-r--r-- | src/gui/tray/activitydata.cpp | 96 | ||||
-rw-r--r-- | src/gui/tray/activitydata.h | 34 | ||||
-rw-r--r-- | src/gui/tray/activitylistmodel.cpp | 119 | ||||
-rw-r--r-- | src/gui/tray/activitylistmodel.h | 3 | ||||
-rw-r--r-- | src/gui/tray/asyncimageresponse.cpp | 109 | ||||
-rw-r--r-- | src/gui/tray/asyncimageresponse.h | 37 | ||||
-rw-r--r-- | src/gui/tray/trayimageprovider.cpp | 25 | ||||
-rw-r--r-- | src/gui/tray/trayimageprovider.h (renamed from src/gui/tray/unifiedsearchresultimageprovider.h) | 6 | ||||
-rw-r--r-- | src/gui/tray/unifiedsearchresultimageprovider.cpp | 131 | ||||
-rw-r--r-- | src/gui/tray/usermodel.cpp | 83 | ||||
-rw-r--r-- | src/gui/tray/usermodel.h | 1 |
19 files changed, 509 insertions, 260 deletions
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 <QtCore> #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 <QIcon> #include <QJsonObject> +#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<PreviewData> _previews; // Stores information about the error int _status; @@ -127,5 +160,6 @@ using ActivityList = QList<Activity>; 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<int, QByteArray> 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(); - - 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); + for (const auto &activ : activities) { + const auto json = activ.toObject(); - 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 <alex@nextcloud.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 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 <QIcon> +#include <QPainter> +#include <QSvgRenderer> + +#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<QNetworkReply *>(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("<svg"))) { + // SVG image needs proper scaling, let's do it with QPainter and QSvgRenderer + QSvgRenderer svgRenderer; + if (svgRenderer.load(imageData)) { + QImage scaledSvg(_requestedImageSize, QImage::Format_ARGB32); + scaledSvg.fill("transparent"); + QPainter painterForSvg(&scaledSvg); + svgRenderer.render(&painterForSvg); + setImageAndEmitFinished(scaledSvg); + return; + } else { + processNextImage(); + } + } else { + setImageAndEmitFinished(QImage::fromData(imageData)); + } + } +} + diff --git a/src/gui/tray/asyncimageresponse.h b/src/gui/tray/asyncimageresponse.h new file mode 100644 index 000000000..b7394acdf --- /dev/null +++ b/src/gui/tray/asyncimageresponse.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.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 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 <QImage> +#include <QQuickImageProvider> + +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 <alex@nextcloud.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 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 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 <alex@nextcloud.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 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 <QImage> -#include <QPainter> -#include <QSvgRenderer> - -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<QNetworkReply *>(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("<svg"))) { - // SVG image needs proper scaling, let's do it with QPainter and QSvgRenderer - QSvgRenderer svgRenderer; - if (svgRenderer.load(imageData)) { - QImage scaledSvg(_requestedImageSize, QImage::Format_ARGB32); - scaledSvg.fill("transparent"); - QPainter painterForSvg(&scaledSvg); - svgRenderer.render(&painterForSvg); - setImageAndEmitFinished(scaledSvg); - return; - } else { - processNextImage(); - } - } else { - setImageAndEmitFinished(QImage::fromData(imageData)); - } - } - } - - QImage _image; - QStringList _imagePaths; - QSize _requestedImageSize; - int _index = 0; -}; -} - -namespace OCC { - -QQuickImageResponse *UnifiedSearchResultImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize) -{ - return new AsyncImageResponse(id, requestedSize); -} - -} diff --git a/src/gui/tray/usermodel.cpp b/src/gui/tray/usermodel.cpp index 17eb51645..487d995cb 100644 --- a/src/gui/tray/usermodel.cpp +++ b/src/gui/tray/usermodel.cpp @@ -16,6 +16,7 @@ #include "tray/notificationcache.h" #include "tray/unifiedsearchresultslistmodel.h" #include "userstatusconnector.h" +#include "thumbnailjob.h" #include <QDesktopServices> #include <QIcon> @@ -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. |