Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/desktop.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/src/gui
diff options
context:
space:
mode:
authorClaudio Cambra <claudio.cambra@gmail.com>2022-01-20 14:54:36 +0300
committerClaudio Cambra (Rebase PR Action) <claudio.cambra@gmail.com>2022-03-17 13:46:09 +0300
commit65f2bada3e3095eb88c2e9b494ff8b87a72b5227 (patch)
treee1086cd1cdc1e3b44e84450a62e5e93ba65306cf /src/gui
parent315ebb0462ac77745e4b0f74d58376f74f442405 (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.txt4
-rw-r--r--src/gui/systray.cpp10
-rw-r--r--src/gui/tray/ActivityItem.qml2
-rw-r--r--src/gui/tray/ActivityItemContent.qml68
-rw-r--r--src/gui/tray/SyncStatus.qml16
-rw-r--r--src/gui/tray/UnifiedSearchInputContainer.qml9
-rw-r--r--src/gui/tray/UnifiedSearchResultItem.qml2
-rw-r--r--src/gui/tray/Window.qml14
-rw-r--r--src/gui/tray/activitydata.cpp96
-rw-r--r--src/gui/tray/activitydata.h34
-rw-r--r--src/gui/tray/activitylistmodel.cpp119
-rw-r--r--src/gui/tray/activitylistmodel.h3
-rw-r--r--src/gui/tray/asyncimageresponse.cpp109
-rw-r--r--src/gui/tray/asyncimageresponse.h37
-rw-r--r--src/gui/tray/trayimageprovider.cpp25
-rw-r--r--src/gui/tray/trayimageprovider.h (renamed from src/gui/tray/unifiedsearchresultimageprovider.h)6
-rw-r--r--src/gui/tray/unifiedsearchresultimageprovider.cpp131
-rw-r--r--src/gui/tray/usermodel.cpp83
-rw-r--r--src/gui/tray/usermodel.h1
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.