diff --git a/REUSE.toml b/REUSE.toml
index 7bd192ed0570c..be2cdfaf2ed0f 100644
--- a/REUSE.toml
+++ b/REUSE.toml
@@ -127,7 +127,7 @@ SPDX-FileCopyrightText = "2016 Nextcloud GmbH"
SPDX-License-Identifier = "LicenseRef-NextcloudTrademarks"
[[annotations]]
-path = ["theme/white/error.svg", "theme/black/error.svg","theme/white/warning.svg", "theme/black/warning.svg", "theme/white/info.svg", "theme/black/info.svg", "theme/info.svg", "theme/white/nextcloud/*.svg", "theme/black/nextcloud/*.svg", "theme/colored/nextcloud/*.svg", "theme/black/label.svg", "theme/white/label.svg", "theme/colored/user-status-*.svg", "theme/account.svg","theme/add.svg","theme/change.svg","theme/chevron-double-up.svg","theme/close.svg","theme/confirm.svg","theme/copy.svg","theme/delete.svg","theme/external.svg","theme/files.svg","theme/lock-broken.svg","theme/lock-http.svg","theme/lock-https.svg","theme/lock.svg","theme/magnifying-glass.svg","theme/more.svg","theme/network.svg","theme/public.svg","theme/reply.svg","theme/send.svg","theme/settings.svg","theme/share.svg","theme/sync-arrow.svg", "theme/black/account-group.svg","theme/black/clear.svg","theme/black/expand-less-black.svg","theme/black/folder-group.svg","theme/black/search.svg", "theme/colored/add-bordered.svg", "theme/colored/change-bordered.svg", "theme/colored/delete-bordered.svg", "theme/colored/delete.svg", "theme/*/activity.svg", "theme/*/add.svg","theme/*/bell.svg","theme/*/calendar.svg","theme/*/caret-down.svg","theme/*/change.svg","theme/*/close.svg","theme/*/comment.svg","theme/*/confirm.svg","theme/*/control-next.svg","theme/*/control-prev.svg","theme/*/edit.svg","theme/*/email.svg","theme/*/external.png","theme/*/external.svg","theme/*/external@2x.png","theme/*/folder.png","theme/*/folder.svg","theme/*/folder@2x.png","theme/*/more-apps.svg","theme/*/nc-assistant-app.svg","theme/*/settings.svg","theme/*/user.svg","theme/*/wizard-files.png","theme/*/wizard-files.svg","theme/*/wizard-files@2x.png","theme/*/wizard-groupware.png","theme/*/wizard-groupware.svg","theme/*/wizard-groupware@2x.png", "theme/cfapishellext_custom_states/0-locked.svg","theme/cfapishellext_custom_states/1-shared.svg","theme/cfapishellext_custom_states/1024-0-locked.png","theme/cfapishellext_custom_states/1024-1-shared.png","theme/cfapishellext_custom_states/128-0-locked.png","theme/cfapishellext_custom_states/128-1-shared.png","theme/cfapishellext_custom_states/24-0-locked.png","theme/cfapishellext_custom_states/24-1-shared.png","theme/cfapishellext_custom_states/256-0-locked.png","theme/cfapishellext_custom_states/256-1-shared.png","theme/cfapishellext_custom_states/32-0-locked.png","theme/cfapishellext_custom_states/32-1-shared.png","theme/cfapishellext_custom_states/40-0-locked.png","theme/cfapishellext_custom_states/40-1-shared.png","theme/cfapishellext_custom_states/48-0-locked.png","theme/cfapishellext_custom_states/48-1-shared.png","theme/cfapishellext_custom_states/512-0-locked.png","theme/cfapishellext_custom_states/512-1-shared.png","theme/cfapishellext_custom_states/64-0-locked.png","theme/cfapishellext_custom_states/64-1-shared.png"]
+path = ["theme/white/error.svg", "theme/black/error.svg","theme/white/warning.svg", "theme/black/warning.svg", "theme/white/info.svg", "theme/black/info.svg", "theme/info.svg", "theme/white/nextcloud/*.svg", "theme/black/nextcloud/*.svg", "theme/colored/nextcloud/*.svg", "theme/black/label.svg", "theme/white/label.svg", "theme/colored/user-status-*.svg", "theme/account.svg","theme/add.svg","theme/change.svg","theme/chevron-double-up.svg","theme/close.svg","theme/confirm.svg","theme/copy.svg","theme/delete.svg","theme/external.svg","theme/files.svg","theme/lock-broken.svg","theme/lock-http.svg","theme/lock-https.svg","theme/lock.svg","theme/magnifying-glass.svg","theme/more.svg","theme/network.svg","theme/public.svg","theme/reply.svg","theme/send.svg","theme/settings.svg","theme/share.svg","theme/sync-arrow.svg", "theme/black/account-group.svg","theme/black/clear.svg","theme/black/expand-less-black.svg","theme/black/folder-group.svg","theme/black/search.svg", "theme/colored/add-bordered.svg", "theme/colored/change-bordered.svg", "theme/colored/delete-bordered.svg", "theme/colored/delete.svg", "theme/*/activity.svg", "theme/*/add.svg","theme/*/bell.svg","theme/*/calendar.svg","theme/*/caret-down.svg","theme/*/change.svg","theme/*/close.svg","theme/*/comment.svg","theme/*/confirm.svg","theme/*/control-next.svg","theme/*/control-prev.svg","theme/*/edit.svg","theme/*/email.svg","theme/*/external.png","theme/*/external.svg","theme/*/external@2x.png","theme/*/folder.png","theme/*/folder.svg","theme/*/folder@2x.png","theme/*/more-apps.svg","theme/*/nc-assistant-app.svg","theme/*/settings.svg","theme/*/user.svg","theme/*/wizard-files.png","theme/*/wizard-files.svg","theme/*/wizard-files@2x.png","theme/*/wizard-groupware.png","theme/*/wizard-groupware.svg","theme/*/wizard-groupware@2x.png", "theme/cfapishellext_custom_states/0-locked.svg","theme/cfapishellext_custom_states/1-shared.svg","theme/cfapishellext_custom_states/1024-0-locked.png","theme/cfapishellext_custom_states/1024-1-shared.png","theme/cfapishellext_custom_states/128-0-locked.png","theme/cfapishellext_custom_states/128-1-shared.png","theme/cfapishellext_custom_states/24-0-locked.png","theme/cfapishellext_custom_states/24-1-shared.png","theme/cfapishellext_custom_states/256-0-locked.png","theme/cfapishellext_custom_states/256-1-shared.png","theme/cfapishellext_custom_states/32-0-locked.png","theme/cfapishellext_custom_states/32-1-shared.png","theme/cfapishellext_custom_states/40-0-locked.png","theme/cfapishellext_custom_states/40-1-shared.png","theme/cfapishellext_custom_states/48-0-locked.png","theme/cfapishellext_custom_states/48-1-shared.png","theme/cfapishellext_custom_states/512-0-locked.png","theme/cfapishellext_custom_states/512-1-shared.png","theme/cfapishellext_custom_states/64-0-locked.png","theme/cfapishellext_custom_states/64-1-shared.png", "theme/backup.svg", "theme/convert_to_text.svg", "theme/file-open.svg"]
precedence = "aggregate"
SPDX-FileCopyrightText = "2018-2025 Google LLC"
SPDX-License-Identifier = "Apache-2.0"
diff --git a/resources.qrc b/resources.qrc
index 2681104929615..f26f90192acfe 100644
--- a/resources.qrc
+++ b/resources.qrc
@@ -61,5 +61,6 @@
src/gui/ConflictItemFileInfo.qml
src/gui/macOS/ui/FileProviderSettings.qml
src/gui/macOS/ui/FileProviderFileDelegate.qml
+ src/gui/integration/FileActionsWindow.qml
diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt
index b6d636f47ac1c..40f11aeb961e0 100644
--- a/src/gui/CMakeLists.txt
+++ b/src/gui/CMakeLists.txt
@@ -258,6 +258,8 @@ set(client_SRCS
wizard/linklabel.cpp
wizard/wizardproxysettingsdialog.h
wizard/wizardproxysettingsdialog.cpp
+ integration/fileactionsmodel.h
+ integration/fileactionsmodel.cpp
)
if (NOT DISABLE_ACCOUNT_MIGRATION)
diff --git a/src/gui/application.cpp b/src/gui/application.cpp
index ae14bc11a3ff5..db3ad8a3ec064 100644
--- a/src/gui/application.cpp
+++ b/src/gui/application.cpp
@@ -432,6 +432,9 @@ Application::Application(int &argc, char **argv)
connect(FolderMan::instance()->socketApi(), &SocketApi::fileActivityCommandReceived,
_gui.data(), &ownCloudGui::slotShowFileActivityDialog);
+ connect(FolderMan::instance()->socketApi(), &SocketApi::fileActionsCommandReceived,
+ _gui.data(), &ownCloudGui::slotShowFileActionsDialog);
+
// startup procedure.
connect(&_checkConnectionTimer, &QTimer::timeout, this, &Application::slotCheckConnection);
_checkConnectionTimer.setInterval(ConnectionValidator::DefaultCallingIntervalMsec); // check for connection every 32 seconds.
diff --git a/src/gui/integration/FileActionsWindow.qml b/src/gui/integration/FileActionsWindow.qml
new file mode 100644
index 0000000000000..e526fbff6dc25
--- /dev/null
+++ b/src/gui/integration/FileActionsWindow.qml
@@ -0,0 +1,261 @@
+/*
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtQuick.Window
+import QtQuick.Layouts
+import QtQuick.Controls
+import Qt5Compat.GraphicalEffects
+import com.nextcloud.desktopclient
+import Style
+import "./../tray"
+
+ApplicationWindow {
+ id: root
+ height: Style.filesActionsHeight
+ width: Style.filesActionsWidth
+ flags: Systray.useNormalWindow ? Qt.Window : Qt.Dialog | Qt.FramelessWindowHint
+ visible: true
+ color: "transparent"
+
+ property var accountState: ({})
+ property string localPath: ""
+ property string shortLocalPath: ""
+
+ readonly property int windowRadius: Systray.useNormalWindow ? 0.0 : Style.trayWindowRadius
+
+ title: qsTr("File actions for %1").arg(root.shortLocalPath)
+
+ FileActionsModel {
+ id: fileActionModel
+ accountState: root.accountState
+ localPath: root.localPath
+ }
+
+ background: Rectangle {
+ id: maskSource
+ radius: root.windowRadius
+ border.width: Style.trayWindowBorderWidth
+ border.color: palette.dark
+ color: palette.window
+ }
+
+ OpacityMask {
+ anchors.fill: parent
+ anchors.margins: Style.trayWindowBorderWidth
+ source: maskSourceItem
+ maskSource: maskSource
+ }
+
+ Rectangle {
+ id: maskSourceItem
+ anchors.fill: parent
+ anchors.margins: Style.standardSpacing
+ radius: root.windowRadius
+ clip: true
+ color: Style.colorWithoutTransparency(palette.base)
+
+ ColumnLayout {
+ id: windowContent
+ anchors.fill: parent
+ anchors.margins: Style.standardSpacing
+
+ RowLayout {
+ id: windowHeader
+ Layout.fillWidth: true
+ spacing: Style.standardSpacing
+
+ Image {
+ source: fileActionModel.fileIcon + palette.windowText
+ Layout.preferredWidth: Style.minimumActivityItemHeight
+ Layout.preferredHeight: Style.minimumActivityItemHeight
+ Layout.alignment: Qt.AlignVCenter
+ Layout.margins: Style.extraSmallSpacing
+ }
+
+ EnforcedPlainTextLabel {
+ id: headerLocalPath
+ text: root.shortLocalPath
+ elide: Text.ElideRight
+ font.bold: true
+ font.pixelSize: Style.pixelSize
+ color: palette.text
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
+ }
+
+ Button {
+ id: closeButton
+ flat: true
+ padding: Style.extraSmallSpacing
+ spacing: 0
+ icon.source: "image://svgimage-custom-color/close.svg/" + palette.windowText
+ icon.width: Style.extraSmallIconSize
+ icon.height: Style.extraSmallIconSize
+ Layout.alignment: Qt.AlignTop | Qt.AlignRight
+ Layout.rightMargin: Style.extraSmallSpacing
+ Layout.topMargin: Style.extraSmallSpacing
+ onClicked: root.close()
+ background: Rectangle {
+ color: "transparent"
+ radius: root.windowRadius
+ border.width: closeButton.hovered ? Style.trayWindowBorderWidth : 0
+ border.color: palette.dark
+ anchors.fill: parent
+ Layout.margins: Style.extraSmallSpacing
+ }
+ }
+ }
+
+ Rectangle {
+ id: lineTop
+ Layout.fillWidth: true
+ Layout.minimumHeight: Style.extraExtraSmallSpacing
+ color: palette.dark
+ }
+
+ ListView {
+ id: fileActionsView
+ model: fileActionModel
+ clip: true
+ spacing: Style.trayHorizontalMargin
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ delegate: fileActionsDelegate
+ }
+
+ Button {
+ id: responseButton
+ visible: responseText.text !== ""
+ flat: true
+ Layout.fillWidth: true
+ implicitHeight: responseContent.implicitHeight
+
+ padding: Style.standardSpacing
+ leftPadding: Style.standardSpacing
+ rightPadding: Style.standardSpacing
+ spacing: Style.standardSpacing
+
+ background: Rectangle {
+ id: responseBorder
+ radius: root.windowRadius
+ border.width: Style.trayWindowBorderWidth
+ border.color: palette.dark
+ color: palette.window
+ Layout.fillWidth: true
+ }
+
+ contentItem: RowLayout {
+ id: responseContent
+ anchors.fill: parent
+ anchors.margins: Style.smallSpacing
+ spacing: Style.standardSpacing
+ Layout.fillWidth: true
+ Layout.minimumHeight: Style.accountAvatarStateIndicatorSize
+
+ Image {
+ source: "image://svgimage-custom-color/backup.svg/" + palette.windowText
+ Layout.preferredWidth: Style.minimumActivityItemHeight
+ Layout.preferredHeight: Style.minimumActivityItemHeight
+ Layout.alignment: Qt.AlignLeft
+ Layout.bottomMargin: Style.standardSpacing
+ }
+
+ Text {
+ id: responseText
+ text: fileActionModel.responseLabel
+ textFormat: Text.RichText
+ color: palette.text
+ font.pointSize: Style.pixelSize
+ font.underline: true
+ wrapMode: Text.WordWrap
+ Layout.fillWidth: true
+ bottomPadding: Style.standardSpacing
+ }
+ }
+
+ MouseArea {
+ id: responseArea
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onClicked: Qt.openUrlExternally(fileActionModel.responseUrl)
+ }
+ }
+ }
+ }
+
+ Component {
+ id: fileActionsDelegate
+
+ RowLayout {
+ id: fileAction
+ Layout.fillWidth: true
+ height: implicitHeight
+ width: parent.width
+
+ required property string name
+ required property int index
+ required property string icon
+
+ Button {
+ id: fileActionButton
+ flat: true
+ Layout.fillWidth: true
+ implicitHeight: Style.activityListButtonHeight
+
+ padding: Style.standardSpacing
+
+ contentItem: Row {
+ id: fileActionsContent
+ anchors.fill: parent
+ anchors.topMargin: Style.standardSpacing
+ anchors.rightMargin: Style.standardSpacing
+ anchors.bottomMargin: Style.standardSpacing
+ anchors.leftMargin: Style.extraSmallSpacing
+ spacing: Style.standardSpacing
+ Layout.fillWidth: true
+
+ Image {
+ source: fileAction.icon + palette.windowText
+ width: Style.minimumActivityItemHeight
+ height: Style.minimumActivityItemHeight
+ fillMode: Image.PreserveAspectFit
+ Layout.preferredWidth: Style.minimumActivityItemHeight
+ Layout.preferredHeight: Style.minimumActivityItemHeight
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ EnforcedPlainTextLabel {
+ text: fileAction.name
+ color: palette.text
+ font.pixelSize: Style.defaultFontPtSize
+ verticalAlignment: Text.AlignVCenter
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ background: Rectangle {
+ color: "transparent"
+ radius: root.windowRadius
+ border.width: fileActionButton.hovered ? Style.trayWindowBorderWidth : 0
+ border.color: palette.dark
+ anchors.margins: Style.standardSpacing
+ height: parent.height
+ width: parent.width
+ }
+
+ MouseArea {
+ id: fileActionMouseArea
+ anchors.fill: parent
+ anchors.margins: Style.standardSpacing
+ cursorShape: Qt.PointingHandCursor
+ onClicked: fileActionModel.createRequest(fileAction.index)
+ }
+ }
+ }
+ }
+}
diff --git a/src/gui/integration/fileactionsmodel.cpp b/src/gui/integration/fileactionsmodel.cpp
new file mode 100644
index 0000000000000..1d8d913ca0c2e
--- /dev/null
+++ b/src/gui/integration/fileactionsmodel.cpp
@@ -0,0 +1,324 @@
+/*
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "fileactionsmodel.h"
+#include "networkjobs.h"
+#include "account.h"
+#include "folderman.h"
+#include "common/utility.h"
+#include "tray/activitydata.h"
+
+namespace OCC {
+
+Q_LOGGING_CATEGORY(lcFileActions, "nextcloud.gui.fileactions", QtInfoMsg)
+
+FileActionsModel::FileActionsModel(QObject *parent)
+ : QAbstractListModel(parent)
+{
+}
+
+QVariant FileActionsModel::data(const QModelIndex &index, int role) const
+{
+ Q_ASSERT(checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid));
+ const auto row = index.row();
+ switch (role) {
+ case FileActionIconRole:
+ return _fileActions.at(row).icon; // deck.svg
+ case FileActionNameRole:
+ return _fileActions.at(row).name; // Convert file
+ case FileActionUrlRole:
+ return _fileActions.at(row).url; // /ocs/v2.php/apps/declarativetest/newDeckBoard
+ case FileActionMethodRole:
+ return _fileActions.at(row).method; // GET
+ case FileActionParamsRole:
+ return QVariant::fromValue(_fileActions.at(row).params);
+ case FileActionResponseLabelRole:
+ return _response.label;
+ case FileActionResponseUrlRole:
+ return _response.url;
+ default:
+ return QVariant();
+ }
+
+ return {};
+}
+
+int FileActionsModel::rowCount(const QModelIndex &parent) const
+{
+ if (parent.isValid()) {
+ return 0;
+ }
+
+ return _fileActions.size();
+}
+
+QHash FileActionsModel::roleNames() const
+{
+ auto roles = QAbstractListModel::roleNames();
+ roles[FileActionIconRole] = "icon";
+ roles[FileActionNameRole] = "name";
+ roles[FileActionUrlRole] = "url";
+ roles[FileActionMethodRole] = "method";
+ roles[FileActionParamsRole] = "params";
+ roles[FileActionResponseLabelRole] = "responseLabel";
+ roles[FileActionResponseUrlRole] = "responseUrl";
+
+ return roles;
+}
+
+AccountState *FileActionsModel::accountState() const
+{
+ return _accountState;
+}
+
+void FileActionsModel::setAccountState(AccountState *accountState)
+{
+ if (accountState == nullptr) {
+ return;
+ }
+
+ if (accountState == _accountState) {
+ return;
+ }
+
+ _accountState = accountState;
+ _accountUrl = _accountState->account()->url().toString();
+ Q_EMIT accountStateChanged();
+}
+
+QString FileActionsModel::localPath() const
+{
+ return _localPath;
+}
+
+void FileActionsModel::setLocalPath(const QString &localPath)
+{
+ if (localPath.isEmpty()) {
+ return;
+ }
+
+ if (localPath == _localPath) {
+ return;
+ }
+
+ _localPath = localPath;
+
+ setupFileProperties();
+ parseEndpoints();
+
+ Q_EMIT fileChanged();
+}
+
+QByteArray FileActionsModel::fileId() const
+{
+ return _fileId;
+}
+
+void FileActionsModel::setupFileProperties()
+{
+ const auto folderForPath = FolderMan::instance()->folderForPath(_localPath);
+ _filePath = _localPath.mid(folderForPath->cleanPath().length() + 1);
+ SyncJournalFileRecord fileRecord;
+ if (!folderForPath->journalDb()->getFileRecord(_filePath, &fileRecord)) {
+ qCWarning(lcFileActions) << "Invalid file record for path:" << _localPath;
+ return;
+ }
+
+ _fileId = fileRecord._fileId;
+
+ const auto mimeMatchMode = fileRecord.isVirtualFile() ? QMimeDatabase::MatchExtension
+ : QMimeDatabase::MatchDefault;
+ QMimeDatabase mimeDb;
+ const auto mimeType = mimeDb.mimeTypeForFile(_localPath, mimeMatchMode);
+ _mimeType = mimeType;
+
+ _fileIcon = _accountUrl + Activity::relativeServerFileTypeIconPath(_mimeType);
+}
+
+QMimeType FileActionsModel::mimeType() const
+{
+ return _mimeType;
+}
+
+QString FileActionsModel::fileIcon() const
+{
+ return _fileIcon;
+}
+
+QString FileActionsModel::responseLabel() const
+{
+ return _response.label;
+}
+
+void FileActionsModel::setResponseLabel(const QString &label)
+{
+ _response.label = label;
+}
+
+QString FileActionsModel::responseUrl() const
+{
+ return _response.url;
+}
+
+void FileActionsModel::setResponseUrl(const QString &url)
+{
+ _response.url = url;
+}
+
+void FileActionsModel::setResponse(const Response &response)
+{
+ _response = response;
+ Q_EMIT responseChanged();
+}
+
+void FileActionsModel::parseEndpoints()
+{
+ if (!_accountState->isConnected()) {
+ qCWarning(lcFileActions) << "The account is not connected"
+ << _accountUrl;
+ setResponse({ tr("Your account is offline %1.", "account url").arg(_accountUrl),
+ _accountUrl });
+ return;
+ }
+
+ if (_fileId.isEmpty()) {
+ qCWarning(lcFileActions) << "The file id is empty, not initialized"
+ << _localPath;
+ setResponse({ tr("The file id is empty for %1.", "file name").arg(_localPath),
+ _accountUrl });
+ return;
+ }
+
+ if (!_mimeType.isValid()) {
+ qCWarning(lcFileActions) << "The mime type found for the file is not valid"
+ << _localPath;
+ setResponse({ tr("The file type for %1 is not valid.", "file name").arg(_localPath),
+ _accountUrl });
+ return;
+ }
+
+ const auto contextMenuList = _accountState->account()->capabilities().fileActionsByMimeType(_mimeType);
+ if (contextMenuList.isEmpty()) {
+ qCWarning(lcFileActions) << "contextMenuByMimeType is empty, nothing was returned by capabilities"
+ << _localPath;
+ setResponse({ tr("No file actions were returned by the server for %1 files.", "file mymetype")
+ .arg(_mimeType.filterString()),
+ _accountUrl });
+ return;
+ }
+
+ for (const auto &contextMenu : contextMenuList) {
+ QueryList queryList;
+ const auto paramsMap = contextMenu.value("params").toMap();
+ for (auto param = paramsMap.cbegin(), end = paramsMap.cend(); param != end; ++param) {
+ const auto name = param.key();
+ QByteArray value;
+ if (name == fileIdC) {
+ value = _fileId;
+ }
+
+ if (param.key() == filePathC) {
+ value = _filePath.toUtf8();
+ }
+
+ if (!value.isEmpty()) {
+ queryList.append( QueryItem{ name, value } );
+ }
+ }
+
+ _fileActions.append({ parseIcon(contextMenu.value("icon").toString()),
+ contextMenu.value("name").toString(),
+ contextMenu.value("url").toString(),
+ contextMenu.value("method").toString(),
+ queryList });
+ }
+
+ qCDebug(lcFileActions) << "File" << _localPath << "has"
+ << _fileActions.size()
+ << "actions available.";
+ Q_EMIT fileActionModelChanged();
+}
+
+QString FileActionsModel::parseUrl(const QString &url) const
+{
+ auto parsedUrl = url;
+ parsedUrl.replace(fileIdUrlC, _fileId);
+ return parsedUrl;
+}
+
+QString FileActionsModel::parseIcon(const QString &icon) const
+{
+ if (icon.isEmpty()) {
+ return QStringLiteral("image://svgimage-custom-color/convert_to_text.svg/");
+ }
+
+ return _accountUrl + icon;
+}
+
+void FileActionsModel::createRequest(const int row)
+{
+ if (!_accountState) {
+ qCWarning(lcFileActions) << "No account state for"
+ << _localPath;
+ return;
+ }
+
+ const auto requesturl = parseUrl(_fileActions.at(row).url);
+ auto job = new JsonApiJob(_accountState->account(),
+ requesturl,
+ this);
+ connect(job, &JsonApiJob::jsonReceived,
+ this, &FileActionsModel::processRequest);
+ for (const auto ¶m : std::as_const(_fileActions.at(row).params)) {
+ QUrlQuery query;
+ query.addQueryItem(param.name, param.value);
+ job->addQueryParams(query);
+ }
+ const auto verb = job->stringToVerb(_fileActions.at(row).method);
+ job->setVerb(verb);
+ job->setProperty(rowC, row);
+ job->start();
+}
+
+void FileActionsModel::processRequest(const QJsonDocument &json, int statusCode)
+{
+ const auto row = sender()->property(rowC).toInt();
+ const auto fileAction = _fileActions.at(row).name;
+ const auto errorMessage = tr("%1 did not succeed, please try again later. "
+ "If you need help, contact your server administrator.",
+ "file action error message").arg(fileAction);
+ if (statusCode != 200) {
+ qCWarning(lcFileActions) << "File action did not succeed for" << _localPath;
+ setResponse({ errorMessage, _accountUrl });
+ return;
+ }
+
+ const auto root = json.object().value(QStringLiteral("root")).toObject();
+ const auto folderForPath = FolderMan::instance()->folderForPath(_localPath);
+ const auto remoteFolderPath = _accountUrl + folderForPath->remotePath();
+ const auto successMessage = tr("%1 done.", "file action success message").arg(fileAction);
+ if (root.empty()) {
+ setResponse({ successMessage, remoteFolderPath });
+ return;
+ }
+
+ const auto rows = root.value(QStringLiteral("rows")).toArray();
+ if (rows.empty()) {
+ setResponse({ successMessage, remoteFolderPath });
+ return;
+ }
+
+ for (const auto &rowValue : rows) {
+ const auto row = rowValue.toObject();
+ const auto children = row.value("children").toArray();
+ for (const auto &childValue : children) {
+ const auto child = childValue.toObject();
+ setResponse({ child.value(QStringLiteral("element")).toString(),
+ _accountUrl + child.value(QStringLiteral("url")).toString() });
+ }
+ }
+}
+
+} // namespace OCC
diff --git a/src/gui/integration/fileactionsmodel.h b/src/gui/integration/fileactionsmodel.h
new file mode 100644
index 0000000000000..222478a62479d
--- /dev/null
+++ b/src/gui/integration/fileactionsmodel.h
@@ -0,0 +1,115 @@
+/*
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#pragma once
+
+#include
+#include
+
+#include "accountstate.h"
+
+namespace OCC {
+
+Q_DECLARE_LOGGING_CATEGORY(lcFileActions)
+
+class FileActionsModel : public QAbstractListModel {
+ Q_OBJECT
+
+ Q_PROPERTY(AccountState* accountState READ accountState WRITE setAccountState NOTIFY accountStateChanged)
+ Q_PROPERTY(QString localPath READ localPath WRITE setLocalPath NOTIFY fileChanged)
+ Q_PROPERTY(QString fileIcon READ fileIcon NOTIFY fileChanged)
+ Q_PROPERTY(QString responseLabel READ responseLabel WRITE setResponseLabel NOTIFY responseChanged)
+ Q_PROPERTY(QString responseUrl READ responseUrl WRITE setResponseUrl NOTIFY responseChanged)
+
+public:
+ explicit FileActionsModel(QObject *const parent = nullptr);
+ [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;
+ [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ [[nodiscard]] QHash roleNames() const override;
+
+ enum DataRole {
+ FileActionIconRole = Qt::UserRole + 1,
+ FileActionNameRole,
+ FileActionUrlRole,
+ FileActionMethodRole,
+ FileActionParamsRole,
+ FileActionResponseLabelRole,
+ FileActionResponseUrlRole
+ };
+ Q_ENUM(DataRole)
+
+ struct Response {
+ QString label;
+ QString url;
+ };
+
+ struct QueryItem {
+ QString name;
+ QByteArray value;
+ };
+ using QueryList = QList;
+
+ struct FileAction {
+ QString icon;
+ QString name;
+ QString url;
+ QString method;
+ QueryList params;
+ };
+ using ActionList = QList;
+
+ [[nodiscard]] AccountState *accountState() const;
+ void setAccountState(AccountState *accountState);
+
+ [[nodiscard]] QString localPath() const;
+ void setLocalPath(const QString &localPath);
+
+ [[nodiscard]] QByteArray fileId() const;
+ [[nodiscard]] QMimeType mimeType() const;
+ [[nodiscard]] QString fileIcon() const;
+ void setupFileProperties();
+
+ [[nodiscard]] QString responseLabel() const;
+ void setResponseLabel(const QString &label);
+
+ [[nodiscard]] QString responseUrl() const;
+ void setResponseUrl(const QString &url);
+
+ void setResponse(const Response &response);
+
+ void parseEndpoints();
+ [[nodiscard]] QString parseUrl(const QString &url) const;
+ [[nodiscard]] QString parseIcon(const QString &icon) const;
+
+signals:
+ void accountStateChanged();
+ void fileChanged();
+ void responseChanged();
+ void fileActionModelChanged();
+
+public slots:
+ void createRequest(const int row);
+ void processRequest(const QJsonDocument &json, int statusCode);
+
+private:
+ Response _response;
+ ActionList _fileActions;
+ AccountState *_accountState;
+ QString _localPath;
+ QByteArray _fileId;
+ QMimeType _mimeType;
+ QString _filePath;
+ QString _accountUrl;
+ QString _fileIcon;
+
+ static constexpr char fileIdUrlC[] = "{fileId}";
+ static constexpr char fileIdC[] = "fileId";
+ static constexpr char filePathC[] = "filePath";
+ static constexpr char rowC[] = "row";
+};
+}
+
+Q_DECLARE_METATYPE(OCC::FileActionsModel::QueryList)
+Q_DECLARE_METATYPE(OCC::FileActionsModel::ActionList)
diff --git a/src/gui/owncloudgui.cpp b/src/gui/owncloudgui.cpp
index 68432164f2b1a..1fe337b4433d4 100644
--- a/src/gui/owncloudgui.cpp
+++ b/src/gui/owncloudgui.cpp
@@ -33,6 +33,7 @@
#include "tray/sortedactivitylistmodel.h"
#include "tray/syncstatussummary.h"
#include "tray/unifiedsearchresultslistmodel.h"
+#include "integration/fileactionsmodel.h"
#include "filesystem.h"
#ifdef WITH_LIBCLOUDPROVIDERS
@@ -135,6 +136,7 @@ ownCloudGui::ownCloudGui(Application *parent)
qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "ShareeModel");
qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "SortedShareModel");
qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "SyncConflictsModel");
+ qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "FileActionsModel");
qmlRegisterUncreatableType("com.nextcloud.desktopclient", 1, 0, "QAbstractItemModel", "QAbstractItemModel");
qmlRegisterUncreatableType("com.nextcloud.desktopclient", 1, 0, "activity", "Activity");
@@ -716,4 +718,9 @@ void ownCloudGui::slotShowFileActivityDialog(const QString &localPath) const
_tray->createFileActivityDialog(localPath);
}
+void ownCloudGui::slotShowFileActionsDialog(const QString &localPath) const
+{
+ _tray->showFileActionsDialog(localPath);
+}
+
} // end namespace
diff --git a/src/gui/owncloudgui.h b/src/gui/owncloudgui.h
index 40006a905c190..187fbbee0271e 100644
--- a/src/gui/owncloudgui.h
+++ b/src/gui/owncloudgui.h
@@ -95,6 +95,7 @@ public slots:
*/
void slotShowShareDialog(const QString &localPath) const;
void slotShowFileActivityDialog(const QString &localPath) const;
+ void slotShowFileActionsDialog(const QString &localPath) const;
void slotNewAccountWizard();
private slots:
diff --git a/src/gui/socketapi/socketapi.cpp b/src/gui/socketapi/socketapi.cpp
index 58efdddaa7c69..c2d2120944b07 100644
--- a/src/gui/socketapi/socketapi.cpp
+++ b/src/gui/socketapi/socketapi.cpp
@@ -615,6 +615,12 @@ void SocketApi::processLeaveShareRequest(const QString &localFile, SocketListene
FolderMan::instance()->leaveShare(QDir::fromNativeSeparators(localFile));
}
+void SocketApi::processFileActionsRequest(const QString &localFile)
+{
+ const auto fileData = FileData::get(localFile);
+ emit fileActionsCommandReceived(fileData.localPath);
+}
+
void SocketApi::broadcastStatusPushMessage(const QString &systemPath, SyncFileStatus fileStatus)
{
QString msg = buildMessage(QLatin1String("STATUS"), systemPath, fileStatus.toSocketAPIString());
@@ -723,6 +729,13 @@ void SocketApi::command_EDIT(const QString &localFile, SocketListener *listener)
job->start();
}
+void SocketApi::command_FILE_ACTIONS(const QString &localFile, SocketListener *listener)
+{
+ Q_UNUSED(listener);
+
+ processFileActionsRequest(localFile);
+}
+
// don't pull the share manager into socketapi unittests
#ifndef OWNCLOUD_TEST
@@ -1114,8 +1127,9 @@ void OCC::SocketApi::openPrivateLink(const QString &link)
void SocketApi::command_GET_STRINGS(const QString &argument, SocketListener *listener)
{
- static std::array, 6> strings { {
+ static std::array, 7> strings { {
{ "SHARE_MENU_TITLE", tr("Share options") },
+ { "FILE_ACTIONS_MENU_TITLE", tr("File actions") },
{ "FILE_ACTIVITY_MENU_TITLE", tr("Activity") },
{ "CONTEXT_MENU_TITLE", Theme::instance()->appNameGUI() },
{ "COPY_PRIVATE_LINK_MENU_TITLE", tr("Copy private link to clipboard") },
@@ -1164,6 +1178,23 @@ void SocketApi::sendSharingContextMenuOptions(const FileData &fileData, SocketLi
//listener->sendMessage(QLatin1String("MENU_ITEM:EMAIL_PRIVATE_LINK") + flagString + tr("Send private link by email …"));
}
+void SocketApi::sendFileActionsContextMenuOptions(const FileData &fileData, SocketListener *listener)
+{
+ const auto record = fileData.journalRecord();
+ const auto isOnTheServer = record.isValid();
+ auto serverHasIntegration = false;
+ if (const auto folder = fileData.folder;folder) {
+ if (const auto accountState = folder->accountState();
+ accountState && accountState->account()) {
+ serverHasIntegration = accountState->account()->serverHasIntegration();
+ }
+ }
+
+ const auto flagString = isOnTheServer && serverHasIntegration ? QLatin1String("::")
+ : QLatin1String(":d:");
+ listener->sendMessage(QLatin1String("MENU_ITEM:FILE_ACTIONS") + flagString + tr("File actions"));
+}
+
void SocketApi::sendEncryptFolderCommandMenuEntries(const QFileInfo &fileInfo,
const FileData &fileData,
const bool isE2eEncryptedPath,
@@ -1348,6 +1379,7 @@ void SocketApi::command_GET_MENU_ITEMS(const QString &argument, OCC::SocketListe
const auto itemEncryptionFlag = isE2eEncryptedPath ? SharingContextItemEncryptedFlag::EncryptedItem : SharingContextItemEncryptedFlag::NotEncryptedItem;
const auto rootE2eeFolderFlag = isE2eEncryptedRootFolder ? SharingContextItemRootEncryptedFolderFlag::RootEncryptedFolder : SharingContextItemRootEncryptedFolderFlag::NonRootEncryptedFolder;
sendSharingContextMenuOptions(fileData, listener, itemEncryptionFlag, rootE2eeFolderFlag);
+ sendFileActionsContextMenuOptions(fileData, listener);
// Conflict files get conflict resolution actions
bool isConflict = Utility::isConflictFile(fileData.folderRelativePath);
diff --git a/src/gui/socketapi/socketapi.h b/src/gui/socketapi/socketapi.h
index 5dcf6900e8c04..98c86b659abdc 100644
--- a/src/gui/socketapi/socketapi.h
+++ b/src/gui/socketapi/socketapi.h
@@ -64,6 +64,7 @@ public slots:
signals:
void shareCommandReceived(const QString &localPath);
void fileActivityCommandReceived(const QString &localPath);
+ void fileActionsCommandReceived(const QString &localPath);
private slots:
void slotNewConnection();
@@ -107,6 +108,7 @@ private slots:
void processLeaveShareRequest(const QString &localFile, SocketListener *listener);
void processFileActivityRequest(const QString &localFile);
void processEncryptRequest(const QString &localFile);
+ void processFileActionsRequest(const QString &localFile);
Q_INVOKABLE void command_RETRIEVE_FOLDER_STATUS(const QString &argument, OCC::SocketListener *listener);
Q_INVOKABLE void command_RETRIEVE_FILE_STATUS(const QString &argument, OCC::SocketListener *listener);
@@ -131,6 +133,7 @@ private slots:
Q_INVOKABLE void command_MOVE_ITEM(const QString &localFile, OCC::SocketListener *listener);
Q_INVOKABLE void command_LOCK_FILE(const QString &localFile, OCC::SocketListener *listener);
Q_INVOKABLE void command_UNLOCK_FILE(const QString &localFile, OCC::SocketListener *listener);
+ Q_INVOKABLE void command_FILE_ACTIONS(const QString &localFile, OCC::SocketListener *listener);
void setFileLock(const QString &localFile, const SyncFileItem::LockStatus lockState) const;
@@ -149,7 +152,7 @@ private slots:
// Sends the context menu options relating to sharing to listener
void sendSharingContextMenuOptions(const FileData &fileData, SocketListener *listener, SharingContextItemEncryptedFlag itemEncryptionFlag, SharingContextItemRootEncryptedFolderFlag rootE2eeFolderFlag);
-
+ void sendFileActionsContextMenuOptions(const FileData &fileData, SocketListener *listener);
void sendEncryptFolderCommandMenuEntries(const QFileInfo &fileInfo,
const FileData &fileData,
const bool isE2eEncryptedPath,
diff --git a/src/gui/systray.cpp b/src/gui/systray.cpp
index e9fc7564b75b4..39ba5a42b66e4 100644
--- a/src/gui/systray.cpp
+++ b/src/gui/systray.cpp
@@ -436,6 +436,51 @@ void Systray::createFileActivityDialog(const QString &localPath)
Q_EMIT showFileDetailsPage(localPath, FileDetailsPage::Activity);
}
+void Systray::showFileActionsDialog(const QString &localPath)
+{
+ createFileActionsDialog(localPath);
+}
+
+void Systray::createFileActionsDialog(const QString &localPath)
+{
+ if (!_trayEngine) {
+ qCWarning(lcSystray) << "Could not open file actions dialog for" << localPath << "as no tray engine was available";
+ return;
+ }
+
+ const auto folder = FolderMan::instance()->folderForPath(localPath);
+ if (!folder) {
+ qCWarning(lcSystray) << "Could not open file actions dialog for" << localPath << "no responsible folder found";
+ return;
+ }
+
+ QQmlComponent fileActionsQml(trayEngine(), QStringLiteral("qrc:/qml/src/gui/integration/FileActionsWindow.qml"));
+ if (fileActionsQml.isError()) {
+ qCWarning(lcSystray) << fileActionsQml.errorString();
+ qCWarning(lcSystray) << fileActionsQml.errors();
+ return;
+ }
+
+ QFileInfo localFile{localPath};
+ const auto shortLocalPath = localFile.fileName();
+ const QVariantMap initialProperties{
+ {"accountState", QVariant::fromValue(folder->accountState())},
+ {"shortLocalPath", shortLocalPath},
+ {"localPath", localPath},
+ };
+
+ const auto fileActionsDialog = fileActionsQml.createWithInitialProperties(initialProperties);
+ const auto dialog = qobject_cast(fileActionsDialog);
+ if (!dialog) {
+ qCWarning(lcSystray) << "File Actions dialog window resulted in creation of object that was not a window!";
+ return;
+ }
+
+ dialog->show();
+ dialog->raise();
+ dialog->requestActivate();
+}
+
void Systray::presentShareViewInTray(const QString &localPath)
{
const auto folder = FolderMan::instance()->folderForPath(localPath);
@@ -448,6 +493,12 @@ void Systray::presentShareViewInTray(const QString &localPath)
Q_EMIT showFileDetails(folder->accountState(), localPath, FileDetailsPage::Sharing);
}
+void Systray::presentFileActionsViewInSystray(const QString &localPath)
+{
+ qCDebug(lcSystray) << "Opening file actions view in tray for " << localPath;
+ createFileActionsDialog(localPath);
+}
+
void Systray::slotCurrentUserChanged()
{
if (_trayEngine) {
diff --git a/src/gui/systray.h b/src/gui/systray.h
index 87f4a6b196ccd..4cc3e4b0a2b65 100644
--- a/src/gui/systray.h
+++ b/src/gui/systray.h
@@ -146,8 +146,10 @@ public slots:
void createShareDialog(const QString &localPath);
void createFileActivityDialog(const QString &localPath);
+ void showFileActionsDialog(const QString &localPath);
void presentShareViewInTray(const QString &localPath);
+ void presentFileActionsViewInSystray(const QString &localPath);
private slots:
void slotUpdateSyncPausedState();
@@ -165,6 +167,7 @@ private slots:
void setupContextMenu();
void createFileDetailsDialog(const QString &localPath);
+ void createFileActionsDialog(const QString &localPath);
[[nodiscard]] QScreen *currentScreen() const;
[[nodiscard]] QRect currentScreenRect() const;
diff --git a/src/gui/tray/ActivityItemContent.qml b/src/gui/tray/ActivityItemContent.qml
index 2260ed8e6e296..f1b09c765e0ef 100644
--- a/src/gui/tray/ActivityItemContent.qml
+++ b/src/gui/tray/ActivityItemContent.qml
@@ -185,7 +185,29 @@ RowLayout {
display: Button.IconOnly
visible: model.showFileDetails
- onClicked: Systray.presentShareViewInTray(model.openablePath)
+ onClicked: fileMoreButtonMenu.visible ? fileMoreButtonMenu.close() : fileMoreButtonMenu.popup()
+
+ AutoSizingMenu {
+ id: fileMoreButtonMenu
+ closePolicy: Menu.CloseOnPressOutsideParent | Menu.CloseOnEscape
+
+ MenuItem {
+ height: visible ? implicitHeight : 0
+ text: qsTr("File details")
+ font.pixelSize: Style.topLinePixelSize
+ hoverEnabled: true
+ onClicked: Systray.presentShareViewInTray(model.openablePath)
+ }
+
+ MenuItem {
+ visible: model.serverHasIntegration
+ height: visible ? implicitHeight : 0
+ text: qsTr("File actions")
+ font.pixelSize: Style.topLinePixelSize
+ hoverEnabled: true
+ onClicked: Systray.presentFileActionsViewInSystray(model.openablePath)
+ }
+ }
}
Button {
diff --git a/src/gui/tray/activitydata.cpp b/src/gui/tray/activitydata.cpp
index c17b8db4ab75f..aeaad62a3a08b 100644
--- a/src/gui/tray/activitydata.cpp
+++ b/src/gui/tray/activitydata.cpp
@@ -177,50 +177,75 @@ OCC::Activity Activity::fromActivityJson(const QJsonObject &json, const AccountP
QString Activity::relativeServerFileTypeIconPath(const QMimeType &mimeType)
{
- if(mimeType.isValid() && mimeType.inherits("text/plain")) {
- return QStringLiteral("/index.php/apps/theming/img/core/filetypes/text.svg");
- } else if (mimeType.isValid() && mimeType.name().startsWith("image")) {
- return QStringLiteral("/index.php/apps/theming/img/core/filetypes/image.svg");
- } else if (mimeType.isValid() && mimeType.name().startsWith("audio")) {
- return QStringLiteral("/index.php/apps/theming/img/core/filetypes/audio.svg");
- } else if (mimeType.isValid() && mimeType.name().startsWith("video")) {
- return QStringLiteral("/index.php/apps/theming/img/core/filetypes/video.svg");
- } else if (mimeType.isValid() && (mimeType.inherits("application/vnd.oasis.opendocument.text") ||
- mimeType.inherits("application/msword") ||
- mimeType.inherits("application/vnd.openxmlformats-officedocument.wordprocessingml.document") ||
- mimeType.inherits("application/vnd.openxmlformats-officedocument.wordprocessingml.template")||
- mimeType.inherits("application/vnd.ms-word.document.macroEnabled.12") ||
- mimeType.inherits("application/vnd.ms-word.template.macroEnabled.12") ||
- mimeType.inherits("application/vnd.apple.pages"))) {
- return QStringLiteral("/index.php/apps/theming/img/core/filetypes/x-office-document.svg");
- } else if (mimeType.isValid() && mimeType.inherits("application/vnd.oasis.opendocument.graphics")) {
- return QStringLiteral("/index.php/apps/theming/img/core/filetypes/x-office-drawing.svg");
- } else if (mimeType.isValid() && (mimeType.inherits("application/vnd.oasis.opendocument.presentation") ||
- mimeType.inherits("application/vnd.ms-powerpoint") ||
- mimeType.inherits("application/vnd.openxmlformats-officedocument.presentationml.presentation") ||
- mimeType.inherits("application/vnd.openxmlformats-officedocument.presentationml.template") ||
- mimeType.inherits("application/vnd.openxmlformats-officedocument.presentationml.slideshow") ||
- mimeType.inherits("application/vnd.ms-powerpoint.addin.macroEnabled.12") ||
- mimeType.inherits("application/vnd.ms-powerpoint.presentation.macroEnabled.12") ||
- mimeType.inherits("application/vnd.ms-powerpoint.template.macroEnabled.12") ||
- mimeType.inherits("application/vnd.ms-powerpoint.slideshow.macroEnabled.12") ||
- mimeType.inherits("application/vnd.apple.keynote"))) {
- return QStringLiteral("/index.php/apps/theming/img/core/filetypes/x-office-presentation.svg");
- } else if (mimeType.isValid() && (mimeType.inherits("application/vnd.oasis.opendocument.spreadsheet") ||
- mimeType.inherits("application/vnd.ms-excel") ||
- mimeType.inherits("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") ||
- mimeType.inherits("application/vnd.openxmlformats-officedocument.spreadsheetml.template") ||
- mimeType.inherits("application/vnd.ms-excel.sheet.macroEnabled.12") ||
- mimeType.inherits("application/vnd.ms-excel.template.macroEnabled.12") ||
- mimeType.inherits("application/vnd.ms-excel.addin.macroEnabled.12") ||
- mimeType.inherits("application/vnd.ms-excel.sheet.binary.macroEnabled.12") ||
- mimeType.inherits("application/vnd.apple.numbers"))) {
- return QStringLiteral("/index.php/apps/theming/img/core/filetypes/x-office-document.svg");
- } else if (mimeType.isValid() && mimeType.inherits("application/pdf")) {
- return QStringLiteral("/index.php/apps/theming/img/core/filetypes/application-pdf.svg");
- } else {
- return QStringLiteral("/index.php/apps/theming/img/core/filetypes/file.svg");
+ const auto iconPath = QStringLiteral("/index.php/apps/theming/img/core/filetypes/");
+ const auto defaultIcon = iconPath + QStringLiteral("file.svg");
+ if (!mimeType.isValid()) {
+ return defaultIcon;
}
+
+ if (mimeType.inherits("text/plain")) {
+ return iconPath + QStringLiteral("text.svg");
+ }
+
+ if (mimeType.name().startsWith("image")) {
+ return iconPath + QStringLiteral("image.svg");
+ }
+
+ if (mimeType.name().startsWith("audio")) {
+ return iconPath + QStringLiteral("audio.svg");
+ }
+
+ if (mimeType.name().startsWith("video")) {
+ return iconPath + QStringLiteral("video.svg");
+ }
+
+ const auto isDocument = mimeType.inherits("application/vnd.oasis.opendocument.text") ||
+ mimeType.inherits("application/msword") ||
+ mimeType.inherits("application/vnd.openxmlformats-officedocument.wordprocessingml.document") ||
+ mimeType.inherits("application/vnd.openxmlformats-officedocument.wordprocessingml.template") ||
+ mimeType.inherits("application/vnd.ms-word.document.macroEnabled.12")||
+ mimeType.inherits("application/vnd.ms-word.template.macroEnabled.12") ||
+ mimeType.inherits("application/vnd.apple.pages");
+ if (isDocument) {
+ return iconPath + QStringLiteral("x-office-document.svg");
+ }
+
+ if (mimeType.inherits("application/vnd.oasis.opendocument.graphics")) {
+ return iconPath + QStringLiteral("x-office-drawing.svg");
+ }
+
+ const auto isPresentation = mimeType.inherits("application/vnd.oasis.opendocument.presentation") ||
+ mimeType.inherits("application/vnd.ms-powerpoint") ||
+ mimeType.inherits("application/vnd.openxmlformats-officedocument.presentationml.presentation") ||
+ mimeType.inherits("application/vnd.openxmlformats-officedocument.presentationml.template") ||
+ mimeType.inherits("application/vnd.openxmlformats-officedocument.presentationml.slideshow") ||
+ mimeType.inherits("application/vnd.ms-powerpoint.addin.macroEnabled.12") ||
+ mimeType.inherits("application/vnd.ms-powerpoint.presentation.macroEnabled.12") ||
+ mimeType.inherits("application/vnd.ms-powerpoint.template.macroEnabled.12") ||
+ mimeType.inherits("application/vnd.ms-powerpoint.slideshow.macroEnabled.12") ||
+ mimeType.inherits("application/vnd.apple.keynote");
+ if (isPresentation) {
+ return iconPath + QStringLiteral("x-office-presentation.svg");
+ }
+
+ const auto isSpreadsheet = mimeType.inherits("application/vnd.oasis.opendocument.spreadsheet") ||
+ mimeType.inherits("application/vnd.ms-excel") ||
+ mimeType.inherits("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") ||
+ mimeType.inherits("application/vnd.openxmlformats-officedocument.spreadsheetml.template") ||
+ mimeType.inherits("application/vnd.ms-excel.sheet.macroEnabled.12") ||
+ mimeType.inherits("application/vnd.ms-excel.template.macroEnabled.12") ||
+ mimeType.inherits("application/vnd.ms-excel.addin.macroEnabled.12") ||
+ mimeType.inherits("application/vnd.ms-excel.sheet.binary.macroEnabled.12") ||
+ mimeType.inherits("application/vnd.apple.numbers");
+ if (isSpreadsheet) {
+ return iconPath + QStringLiteral("x-office-document.svg");
+ }
+
+ if (mimeType.inherits("application/pdf")) {
+ return iconPath + QStringLiteral("application-pdf.svg");
+ }
+
+ return defaultIcon;
}
}
diff --git a/src/gui/tray/activitylistmodel.cpp b/src/gui/tray/activitylistmodel.cpp
index 45f319b5e1931..20ece0431aeb6 100644
--- a/src/gui/tray/activitylistmodel.cpp
+++ b/src/gui/tray/activitylistmodel.cpp
@@ -83,6 +83,7 @@ QHash ActivityListModel::roleNames() const
roles[TalkNotificationUserAvatarRole] = "userAvatar";
roles[ActivityIndexRole] = "activityIndex";
roles[ActivityRole] = "activity";
+ roles[ActivityIntegrationRole] = "serverHasIntegration";
return roles;
}
@@ -140,19 +141,21 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
{
Q_ASSERT(checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid | QAbstractItemModel::CheckIndexOption::ParentIsInvalid));
- const auto a = _finalList.at(index.row());
- AccountStatePtr ast = AccountManager::instance()->account(a._accName);
- if (!ast && _accountState != ast.data())
+ const auto activity = _finalList.at(index.row());
+ const auto accountState = AccountManager::instance()->account(activity._accName);
+ if (!accountState && _accountState != accountState.data()) {
return QVariant();
-
+ }
const auto getFilePath = [&]() {
- const auto fileName = a._fileAction == QStringLiteral("file_renamed") ? a._renamedFile : a._file;
+ const auto fileName = activity._fileAction == QStringLiteral("file_renamed") ? activity._renamedFile
+ : activity._file;
if (!fileName.isEmpty()) {
- const auto folder = FolderMan::instance()->folder(a._folder);
+ const auto folder = FolderMan::instance()->folder(activity._folder);
- const QString relPath = folder ? folder->remotePathTrailingSlash() + fileName : fileName;
+ const QString relPath = folder ? folder->remotePathTrailingSlash() + fileName
+ : fileName;
- const auto localFiles = FolderMan::instance()->findFileInLocalFolders(relPath, ast->account());
+ const auto localFiles = FolderMan::instance()->findFileInLocalFolders(relPath, accountState->account());
if (localFiles.isEmpty()) {
return QString();
@@ -171,16 +174,17 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
return localFiles.constFirst();
}
+
return QString();
};
- const auto getDisplayPath = [&a, &ast]() {
- if (!a._file.isEmpty()) {
- const auto folder = FolderMan::instance()->folder(a._folder);
+ const auto getDisplayPath = [&activity, &accountState]() {
+ if (!activity._file.isEmpty()) {
+ const auto folder = FolderMan::instance()->folder(activity._folder);
- QString relPath = folder ? folder->remotePathTrailingSlash() + a._file : a._file;
+ QString relPath = folder ? folder->remotePathTrailingSlash() + activity._file : activity._file;
- const auto localFiles = FolderMan::instance()->findFileInLocalFolders(relPath, ast->account());
+ const auto localFiles = FolderMan::instance()->findFileInLocalFolders(relPath, accountState->account());
if (localFiles.count() > 0) {
if (relPath.startsWith('/') || relPath.startsWith('\\')) {
@@ -221,44 +225,47 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
const auto generateIconPath = [&]() {
auto colorIconPath = QStringLiteral("image://svgimage-custom-color/%1");
- if (a._type == Activity::NotificationType && !a._talkNotificationData.userAvatar.isEmpty()) {
+ if (activity._type == Activity::NotificationType && !activity._talkNotificationData.userAvatar.isEmpty()) {
return QStringLiteral("image://svgimage-custom-color/talk-bordered.svg");
- } else if (a._type == Activity::SyncResultType) {
+ } else if (activity._type == Activity::SyncResultType) {
return colorIconPath.arg("error.svg");
- } else if (a._type == Activity::SyncFileItemType) {
- if (a._syncFileItemStatus == SyncFileItem::NormalError
- || a._syncFileItemStatus == SyncFileItem::FatalError
- || a._syncFileItemStatus == SyncFileItem::DetailError
- || a._syncFileItemStatus == SyncFileItem::BlacklistedError) {
+ } else if (activity._type == Activity::SyncFileItemType) {
+ if (activity._syncFileItemStatus == SyncFileItem::NormalError
+ || activity._syncFileItemStatus == SyncFileItem::FatalError
+ || activity._syncFileItemStatus == SyncFileItem::DetailError
+ || activity._syncFileItemStatus == SyncFileItem::BlacklistedError) {
return colorIconPath.arg("error.svg");
- } else if (a._syncFileItemStatus == SyncFileItem::SoftError
- || a._syncFileItemStatus == SyncFileItem::Conflict
- || a._syncFileItemStatus == SyncFileItem::Restoration
- || a._syncFileItemStatus == SyncFileItem::FileLocked
- || a._syncFileItemStatus == SyncFileItem::FileNameInvalid
- || a._syncFileItemStatus == SyncFileItem::FileNameInvalidOnServer
- || a._syncFileItemStatus == SyncFileItem::FileNameClash) {
+ } else if (activity._syncFileItemStatus == SyncFileItem::SoftError
+ || activity._syncFileItemStatus == SyncFileItem::Conflict
+ || activity._syncFileItemStatus == SyncFileItem::Restoration
+ || activity._syncFileItemStatus == SyncFileItem::FileLocked
+ || activity._syncFileItemStatus == SyncFileItem::FileNameInvalid
+ || activity._syncFileItemStatus == SyncFileItem::FileNameInvalidOnServer
+ || activity._syncFileItemStatus == SyncFileItem::FileNameClash) {
return colorIconPath.arg("warning.svg");
- } else if (a._syncFileItemStatus == SyncFileItem::FileIgnored) {
+ } else if (activity._syncFileItemStatus == SyncFileItem::FileIgnored) {
return colorIconPath.arg("info.svg");
} else {
// File sync successful
- if (a._fileAction == "file_created") {
- return a._previews.empty() ? colorIconPath.arg("add.svg") : colorIconPath.arg("add-bordered.svg");
- } else if (a._fileAction == "file_deleted") {
- return a._previews.empty() ? colorIconPath.arg("delete.svg") : colorIconPath.arg("delete-bordered.svg");
+ if (activity._fileAction == "file_created") {
+ return activity._previews.empty() ? colorIconPath.arg("add.svg")
+ : colorIconPath.arg("add-bordered.svg");
+ } else if (activity._fileAction == "file_deleted") {
+ return activity._previews.empty() ? colorIconPath.arg("delete.svg")
+ : colorIconPath.arg("delete-bordered.svg");
} else {
- return a._previews.empty() ? colorIconPath.arg("change.svg") : colorIconPath.arg("change-bordered.svg");
+ return activity._previews.empty() ? colorIconPath.arg("change.svg")
+ : colorIconPath.arg("change-bordered.svg");
}
}
} else {
// We have an activity
- if (a._icon.isEmpty()) {
+ if (activity._icon.isEmpty()) {
return colorIconPath.arg("activity.svg");
}
// using tray-image-provider here as it can read from URLs
- return QStringLiteral("image://tray-image-provider/%1").arg(a._icon);
+ return QStringLiteral("image://tray-image-provider/%1").arg(activity._icon);
}
};
@@ -273,30 +280,30 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
return displayLocation();
case ActionsLinksRole: {
QList customList;
- for (const auto &activityLink : std::as_const(a._links)) {
+ for (const auto &activityLink : std::as_const(activity._links)) {
customList << QVariant::fromValue(activityLink);
}
return customList;
}
case ActionsLinksContextMenuRole: {
- return ActivityListModel::convertLinksToMenuEntries(a);
+ return ActivityListModel::convertLinksToMenuEntries(activity);
}
case ActionsLinksForActionButtonsRole: {
- return ActivityListModel::convertLinksToActionButtons(a);
+ return ActivityListModel::convertLinksToActionButtons(activity);
}
case IconRole:
return generateIconPath();
case ObjectTypeRole:
- return a._objectType;
+ return activity._objectType;
case ObjectIdRole:
- return a._objectId;
+ return activity._objectId;
case ObjectNameRole:
- return a._objectName;
+ return activity._objectName;
case ActionRole: {
- switch (a._type) {
+ switch (activity._type) {
case Activity::ActivityType:
case Activity::DummyFetchingActivityType:
case Activity::DummyMoreActivitiesAvailableType:
@@ -312,71 +319,73 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
break;
}
case ActionTextRole:
- if(a._subjectDisplay.isEmpty()) {
- return a._subject;
+ if(activity._subjectDisplay.isEmpty()) {
+ return activity._subject;
}
- return a._subjectDisplay;
+ return activity._subjectDisplay;
case ActionTextColorRole:
- return a._id == -1 ? QLatin1String("#808080") : QLatin1String("#222"); // FIXME: This is a temporary workaround for _showMoreActivitiesAvailableEntry
+ return activity._id == -1 ? QLatin1String("#808080") : QLatin1String("#222"); // FIXME: This is a temporary workaround for _showMoreActivitiesAvailableEntry
case MessageRole:
- return a._message;
+ return activity._message;
case LinkRole: {
- if (a._link.isEmpty()) {
+ if (activity._link.isEmpty()) {
return "";
} else {
- return a._link.toString();
+ return activity._link.toString();
}
}
case AccountRole:
- return a._accName;
+ return activity._accName;
case PointInTimeRole:
//return a._id == -1 ? "" : QString("%1 - %2").arg(Utility::timeAgoInWords(a._dateTime.toLocalTime()), a._dateTime.toLocalTime().toString(Qt::DefaultLocaleShortDate));
- return a._id == -1 ? "" : Utility::timeAgoInWords(a._dateTime.toLocalTime());
+ return activity._id == -1 ? "" : Utility::timeAgoInWords(activity._dateTime.toLocalTime());
case AccountConnectedRole:
- return (ast && ast->isConnected());
+ return (accountState && accountState->isConnected());
case DisplayActions:
return _displayActions;
case ShowFileDetailsRole:
return _displayActions &&
- a._objectType == QStringLiteral("files") &&
- a._fileAction != "file_deleted" &&
- a._syncFileItemStatus != SyncFileItem::FileIgnored &&
+ activity._objectType == QStringLiteral("files") &&
+ activity._fileAction != "file_deleted" &&
+ activity._syncFileItemStatus != SyncFileItem::FileIgnored &&
!data(index, OpenablePathRole).toString().isEmpty();
case DismissableRole:
// Do not allow dismissal of things requiring user input regarding syncing
- return !a._links.isEmpty() &&
- a._syncFileItemStatus != SyncFileItem::FileNameClash &&
- a._syncFileItemStatus != SyncFileItem::Conflict &&
- a._syncFileItemStatus != SyncFileItem::FileNameInvalid &&
- a._syncFileItemStatus != SyncFileItem::FileNameInvalidOnServer;
+ return !activity._links.isEmpty() &&
+ activity._syncFileItemStatus != SyncFileItem::FileNameClash &&
+ activity._syncFileItemStatus != SyncFileItem::Conflict &&
+ activity._syncFileItemStatus != SyncFileItem::FileNameInvalid &&
+ activity._syncFileItemStatus != SyncFileItem::FileNameInvalidOnServer;
case IsCurrentUserFileActivityRole:
- return a._isCurrentUserFileActivity;
+ return activity._isCurrentUserFileActivity;
case ThumbnailRole: {
- if ((a._type == Activity::NotificationType || a._type == Activity::OpenSettingsNotificationType) &&
- !a._talkNotificationData.userAvatar.isEmpty()) {
- return generateAvatarThumbnailMap(a._talkNotificationData.userAvatar);
+ if ((activity._type == Activity::NotificationType || activity._type == Activity::OpenSettingsNotificationType) &&
+ !activity._talkNotificationData.userAvatar.isEmpty()) {
+ return generateAvatarThumbnailMap(activity._talkNotificationData.userAvatar);
}
- if(a._previews.empty()) {
+ if(activity._previews.empty()) {
return {};
}
- const auto preview = a._previews[0];
+ const auto preview = activity._previews[0];
return(generatePreviewMap(preview));
}
case TalkNotificationConversationTokenRole:
- return a._talkNotificationData.conversationToken;
+ return activity._talkNotificationData.conversationToken;
case TalkNotificationMessageIdRole:
- return a._talkNotificationData.messageId;
+ return activity._talkNotificationData.messageId;
case TalkNotificationMessageSentRole:
- return replyMessageSent(a);
+ return replyMessageSent(activity);
case TalkNotificationUserAvatarRole:
- return a._talkNotificationData.userAvatar;
+ return activity._talkNotificationData.userAvatar;
case ActivityIndexRole:
return index.row();
case ActivityRole:
- return QVariant::fromValue(a);
+ return QVariant::fromValue(activity);
+ case ActivityIntegrationRole:
+ return accountState->account()->serverHasIntegration();
}
return {};
diff --git a/src/gui/tray/activitylistmodel.h b/src/gui/tray/activitylistmodel.h
index d200baa6ed669..cd8ad12ad2a62 100644
--- a/src/gui/tray/activitylistmodel.h
+++ b/src/gui/tray/activitylistmodel.h
@@ -74,6 +74,7 @@ class ActivityListModel : public QAbstractListModel
TalkNotificationUserAvatarRole,
ActivityIndexRole,
ActivityRole,
+ ActivityIntegrationRole
};
Q_ENUM(DataRole)
diff --git a/src/libsync/account.cpp b/src/libsync/account.cpp
index d2d513ce53de7..0a7ad51ea8c2b 100644
--- a/src/libsync/account.cpp
+++ b/src/libsync/account.cpp
@@ -748,6 +748,7 @@ void Account::setCapabilities(const QVariantMap &caps)
updateServerColors();
updateServerSubcription();
updateDesktopEnterpriseChannel();
+ updateServerHasIntegration();
emit capabilitiesChanged();
@@ -1551,4 +1552,14 @@ void Account::setDownloadLimit(const unsigned int limit)
emit downloadLimitChanged();
}
+bool Account::serverHasIntegration() const
+{
+ return _serverHasIntegration;
+}
+
+void Account::updateServerHasIntegration()
+{
+ _serverHasIntegration = capabilities().serverHasClientIntegration();
+}
+
} // namespace OCC
diff --git a/src/libsync/account.h b/src/libsync/account.h
index 41bf120cf7b28..a7aa5b46b9004 100644
--- a/src/libsync/account.h
+++ b/src/libsync/account.h
@@ -438,6 +438,8 @@ class OWNCLOUDSYNC_EXPORT Account : public QObject
void setLastRootETag(const QByteArray &etag);
#endif
+ [[nodiscard]] bool serverHasIntegration() const;
+
public slots:
/// Used when forgetting credentials
void clearQNAMCache();
@@ -508,6 +510,7 @@ public slots:
void userCertificateNeedsMigrationChanged();
void rootFolderQuotaChanged(const int64_t &usedBytes, const int64_t &availableBytes);
+
protected Q_SLOTS:
void slotCredentialsFetched();
void slotCredentialsAsked();
@@ -604,6 +607,9 @@ private slots:
QByteArray _lastRootETag; // Runtime-only, not persisted
#endif
+ void updateServerHasIntegration();
+ bool _serverHasIntegration;
+
/* IMPORTANT - remove later - FIXME MS@2019-12-07 -->
* TODO: For "Log out" & "Remove account": Remove client CA certs and KEY!
*
diff --git a/src/libsync/capabilities.cpp b/src/libsync/capabilities.cpp
index 92bfa5d6d73ea..73b796fa2b212 100644
--- a/src/libsync/capabilities.cpp
+++ b/src/libsync/capabilities.cpp
@@ -437,6 +437,74 @@ QStringList Capabilities::forbiddenFilenameExtensions() const
return _capabilities["files"].toMap()["forbidden_filename_extensions"].toStringList();
}
+bool Capabilities::serverHasClientIntegration() const
+{
+ return !_capabilities[QStringLiteral("client_integration")].toMap().isEmpty();
+}
+
+QList Capabilities::fileActionsByMimeType(const QMimeType &fileMimeType) const
+{
+ const auto fileActionsMap = _capabilities.value("client_integration").toMap();
+ QVariantList contextMenuMapList;
+ for (const auto &fileContextMenu : fileActionsMap) {
+ const auto fileContextMenuMap = fileContextMenu.toMap();
+ if (!fileContextMenuMap.contains("context-menu")) {
+ continue;
+ }
+
+ contextMenuMapList.append(fileContextMenuMap.value("context-menu").toList());
+ }
+
+ if (contextMenuMapList.empty()) {
+ qCDebug(lcServerCapabilities) << "There is no context menu available in the capabilities.";
+ return {};
+ }
+
+ const auto fileMimeTypeName = fileMimeType.name();
+ qCDebug(lcServerCapabilities) << "Filtering file actions by mimeType:" << fileMimeTypeName;
+
+ QList fileActionsByMimeType;
+ for (const auto &contextMenu : std::as_const(contextMenuMapList)) {
+ const auto contextMenuMap = contextMenu.toMap();
+ const auto mimetypeFilters = contextMenuMap.value("mimetype_filters").toString();
+ const auto filesMimeTypeFilterList = mimetypeFilters.split(",", Qt::SkipEmptyParts);
+
+ if (filesMimeTypeFilterList.isEmpty()) {
+ qCDebug(lcServerCapabilities) << "Found file action for all mimetypes:" << contextMenuMap;
+ fileActionsByMimeType.append(contextMenuMap);
+ continue;
+ }
+
+ for (const auto &mimeType : filesMimeTypeFilterList) {
+ const auto capabilitiesMimeType = mimeType.trimmed();
+ QString mimeTypeAlias;
+ if (const auto capabilitiesMimeTypeSplit = capabilitiesMimeType.split("/");
+ !capabilitiesMimeTypeSplit.isEmpty()){
+ mimeTypeAlias = capabilitiesMimeTypeSplit.last();
+ }
+
+ qCDebug(lcServerCapabilities) << "Context menu for mimeType:" << mimeTypeAlias << capabilitiesMimeType;
+ qCDebug(lcServerCapabilities) << fileMimeTypeName << "inherits" << capabilitiesMimeType << "?"
+ << fileMimeType.inherits(capabilitiesMimeType);
+
+ if (!capabilitiesMimeType.isEmpty()
+ && !fileMimeTypeName.startsWith(capabilitiesMimeType)
+ && !fileMimeTypeName.contains(capabilitiesMimeType)
+ && !fileMimeType.inherits(capabilitiesMimeType) &&
+ !fileMimeTypeName.startsWith(mimeTypeAlias)
+ && !fileMimeTypeName.contains(mimeTypeAlias)
+ && !fileMimeType.inherits(mimeTypeAlias)) {
+ continue;
+ }
+
+ qCDebug(lcServerCapabilities) << "Found file action:" << contextMenuMap;
+ fileActionsByMimeType.append(contextMenuMap);
+ }
+ }
+
+ return fileActionsByMimeType;
+}
+
/*-------------------------------------------------------------------------------------*/
// Direct Editing
@@ -466,6 +534,7 @@ DirectEditor* Capabilities::getDirectEditorForOptionalMimetype(const QMimeType &
return nullptr;
}
+
/*-------------------------------------------------------------------------------------*/
DirectEditor::DirectEditor(const QString &id, const QString &name, QObject* parent)
diff --git a/src/libsync/capabilities.h b/src/libsync/capabilities.h
index 68569ab228d44..a06d4a1036761 100644
--- a/src/libsync/capabilities.h
+++ b/src/libsync/capabilities.h
@@ -175,6 +175,9 @@ class OWNCLOUDSYNC_EXPORT Capabilities
[[nodiscard]] bool serverHasValidSubscription() const;
[[nodiscard]] QString desktopEnterpriseChannel() const;
+ [[nodiscard]] bool serverHasClientIntegration() const;
+ [[nodiscard]] QList fileActionsByMimeType(const QMimeType &fileMimeType) const;
+
// Direct Editing
void addDirectEditor(DirectEditor* directEditor);
DirectEditor* getDirectEditorForMimetype(const QMimeType &mimeType);
@@ -184,7 +187,6 @@ class OWNCLOUDSYNC_EXPORT Capabilities
[[nodiscard]] QMap serverThemingMap() const;
QVariantMap _capabilities;
-
QList _directEditors;
};
diff --git a/src/libsync/networkjobs.cpp b/src/libsync/networkjobs.cpp
index 2b994774f9e3b..4d3b5403e24bb 100644
--- a/src/libsync/networkjobs.cpp
+++ b/src/libsync/networkjobs.cpp
@@ -1464,6 +1464,23 @@ QByteArray SimpleApiJob::verbToString() const
return "GET";
}
+SimpleApiJob::Verb SimpleApiJob::stringToVerb(const QString &verb) const
+{
+ if (verb == QStringLiteral("POST")) {
+ return Verb::Post;
+ }
+
+ if (verb == QStringLiteral("PUT")) {
+ return Verb::Put;
+ }
+
+ if (verb == QStringLiteral("DELETE")) {
+ return Verb::Delete;
+ }
+
+ return Verb::Get;
+}
+
void SimpleApiJob::start()
{
addRawHeader("OCS-APIREQUEST", "true");
diff --git a/src/libsync/networkjobs.h b/src/libsync/networkjobs.h
index 1134d49ecb246..4e5b9c485be9c 100644
--- a/src/libsync/networkjobs.h
+++ b/src/libsync/networkjobs.h
@@ -439,6 +439,8 @@ class OWNCLOUDSYNC_EXPORT SimpleApiJob : public AbstractNetworkJob
void setVerb(Verb value);
+ [[nodiscard]] Verb stringToVerb(const QString &verb) const;
+
/**
* @brief addQueryParams - add more parameters to the ocs call
* @param params: list pairs of strings containing the parameter name and the value.
diff --git a/theme.qrc.in b/theme.qrc.in
index 4bf77d90807bc..15581d0db340e 100644
--- a/theme.qrc.in
+++ b/theme.qrc.in
@@ -291,5 +291,8 @@
theme/chevron-double-up.svg
theme/call-notification.wav
theme/info.svg
+ theme/file-open.svg
+ theme/backup.svg
+ theme/convert_to_text.svg
diff --git a/theme/Style/Style.qml b/theme/Style/Style.qml
index 98856f8c8e126..07d80f19e53f7 100644
--- a/theme/Style/Style.qml
+++ b/theme/Style/Style.qml
@@ -50,6 +50,8 @@ QtObject {
property int trayHorizontalMargin: 10
property int trayModalWidth: 380
property int trayModalHeight: 490
+ property int filesActionsWidth: 380
+ property int filesActionsHeight: 350
property int trayListItemIconSize: accountAvatarSize
property int trayDrawerMargin: trayWindowHeaderHeight
property real thumbnailImageSizeReduction: 0.2 // We reserve some space within the thumbnail "item", here about 20%.
diff --git a/theme/backup.svg b/theme/backup.svg
new file mode 100644
index 0000000000000..696aaba720e95
--- /dev/null
+++ b/theme/backup.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/theme/convert_to_text.svg b/theme/convert_to_text.svg
new file mode 100644
index 0000000000000..62427bc8f8068
--- /dev/null
+++ b/theme/convert_to_text.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/theme/file-open.svg b/theme/file-open.svg
new file mode 100644
index 0000000000000..871c7217e7137
--- /dev/null
+++ b/theme/file-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file