mirror of
https://github.com/nextcloud/desktop.git
synced 2024-11-25 22:46:04 +03:00
Merge pull request #4189 from nextcloud/feature/activity_thumbnails
Add thumbnails for files in the activity view
This commit is contained in:
commit
6aefe8f2e3
26 changed files with 835 additions and 261 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,20 +20,67 @@ RowLayout {
|
|||
signal dismissButtonClicked()
|
||||
signal shareButtonClicked()
|
||||
|
||||
spacing: 10
|
||||
spacing: Style.trayHorizontalMargin
|
||||
|
||||
Item {
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
|
||||
Layout.preferredWidth: Style.trayListItemIconSize
|
||||
Layout.preferredHeight: Style.trayListItemIconSize
|
||||
|
||||
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
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
|
||||
Layout.preferredWidth: 32
|
||||
Layout.preferredHeight: 32
|
||||
|
||||
verticalAlignment: Qt.AlignCenter
|
||||
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 {
|
||||
id: activityTextColumn
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
109
src/gui/tray/asyncimageresponse.cpp
Normal file
109
src/gui/tray/asyncimageresponse.cpp
Normal file
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
37
src/gui/tray/asyncimageresponse.h
Normal file
37
src/gui/tray/asyncimageresponse.h
Normal file
|
@ -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;
|
||||
};
|
25
src/gui/tray/trayimageprovider.cpp
Normal file
25
src/gui/tray/trayimageprovider.cpp
Normal file
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
184
test/testactivitydata.cpp
Normal file
184
test/testactivitydata.cpp
Normal file
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
* Copyright (C) by Claudio Cambra <claudio.cambra@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 "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 <QTest>
|
||||
|
||||
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> 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<QJsonObject>("activityJsonObject");
|
||||
QTest::addColumn<QString>("fileFormat");
|
||||
QTest::addColumn<QString>("mimeTypeExpected");
|
||||
QTest::addColumn<QString>("objectTypeExpected");
|
||||
QTest::addColumn<QString>("subjectExpected");
|
||||
QTest::addColumn<QString>("pathExpected");
|
||||
QTest::addColumn<QString>("fileNameExpected");
|
||||
QTest::addColumn<QString>("activityTypeExpected");
|
||||
QTest::addColumn<int>("activityIdExpected");
|
||||
QTest::addColumn<QString>("messageExpected");
|
||||
QTest::addColumn<QString>("objectNameExpected");
|
||||
QTest::addColumn<QString>("linkExpected");
|
||||
QTest::addColumn<QString>("datetimeExpected");
|
||||
QTest::addColumn<QString>("iconExpected");
|
||||
QTest::addColumn<QString>("subjectRichStringExpected");
|
||||
QTest::addColumn<QJsonArray>("subjectRichDataExpected");
|
||||
QTest::addColumn<QString>("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"
|
|
@ -190,13 +190,16 @@
|
|||
<file>theme/copy.svg</file>
|
||||
<file>theme/more.svg</file>
|
||||
<file>theme/change.svg</file>
|
||||
<file>theme/colored/change-bordered.svg</file>
|
||||
<file>theme/lock-http.svg</file>
|
||||
<file>theme/lock-https.svg</file>
|
||||
<file>theme/lock-broken.svg</file>
|
||||
<file>theme/network.svg</file>
|
||||
<file>theme/account.svg</file>
|
||||
<file>theme/colored/add.svg</file>
|
||||
<file>theme/colored/add-bordered.svg</file>
|
||||
<file>theme/colored/delete.svg</file>
|
||||
<file>theme/colored/delete-bordered.svg</file>
|
||||
<file>theme/colored/@APPLICATION_ICON_NAME@-icon.svg</file>
|
||||
<file>theme/add.svg</file>
|
||||
<file>theme/share.svg</file>
|
||||
|
|
|
@ -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
|
||||
|
|
43
theme/colored/add-bordered.svg
Normal file
43
theme/colored/add-bordered.svg
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
height="16"
|
||||
width="16"
|
||||
version="1.1"
|
||||
viewbox="0 0 16 16"
|
||||
id="svg4"
|
||||
sodipodi:docname="add-bordered.svg"
|
||||
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
id="namedview6"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="39.375"
|
||||
inkscape:cx="7.9873016"
|
||||
inkscape:cy="8"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1020"
|
||||
inkscape:window-x="1"
|
||||
inkscape:window-y="1111"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg4" />
|
||||
<path
|
||||
fill="#000000"
|
||||
d="m 9.02,13.98 h -2 V 8.9799996 h -5 V 6.98 h 5 v -5 h 2 v 5 l 5,-0.028 v 2.0279996 h -5 z"
|
||||
id="path2929"
|
||||
style="fill:#000001;fill-opacity:1;stroke:#ffffff;stroke-opacity:1;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none" />
|
||||
<path
|
||||
fill="#000"
|
||||
d="M9.02 13.98h-2v-5h-5v-2h5v-5h2v5l5-.028V8.98h-5z"
|
||||
id="path2" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
49
theme/colored/change-bordered.svg
Normal file
49
theme/colored/change-bordered.svg
Normal file
|
@ -0,0 +1,49 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
version="1.1"
|
||||
height="16"
|
||||
id="svg6"
|
||||
sodipodi:docname="change-bordered.svg"
|
||||
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs10" />
|
||||
<sodipodi:namedview
|
||||
id="namedview8"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="27.84233"
|
||||
inkscape:cx="6.0878526"
|
||||
inkscape:cy="3.9867353"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1020"
|
||||
inkscape:window-x="1"
|
||||
inkscape:window-y="1111"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg6" />
|
||||
<path
|
||||
d="m2 8.75v4.5l1.408-1.41c1.116 1.334 2.817 2.145 4.592 2.16 2.16 0.01827 4.116-1.132 5.196-3.002l-1.948-1.125c-0.677 1.171-1.9005 1.886-3.248 1.875-1.18-0.01-2.3047-0.572-3-1.5l1.5-1.5z"
|
||||
id="path2731"
|
||||
style="stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke:#ffffff;stroke-opacity:1" />
|
||||
<path
|
||||
d="M 8,2 C 5.858,2 3.875,3.145 2.804,5 L 4.752,6.125 C 5.423,4.963 6.658,4.25 7.9996,4.25 c 1.1906,0 2.297,0.56157 3,1.5 l -1.5,1.5 h 4.5 v -4.5 l -1.406,1.406 C 11.4646,2.808 9.792,2 8,2 Z"
|
||||
id="path827"
|
||||
style="stroke:#ffffff;stroke-opacity:1;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
sodipodi:nodetypes="cccsccccccc" />
|
||||
<path
|
||||
d="m8 2c-2.142 0-4.125 1.145-5.196 3l1.948 1.125c0.671-1.162 1.906-1.875 3.2476-1.875 1.1906 0 2.297 0.56157 3 1.5l-1.5 1.5h4.5v-4.5l-1.406 1.406c-1.129-1.348-2.802-2.1563-4.594-2.1563z"
|
||||
id="path2" />
|
||||
<path
|
||||
d="m2 8.75v4.5l1.408-1.41c1.116 1.334 2.817 2.145 4.592 2.16 2.16 0.01827 4.116-1.132 5.196-3.002l-1.948-1.125c-0.677 1.171-1.9005 1.886-3.248 1.875-1.18-0.01-2.3047-0.572-3-1.5l1.5-1.5z"
|
||||
id="path4" />
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
43
theme/colored/delete-bordered.svg
Normal file
43
theme/colored/delete-bordered.svg
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
height="16"
|
||||
width="16"
|
||||
version="1.1"
|
||||
viewBox="0 0 16 16"
|
||||
id="svg4"
|
||||
sodipodi:docname="delete-bordered.svg"
|
||||
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
id="namedview6"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="39.375"
|
||||
inkscape:cx="7.9873016"
|
||||
inkscape:cy="8"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1020"
|
||||
inkscape:window-x="1"
|
||||
inkscape:window-y="1111"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg4" />
|
||||
<path
|
||||
d="m3.0503 4.4645 3.5355 3.5355-3.5355 3.536 1.4142 1.414 3.5355-3.5358 3.536 3.5358 1.414-1.414-3.5358-3.536 3.5358-3.5355-1.414-1.4142-3.536 3.5355-3.5355-3.5355-1.4142 1.4142z"
|
||||
fill="#000"
|
||||
id="path849"
|
||||
style="stroke:#ffffff;stroke-opacity:1;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none" />
|
||||
<path
|
||||
d="m3.0503 4.4645 3.5355 3.5355-3.5355 3.536 1.4142 1.414 3.5355-3.5358 3.536 3.5358 1.414-1.414-3.5358-3.536 3.5358-3.5355-1.414-1.4142-3.536 3.5355-3.5355-3.5355-1.4142 1.4142z"
|
||||
fill="#000"
|
||||
id="path2" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
Loading…
Reference in a new issue