From 41aa7adc3f25c1dc8946f354e4d8391274c3324b Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Mon, 23 Jun 2025 15:49:56 +0200 Subject: [PATCH 01/22] feat(socketapii): add file actions option to list of options for files. - declarativeui: add endpoints model. - capabilities: fetch endpoints in capabilities. - display endpoints in an application window. - list file actions in context menu. - remove EndpointModel from DeclarativeUi class. - list declarative ui and file actions in the tray activitiy. - add icon and filter to endpoint model and UI. Signed-off-by: Camila Ayres --- resources.qrc | 2 + src/gui/CMakeLists.txt | 6 + src/gui/application.cpp | 6 + src/gui/declarativeui/DeclarativeUiWindow.qml | 90 +++++++++ src/gui/declarativeui/FileActionsWindow.qml | 106 +++++++++++ src/gui/declarativeui/declarativeui.cpp | 68 +++++++ src/gui/declarativeui/declarativeui.h | 51 +++++ src/gui/declarativeui/declarativeuimodel.cpp | 108 +++++++++++ src/gui/declarativeui/declarativeuimodel.h | 57 ++++++ src/gui/declarativeui/endpointmodel.cpp | 174 ++++++++++++++++++ src/gui/declarativeui/endpointmodel.h | 79 ++++++++ src/gui/owncloudgui.cpp | 13 ++ src/gui/owncloudgui.h | 2 + src/gui/socketapi/socketapi.cpp | 22 ++- src/gui/socketapi/socketapi.h | 5 + src/gui/systray.cpp | 95 ++++++++++ src/gui/systray.h | 6 + src/gui/tray/ActivityItemContent.qml | 31 +++- src/libsync/capabilities.cpp | 13 ++ src/libsync/capabilities.h | 4 +- 20 files changed, 935 insertions(+), 3 deletions(-) create mode 100644 src/gui/declarativeui/DeclarativeUiWindow.qml create mode 100644 src/gui/declarativeui/FileActionsWindow.qml create mode 100644 src/gui/declarativeui/declarativeui.cpp create mode 100644 src/gui/declarativeui/declarativeui.h create mode 100644 src/gui/declarativeui/declarativeuimodel.cpp create mode 100644 src/gui/declarativeui/declarativeuimodel.h create mode 100644 src/gui/declarativeui/endpointmodel.cpp create mode 100644 src/gui/declarativeui/endpointmodel.h diff --git a/resources.qrc b/resources.qrc index 2681104929615..0ce9ba9810fd4 100644 --- a/resources.qrc +++ b/resources.qrc @@ -61,5 +61,7 @@ src/gui/ConflictItemFileInfo.qml src/gui/macOS/ui/FileProviderSettings.qml src/gui/macOS/ui/FileProviderFileDelegate.qml + src/gui/declarativeui/DeclarativeUiWindow.qml + src/gui/declarativeui/FileActionsWindow.qml diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index b6d636f47ac1c..923b17513f853 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -258,6 +258,12 @@ set(client_SRCS wizard/linklabel.cpp wizard/wizardproxysettingsdialog.h wizard/wizardproxysettingsdialog.cpp + declarativeui/declarativeuimodel.h + declarativeui/declarativeuimodel.cpp + declarativeui/declarativeui.h + declarativeui/declarativeui.cpp + declarativeui/endpointmodel.h + declarativeui/endpointmodel.cpp ) if (NOT DISABLE_ACCOUNT_MIGRATION) diff --git a/src/gui/application.cpp b/src/gui/application.cpp index ae14bc11a3ff5..802701d9e0484 100644 --- a/src/gui/application.cpp +++ b/src/gui/application.cpp @@ -432,6 +432,12 @@ 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); + + connect(FolderMan::instance()->socketApi(), &SocketApi::declarativeUiCommandReceived, + _gui.data(), &ownCloudGui::slotShowDeclarativeUiDialog); + // startup procedure. connect(&_checkConnectionTimer, &QTimer::timeout, this, &Application::slotCheckConnection); _checkConnectionTimer.setInterval(ConnectionValidator::DefaultCallingIntervalMsec); // check for connection every 32 seconds. diff --git a/src/gui/declarativeui/DeclarativeUiWindow.qml b/src/gui/declarativeui/DeclarativeUiWindow.qml new file mode 100644 index 0000000000000..d08f05543e79d --- /dev/null +++ b/src/gui/declarativeui/DeclarativeUiWindow.qml @@ -0,0 +1,90 @@ +/* + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Window +import QtQuick.Layouts +import QtQuick.Controls + +import com.nextcloud.desktopclient +import Style + +ApplicationWindow { + id: root + width: 400 + height: 500 + minimumWidth: 300 + minimumHeight: 300 + LayoutMirroring.childrenInherit: true + LayoutMirroring.enabled: Application.layoutDirection === Qt.RightToLeft + + property var accountState: ({}) + property string localPath: "" + + title: qsTr("Declarative UI for %1").arg(root.localPath) + + Component { + id: declarativeUiDelegate + + Item { + id: declarativeUiItem + width: parent.width + height: 40 + + required property string name + required property string type + required property string label + required property string url + required property string text + + Row { + anchors.fill: parent + anchors.margins: 8 + spacing: 5 + height: implicitHeight + + Text { + text: declarativeUiItem.text + color: Style.accentColor + font.pixelSize: Style.pixelSize + verticalAlignment: Text.AlignVCenter + visible: declarativeUiItem.name == "Text" + } + + Image { + source: declarativeUiItem.url + width: 50 + height: 50 + verticalAlignment: Text.AlignVCenter + visible: declarativeUiItem.name == "Image" + } + + Button { + text: declarativeUiItem.label + width: 120 + height: 30 + visible: declarativeUiItem.name == "Button" + } + } + } + } + + DeclarativeUi { + id: declarativeUi + accountState: root.accountState + localPath: root.localPath + } + + ListView { + id: declarativeUiView + model: declarativeUi.declarativeUiModel + delegate: declarativeUiDelegate + + anchors.fill: parent + anchors.margins: 10 + } + + +} diff --git a/src/gui/declarativeui/FileActionsWindow.qml b/src/gui/declarativeui/FileActionsWindow.qml new file mode 100644 index 0000000000000..e17913cdb8242 --- /dev/null +++ b/src/gui/declarativeui/FileActionsWindow.qml @@ -0,0 +1,106 @@ +/* + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Window +import QtQuick.Layouts +import QtQuick.Controls + +import com.nextcloud.desktopclient +import Style +import "../tray" + +ApplicationWindow { + id: root + width: 400 + height: 500 + minimumWidth: 300 + minimumHeight: 300 + LayoutMirroring.childrenInherit: true + LayoutMirroring.enabled: Application.layoutDirection === Qt.RightToLeft + flags: Qt.Window + color: Style.currentUserHeaderColor + + property var accountState: ({}) + property string localPath: "" + + title: qsTr("File actions for %1").arg(root.localPath) + + EndpointModel { + id: endpointModel + accountState: root.accountState + localPath: root.localPath + } + + RowLayout { + spacing: 8 + Layout.fillWidth: true + + Image { + source: "image://svgimage-custom-color/folder.svg/" + palette.windowText + Layout.minimumWidth: Style.headerButtonIconSize + Layout.minimumHeight: Style.headerButtonIconSize + } + + EnforcedPlainTextLabel { + text: root.localPath + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + } + + Button { + icon.source: "image://svgimage-custom-color/add.svg/" + palette.windowText + icon.width: Style.activityListButtonIconSize + icon.height: Style.activityListButtonIconSize + Layout.minimumWidth: Style.activityListButtonWidth + Layout.minimumHeight: Style.activityListButtonHeight + } + + Button { + icon.source: "image://svgimage-custom-color/close.svg/" + palette.windowText + icon.width: Style.activityListButtonIconSize + icon.height: Style.activityListButtonIconSize + Layout.minimumWidth: Style.activityListButtonWidth + Layout.minimumHeight: Style.activityListButtonHeight + } + } + + Component { + id: fileActionsDelegate + + Item { + id: fileActionsItem + width: parent.width + height: 40 + + required property string name + + Row { + anchors.fill: parent + anchors.margins: 8 + spacing: 5 + height: implicitHeight + + Button { + icon.source: "image://svgimage-custom-color/files.svg/" + palette.windowText + text: fileActionsItem.name + font.pixelSize: Style.pixelSize + height: implicitHeight + onClicked: endpointModel.createRequest(endpointModel.index) + } + } + } + } + + ListView { + id: fileActionsView + model: endpointModel + delegate: fileActionsDelegate + + anchors.fill: parent + anchors.margins: 10 + } + +} diff --git a/src/gui/declarativeui/declarativeui.cpp b/src/gui/declarativeui/declarativeui.cpp new file mode 100644 index 0000000000000..ab46603ab9230 --- /dev/null +++ b/src/gui/declarativeui/declarativeui.cpp @@ -0,0 +1,68 @@ +/* + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "declarativeui.h" +#include "networkjobs.h" +#include "accountfwd.h" +#include "account.h" + +namespace OCC { + +Q_LOGGING_CATEGORY(lcDeclarativeUi, "nextcloud.gui.declarativeui", QtInfoMsg) + +DeclarativeUi::DeclarativeUi(QObject *parent) + : QObject(parent) +{ +} + +void DeclarativeUi::setAccountState(AccountState *accountState) +{ + if (accountState == nullptr) { + return; + } + + if (accountState == _accountState) { + return; + } + + _accountState = accountState; + _declarativeUiModel = std::make_unique(_accountState->account(), this); + connect(_declarativeUiModel.get(), &DeclarativeUiModel::pageFetched, + this, &DeclarativeUi::declarativeUiFetched); + connect(this, &DeclarativeUi::declarativeUiFetched, + this, &DeclarativeUi::declarativeUiModelChanged); + + Q_EMIT accountStateChanged(); +} + +void DeclarativeUi::setLocalPath(const QString &localPath) +{ + if (localPath.isEmpty()) { + return; + } + + if (localPath == _localPath) { + return; + } + + _localPath = localPath; + Q_EMIT localPathChanged(); +} + +AccountState *DeclarativeUi::accountState() const +{ + return _accountState; +} + +QString DeclarativeUi::localPath() const +{ + return _localPath; +} + +DeclarativeUiModel *DeclarativeUi::declarativeUiModel() const { + return _declarativeUiModel.get(); +} + +} diff --git a/src/gui/declarativeui/declarativeui.h b/src/gui/declarativeui/declarativeui.h new file mode 100644 index 0000000000000..1028a8f688f06 --- /dev/null +++ b/src/gui/declarativeui/declarativeui.h @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +#include +#include + +#include "accountstate.h" +#include "declarativeuimodel.h" +#include "endpointmodel.h" + +namespace OCC { + +Q_DECLARE_LOGGING_CATEGORY(lcDeclarativeUi) +class JsonApiJob; + +class DeclarativeUi : public QObject +{ + Q_OBJECT + Q_PROPERTY(AccountState* accountState READ accountState WRITE setAccountState NOTIFY accountStateChanged) + Q_PROPERTY(QString localPath READ localPath WRITE setLocalPath NOTIFY localPathChanged) + Q_PROPERTY(DeclarativeUiModel* declarativeUiModel READ declarativeUiModel NOTIFY declarativeUiModelChanged) + +public: + DeclarativeUi(QObject *parent = nullptr); + + void setAccountState(AccountState *accountState); + void setLocalPath(const QString &localPath); + + [[nodiscard]] AccountState *accountState() const; + [[nodiscard]] QString localPath() const; + [[nodiscard]] DeclarativeUiModel *declarativeUiModel() const; + +signals: + void declarativeUiFetched(); + void endpointsParsed(); + void localPathChanged(); + void accountStateChanged(); + void declarativeUiModelChanged(); + +private: + AccountState *_accountState; + QString _localPath; + + std::unique_ptr _declarativeUiModel; +}; + +} diff --git a/src/gui/declarativeui/declarativeuimodel.cpp b/src/gui/declarativeui/declarativeuimodel.cpp new file mode 100644 index 0000000000000..e3216a6db1de2 --- /dev/null +++ b/src/gui/declarativeui/declarativeuimodel.cpp @@ -0,0 +1,108 @@ +/* + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "declarativeuimodel.h" +#include "networkjobs.h" + +namespace OCC { + +DeclarativeUiModel::DeclarativeUiModel(const AccountPtr &account, QObject *parent) + : QAbstractListModel(parent) + , _account(account) +{ + fetchPage(); +} + +void DeclarativeUiModel::fetchPage() +{ + if (!_account) { + return; + } + + auto job = new JsonApiJob(_account, + QLatin1String("ocs/v2.php/apps/declarativetest/version1"), + this); + connect(job, &JsonApiJob::jsonReceived, + this, &DeclarativeUiModel::slotPageFetched); + job->start(); +} + +void DeclarativeUiModel::slotPageFetched(const QJsonDocument &json) +{ + const auto root = json.object().value(QStringLiteral("root")).toObject(); + if (root.empty()) { + return; + } + const auto orientation = root.value(QStringLiteral("orientation")).toString(); + const auto rows = root.value(QStringLiteral("rows")).toArray(); + if (rows.empty()) { + 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(); + Element element; + element.name = child.value(QStringLiteral("element")).toString(); + element.type = child.value(QStringLiteral("type")).toString(); + element.label = child.value(QStringLiteral("label")).toString(); + element.url = _account->url().toString() + child.value(QStringLiteral("url")).toString(); + element.text = child.value(QStringLiteral("text")).toString(); + _page.append(element); + } + } + + Q_EMIT pageFetched(); +} + +QVariant DeclarativeUiModel::data(const QModelIndex &index, int role) const +{ + Q_ASSERT(checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid)); + switch (role) { + case ElementNameRole: + return _page.at(index.row()).name; // Button, Text, Image + case ElementTypeRole: + return _page.at(index.row()).type; // Primary, Secondarys + case ElementLabelRole: + return _page.at(index.row()).label; // Cancel, Submit + case ElementUrlRole: + return _page.at(index.row()).url; // /core/img/logo/log.png + case ElementTextRole: + return _page.at(index.row()).text; // String + } + + return {}; +} + +int DeclarativeUiModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + + return _page.size(); +} + +QHash DeclarativeUiModel::roleNames() const +{ + auto roles = QAbstractListModel::roleNames(); + roles[ElementNameRole] = "name"; + roles[ElementTypeRole] = "type"; + roles[ElementLabelRole] = "label"; + roles[ElementUrlRole] = "url"; + roles[ElementTextRole] = "text"; + + return roles; +} + +QString DeclarativeUiModel::pageOrientation() const +{ + return _pageOrientation; +} + +} diff --git a/src/gui/declarativeui/declarativeuimodel.h b/src/gui/declarativeui/declarativeuimodel.h new file mode 100644 index 0000000000000..f0f85047e3d0a --- /dev/null +++ b/src/gui/declarativeui/declarativeuimodel.h @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +#include +#include "libsync/account.h" + +namespace OCC { + +class JsonApiJob; + +class DeclarativeUiModel : public QAbstractListModel +{ + Q_OBJECT + +public: + explicit DeclarativeUiModel(const AccountPtr &accountState, QObject *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 { + ElementNameRole = Qt::UserRole + 1, + ElementTypeRole, + ElementLabelRole, + ElementTextRole, + ElementUrlRole + }; + Q_ENUM(DataRole) + + [[nodiscard]] QString pageOrientation() const; + void fetchPage(); + +signals: + void pageFetched(); + +public slots: + void slotPageFetched(const QJsonDocument &json); + +private: + struct Element { + QString name; + QString type; + QString label; + QString url; + QString text; + }; + QList _page; + QString _pageOrientation; + + AccountPtr _account; +}; + +} diff --git a/src/gui/declarativeui/endpointmodel.cpp b/src/gui/declarativeui/endpointmodel.cpp new file mode 100644 index 0000000000000..da4af05566a1f --- /dev/null +++ b/src/gui/declarativeui/endpointmodel.cpp @@ -0,0 +1,174 @@ +/* + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "endpointmodel.h" +#include "networkjobs.h" +#include "account.h" + +namespace OCC { + +EndpointModel::EndpointModel(QObject *parent) + : QAbstractListModel(parent) +{ +} + +void EndpointModel::parseEndpoints() +{ + if (!_accountState->isConnected()) { + return; + } + + const auto elementsList = _accountState->account()->capabilities().declarativeUiEndpoints(); + for (const auto &element : elementsList) { + const auto elementMap = element.toMap(); + const auto type = elementMap.value("type").toString(); // context-menu, create-new + const auto endpoints = elementMap.value("endpoints").toList(); + for (const auto &endpoint : endpoints) { + const auto element = endpoint.toMap(); + _endpoints.append({element.value("type").toString(), + element.value("name").toString(), + element.value("url").toString(), + element.value("desktop_icon").toString(), + element.value("filter").toString(), + element.value("parameter").toString()}); + } + } + + Q_EMIT endpointModelChanged(); +} + +QVariant EndpointModel::data(const QModelIndex &index, int role) const +{ + Q_ASSERT(checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid)); + const auto row = index.row(); + switch (role) { + case EndpointTypeRole: + return _endpoints.at(row).type; //context-menu, create-new + case EndpointNameRole: + return _endpoints.at(row).name; // Deck board + case EndpointUrlRole: + return _endpoints.at(row).url; // /ocs/v2.php/apps/declarativetest/newDeckBoard + case EndpointIconRole: + return _endpoints.at(row).icon; // zip + case EndpointFilterRole: + return _endpoints.at(row).filter; // image/ + case EndpointParameterRole: + return _endpoints.at(row).parameter; // fileId + case EndpointVerbRole: + return _endpoints.at(row).verb; // POST, GET + } + + return {}; +} + +int EndpointModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + + return _endpoints.size(); +} + +QHash EndpointModel::roleNames() const +{ + auto roles = QAbstractListModel::roleNames(); + roles[EndpointTypeRole] = "type"; + roles[EndpointNameRole] = "name"; + roles[EndpointUrlRole] = "url"; + roles[EndpointIconRole] = "icon"; + roles[EndpointFilterRole] = "filter"; + roles[EndpointParameterRole] = "parameter"; + roles[EndpointVerbRole] = "verb"; + + return roles; +} + +void EndpointModel::setAccountState(AccountState *accountState) +{ + if (accountState == nullptr) { + return; + } + + if (accountState == _accountState) { + return; + } + + _accountState = accountState; + parseEndpoints(); + Q_EMIT accountStateChanged(); +} + +void EndpointModel::setLocalPath(const QString &localPath) +{ + if (localPath.isEmpty()) { + return; + } + + if (localPath == _localPath) { + return; + } + + _localPath = localPath; + Q_EMIT localPathChanged(); +} + +AccountState *EndpointModel::accountState() const +{ + return _accountState; +} + +QString EndpointModel::localPath() const +{ + return _localPath; +} + +void EndpointModel::createRequest(const int row) +{ + if (!_accountState) { + return; + } + + auto job = new JsonApiJob(_accountState->account(), + _endpoints.at(row).url, + this); + connect(job, &JsonApiJob::jsonReceived, + this, &EndpointModel::processRequest); + QUrlQuery params; + params.addQueryItem(_endpoints.at(row).parameter, 0); //fileId + job->addQueryParams(params); + job->setVerb(SimpleApiJob::Verb::Post); //fixit _endpoints.at(row).verb + job->start(); +} + +void EndpointModel::processRequest(const QJsonDocument &json) +{ + const auto root = json.object().value(QStringLiteral("root")).toObject(); + if (root.empty()) { + return; + } + const auto orientation = root.value(QStringLiteral("orientation")).toString(); + const auto rows = root.value(QStringLiteral("rows")).toArray(); + if (rows.empty()) { + 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(); + _response.name = child.value(QStringLiteral("element")).toString(); + _response.type = child.value(QStringLiteral("type")).toString(); + _response.label = child.value(QStringLiteral("label")).toString(); + _response.url = _accountState->account()->url().toString() + + child.value(QStringLiteral("url")).toString(); + _response.text = child.value(QStringLiteral("text")).toString(); + } + } +} + +} // namespace OCC diff --git a/src/gui/declarativeui/endpointmodel.h b/src/gui/declarativeui/endpointmodel.h new file mode 100644 index 0000000000000..2f63e69c537df --- /dev/null +++ b/src/gui/declarativeui/endpointmodel.h @@ -0,0 +1,79 @@ +/* + * 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 { + +class EndpointModel : public QAbstractListModel { + Q_OBJECT + Q_PROPERTY(AccountState* accountState READ accountState WRITE setAccountState NOTIFY accountStateChanged) + Q_PROPERTY(QString localPath READ localPath WRITE setLocalPath NOTIFY localPathChanged) + +public: + explicit EndpointModel(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 { + EndpointTypeRole = Qt::UserRole + 1, + EndpointNameRole, + EndpointUrlRole, + EndpointIconRole, + EndpointFilterRole, + EndpointParameterRole, + EndpointVerbRole + }; + Q_ENUM(DataRole) + + void parseEndpoints(); + + void setAccountState(AccountState *accountState); + void setLocalPath(const QString &localPath); + + [[nodiscard]] AccountState *accountState() const; + [[nodiscard]] QString localPath() const; + +signals: + void endpointModelChanged(); + void localPathChanged(); + void accountStateChanged(); + void requestDone(); + +public slots: + void createRequest(const int row); + void processRequest(const QJsonDocument &json); + +private: + struct Response { + QString name; + QString type; + QString label; + QString url; + QString text; + }; + Response _response; + + struct Endpoint { + QString type; + QString name; + QString url; + QString icon; + QString filter; + QString parameter; + QString verb; + }; + QList _endpoints; + AccountState *_accountState; + QString _localPath; +}; + +} diff --git a/src/gui/owncloudgui.cpp b/src/gui/owncloudgui.cpp index 68432164f2b1a..35b8a6c7d3385 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 "declarativeui/declarativeui.h" #include "filesystem.h" #ifdef WITH_LIBCLOUDPROVIDERS @@ -135,6 +136,8 @@ 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, "DeclarativeUi"); + qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "EndpointModel"); qmlRegisterUncreatableType("com.nextcloud.desktopclient", 1, 0, "QAbstractItemModel", "QAbstractItemModel"); qmlRegisterUncreatableType("com.nextcloud.desktopclient", 1, 0, "activity", "Activity"); @@ -716,4 +719,14 @@ void ownCloudGui::slotShowFileActivityDialog(const QString &localPath) const _tray->createFileActivityDialog(localPath); } +void ownCloudGui::slotShowDeclarativeUiDialog(const QString &localPath) const +{ + _tray->showDeclarativeUiDialog(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..b069a900c2645 100644 --- a/src/gui/owncloudgui.h +++ b/src/gui/owncloudgui.h @@ -95,6 +95,8 @@ public slots: */ void slotShowShareDialog(const QString &localPath) const; void slotShowFileActivityDialog(const QString &localPath) const; + void slotShowDeclarativeUiDialog(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..4021e18d13085 100644 --- a/src/gui/socketapi/socketapi.cpp +++ b/src/gui/socketapi/socketapi.cpp @@ -615,6 +615,18 @@ 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::processDeclarativeUiRequest(const QString &localFile) +{ + const auto fileData = FileData::get(localFile); + emit declarativeUiCommandReceived(fileData.localPath); +} + void SocketApi::broadcastStatusPushMessage(const QString &systemPath, SyncFileStatus fileStatus) { QString msg = buildMessage(QLatin1String("STATUS"), systemPath, fileStatus.toSocketAPIString()); @@ -723,6 +735,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,10 +1133,11 @@ 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_ACTIVITY_MENU_TITLE", tr("Activity") }, { "CONTEXT_MENU_TITLE", Theme::instance()->appNameGUI() }, + { "FILE_ACTIONS_MENU_TITLE", tr("File actions") }, { "COPY_PRIVATE_LINK_MENU_TITLE", tr("Copy private link to clipboard") }, { "EMAIL_PRIVATE_LINK_MENU_TITLE", tr("Send private link by email …") }, { "CONTEXT_MENU_ICON", APPLICATION_ICON_NAME }, diff --git a/src/gui/socketapi/socketapi.h b/src/gui/socketapi/socketapi.h index 5dcf6900e8c04..3ed469baaf7a6 100644 --- a/src/gui/socketapi/socketapi.h +++ b/src/gui/socketapi/socketapi.h @@ -64,6 +64,8 @@ public slots: signals: void shareCommandReceived(const QString &localPath); void fileActivityCommandReceived(const QString &localPath); + void fileActionsCommandReceived(const QString &localPath); + void declarativeUiCommandReceived(const QString &localPath); private slots: void slotNewConnection(); @@ -107,6 +109,8 @@ private slots: void processLeaveShareRequest(const QString &localFile, SocketListener *listener); void processFileActivityRequest(const QString &localFile); void processEncryptRequest(const QString &localFile); + void processFileActionsRequest(const QString &localFile); + void processDeclarativeUiRequest(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 +135,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; diff --git a/src/gui/systray.cpp b/src/gui/systray.cpp index e9fc7564b75b4..d27837b15a3cd 100644 --- a/src/gui/systray.cpp +++ b/src/gui/systray.cpp @@ -16,6 +16,7 @@ #include "configfile.h" #include "accessmanager.h" #include "callstatechecker.h" +#include "declarativeui/declarativeui.h" #include #include @@ -436,6 +437,88 @@ void Systray::createFileActivityDialog(const QString &localPath) Q_EMIT showFileDetailsPage(localPath, FileDetailsPage::Activity); } +void Systray::showDeclarativeUiDialog(const QString &localPath) +{ + createDeclarativeUiDialog(localPath); +} + +void Systray::showFileActionsDialog(const QString &localPath) +{ + createFileActionsDialog(localPath); +} + +void Systray::createDeclarativeUiDialog(const QString &localPath) +{ + if (!_trayEngine) { + qCWarning(lcSystray) << "Could not open declarative UI dialog for" << localPath << "as no tray engine was available"; + return; + } + + const auto folder = FolderMan::instance()->folderForPath(localPath); + if (!folder) { + qCWarning(lcSystray) << "Could not open declarative UI dialog for" << localPath << "no responsible folder found"; + return; + } + + QQmlComponent declarativeUiQml(trayEngine(), QStringLiteral("qrc:/qml/src/gui/declarativeui/DeclarativeUiWindow.qml")); + if (declarativeUiQml.isError()) { + qCWarning(lcSystray) << declarativeUiQml.errorString(); + qCWarning(lcSystray) << declarativeUiQml.errors(); + return; + } + + const QVariantMap initialProperties{ + {"accountState", QVariant::fromValue(folder->accountState())}, + {"localPath", localPath}, + }; + const auto declarativeUiDialog = declarativeUiQml.createWithInitialProperties(initialProperties); + const auto dialog = qobject_cast(declarativeUiDialog); + if (!dialog) { + qCWarning(lcSystray) << "Declarative UI dialog window resulted in creation of object that was not a window!"; + return; + } + + dialog->show(); + dialog->raise(); + dialog->requestActivate(); +} + +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/declarativeui/FileActionsWindow.qml")); + if (fileActionsQml.isError()) { + qCWarning(lcSystray) << fileActionsQml.errorString(); + qCWarning(lcSystray) << fileActionsQml.errors(); + return; + } + + const QVariantMap initialProperties{ + {"accountState", QVariant::fromValue(folder->accountState())}, + {"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 +531,18 @@ void Systray::presentShareViewInTray(const QString &localPath) Q_EMIT showFileDetails(folder->accountState(), localPath, FileDetailsPage::Sharing); } +void Systray::presentDeclarativeUiViewInSystray(const QString &localPath) +{ + qCDebug(lcSystray) << "Opening declarative ui view in tray for " << localPath; + createDeclarativeUiDialog(localPath); +} + +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..cc17353503dce 100644 --- a/src/gui/systray.h +++ b/src/gui/systray.h @@ -146,8 +146,12 @@ public slots: void createShareDialog(const QString &localPath); void createFileActivityDialog(const QString &localPath); + void showDeclarativeUiDialog(const QString &localPath); + void showFileActionsDialog(const QString &localPath); void presentShareViewInTray(const QString &localPath); + void presentDeclarativeUiViewInSystray(const QString &localPath); + void presentFileActionsViewInSystray(const QString &localPath); private slots: void slotUpdateSyncPausedState(); @@ -165,6 +169,8 @@ private slots: void setupContextMenu(); void createFileDetailsDialog(const QString &localPath); + void createDeclarativeUiDialog(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..6bbf27ae5e95e 100644 --- a/src/gui/tray/ActivityItemContent.qml +++ b/src/gui/tray/ActivityItemContent.qml @@ -185,7 +185,36 @@ 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 { + height: visible ? implicitHeight : 0 + text: qsTr("File actions") + font.pixelSize: Style.topLinePixelSize + hoverEnabled: true + onClicked: Systray.presentFileActionsViewInSystray(model.openablePath) + } + + MenuItem { + height: visible ? implicitHeight : 0 + text: qsTr("Declarative UI") + font.pixelSize: Style.topLinePixelSize + hoverEnabled: true + onClicked: Systray.presentDeclarativeUiViewInSystray(model.openablePath) + } + } } Button { diff --git a/src/libsync/capabilities.cpp b/src/libsync/capabilities.cpp index 92bfa5d6d73ea..af8a00a45211a 100644 --- a/src/libsync/capabilities.cpp +++ b/src/libsync/capabilities.cpp @@ -437,6 +437,18 @@ QStringList Capabilities::forbiddenFilenameExtensions() const return _capabilities["files"].toMap()["forbidden_filename_extensions"].toStringList(); } +bool Capabilities::serverHasDeclarativeUi() const +{ + return _capabilities[QStringLiteral("declarativeui")].toMap().isEmpty(); +} + +QVariantList Capabilities::declarativeUiEndpoints() const +{ + const auto declarativeUi = _capabilities.value("declarativeui").toMap(); + const auto hooks = declarativeUi.value("hooks").toList(); + return hooks; +} + /*-------------------------------------------------------------------------------------*/ // Direct Editing @@ -466,6 +478,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..ece247a65be1d 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 serverHasDeclarativeUi() const; + [[nodiscard]] QVariantList declarativeUiEndpoints() 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; }; From b5caa1efb4f833172c5ce57c21992d46743164ec Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Wed, 3 Sep 2025 18:18:00 +0200 Subject: [PATCH 02/22] feat(declarativeui): style file actions window. - display response from request from file actions. Signed-off-by: Camila Ayres --- src/gui/declarativeui/FileActionsWindow.qml | 178 +++++++++++++------- src/gui/declarativeui/endpointmodel.cpp | 61 ++++++- src/gui/declarativeui/endpointmodel.h | 39 ++++- src/gui/systray.cpp | 6 +- 4 files changed, 212 insertions(+), 72 deletions(-) diff --git a/src/gui/declarativeui/FileActionsWindow.qml b/src/gui/declarativeui/FileActionsWindow.qml index e17913cdb8242..10720ab3b6cf0 100644 --- a/src/gui/declarativeui/FileActionsWindow.qml +++ b/src/gui/declarativeui/FileActionsWindow.qml @@ -7,26 +7,24 @@ import QtQuick import QtQuick.Window import QtQuick.Layouts import QtQuick.Controls - import com.nextcloud.desktopclient import Style -import "../tray" ApplicationWindow { id: root width: 400 - height: 500 + height: 300 minimumWidth: 300 - minimumHeight: 300 - LayoutMirroring.childrenInherit: true - LayoutMirroring.enabled: Application.layoutDirection === Qt.RightToLeft - flags: Qt.Window - color: Style.currentUserHeaderColor + minimumHeight: 200 + flags: Qt.Dialog + visible: true property var accountState: ({}) property string localPath: "" + property string shortLocalPath: "" + property var response: ({}) - title: qsTr("File actions for %1").arg(root.localPath) + title: qsTr("File actions for %1").arg(root.shortLocalPath) EndpointModel { id: endpointModel @@ -34,73 +32,129 @@ ApplicationWindow { localPath: root.localPath } - RowLayout { - spacing: 8 - Layout.fillWidth: true + Rectangle { + anchors.fill: parent + color: Style.infoBoxBackgroundColor + //radius: Style.trayWindowRadius + border.color: Style.accentColor + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.standardSpacing + spacing: Style.standardSpacing + + RowLayout { + Layout.fillWidth: true + spacing: Style.standardSpacing + + Image { + source: "image://svgimage-custom-color/files.svg/" + palette.windowText + width: Style.minimumActivityItemHeight + height: Style.minimumActivityItemHeight + } - Image { - source: "image://svgimage-custom-color/folder.svg/" + palette.windowText - Layout.minimumWidth: Style.headerButtonIconSize - Layout.minimumHeight: Style.headerButtonIconSize - } + ColumnLayout { + Layout.fillWidth: true + spacing: Style.extraSmallSpacing - EnforcedPlainTextLabel { - text: root.localPath - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft - } + Label { + text: root.shortLocalPath + font.bold: true + font.pixelSize: Style.pixelSize + color: Style.ncHeaderTextColor + } + } + } - Button { - icon.source: "image://svgimage-custom-color/add.svg/" + palette.windowText - icon.width: Style.activityListButtonIconSize - icon.height: Style.activityListButtonIconSize - Layout.minimumWidth: Style.activityListButtonWidth - Layout.minimumHeight: Style.activityListButtonHeight - } + Rectangle { + Layout.fillWidth: true + height: Style.extraExtraSmallSpacing + color: Style.accentColor + } + + ListView { + id: fileActionsView + model: endpointModel + clip: true + spacing: Style.trayHorizontalMargin + Layout.fillWidth: true + Layout.fillHeight: true + delegate: fileActionsDelegate + } + + Rectangle { + Layout.fillWidth: true + height: Style.extraExtraSmallSpacing + color: Style.accentColor + } + + Text { + id: response + text: endpointModel.declarativeUiText + textFormat: Text.RichText + color: Style.ncHeaderTextColor + font.pointSize: Style.pixelSize + font.underline: true + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: Qt.openUrlExternally(endpointModel.declarativeUiUrl) + } + } - Button { - icon.source: "image://svgimage-custom-color/close.svg/" + palette.windowText - icon.width: Style.activityListButtonIconSize - icon.height: Style.activityListButtonIconSize - Layout.minimumWidth: Style.activityListButtonWidth - Layout.minimumHeight: Style.activityListButtonHeight + Rectangle { + visible: response.text != "" + Layout.fillWidth: true + height: Style.extraExtraSmallSpacing + color: Style.accentColor + } } } Component { id: fileActionsDelegate - Item { - id: fileActionsItem - width: parent.width - height: 40 + RowLayout { + Layout.fillWidth: true + spacing: Style.standardSpacing + height: implicitHeight required property string name - - Row { - anchors.fill: parent - anchors.margins: 8 - spacing: 5 - height: implicitHeight - - Button { - icon.source: "image://svgimage-custom-color/files.svg/" + palette.windowText - text: fileActionsItem.name - font.pixelSize: Style.pixelSize - height: implicitHeight - onClicked: endpointModel.createRequest(endpointModel.index) + required property int index + + Button { + Layout.fillWidth: true + implicitHeight: Style.activityListButtonHeight + + padding: 0 + leftPadding: Style.standardSpacing + rightPadding: Style.standardSpacing + spacing: Style.standardSpacing + + contentItem: Row { + anchors.fill: parent + anchors.margins: Style.smallSpacing + spacing: Style.standardSpacing + + Image { + source: "image://svgimage-custom-color/settings.svg/" + palette.windowText + width: Style.minimumActivityItemHeight + height: Style.minimumActivityItemHeight + fillMode: Image.PreserveAspectFit + anchors.verticalCenter: parent.verticalCenter + } + + Label { + text: name + color: Style.ncHeaderTextColor + font.pixelSize: Style.pixelSize + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + } } + + onClicked: endpointModel.createRequest(index) } } } - - ListView { - id: fileActionsView - model: endpointModel - delegate: fileActionsDelegate - - anchors.fill: parent - anchors.margins: 10 - } - } diff --git a/src/gui/declarativeui/endpointmodel.cpp b/src/gui/declarativeui/endpointmodel.cpp index da4af05566a1f..7589cd034c528 100644 --- a/src/gui/declarativeui/endpointmodel.cpp +++ b/src/gui/declarativeui/endpointmodel.cpp @@ -32,7 +32,8 @@ void EndpointModel::parseEndpoints() element.value("url").toString(), element.value("desktop_icon").toString(), element.value("filter").toString(), - element.value("parameter").toString()}); + element.value("parameter").toString(), + element.value("verb").toString()}); } } @@ -115,6 +116,12 @@ void EndpointModel::setLocalPath(const QString &localPath) Q_EMIT localPathChanged(); } +void EndpointModel::setResponse(const Response &response) +{ + _response = response; + Q_EMIT responseChanged(); +} + AccountState *EndpointModel::accountState() const { return _accountState; @@ -125,6 +132,56 @@ QString EndpointModel::localPath() const return _localPath; } +QString EndpointModel::name() const +{ + return _response.name; +} + +void EndpointModel::setName(const QString &name) +{ + _response.name = name; +} + +QString EndpointModel::type() const +{ + return _response.type; +} + +void EndpointModel::setType(const QString &type) +{ + _response.type = type; +} + +QString EndpointModel::label() const +{ + return _response.label; +} + +void EndpointModel::setLabel(const QString &label) +{ + _response.label = label; +} + +QString EndpointModel::url() const +{ + return _response.url; +} + +void EndpointModel::setUrl(const QString &url) +{ + _response.url = url; +} + +QString EndpointModel::text() const +{ + return _response.text; +} + +void EndpointModel::setText(const QString &text) +{ + _response.text = text; +} + void EndpointModel::createRequest(const int row) { if (!_accountState) { @@ -169,6 +226,8 @@ void EndpointModel::processRequest(const QJsonDocument &json) _response.text = child.value(QStringLiteral("text")).toString(); } } + + Q_EMIT responseChanged(); } } // namespace OCC diff --git a/src/gui/declarativeui/endpointmodel.h b/src/gui/declarativeui/endpointmodel.h index 2f63e69c537df..ceec6c5ec30c2 100644 --- a/src/gui/declarativeui/endpointmodel.h +++ b/src/gui/declarativeui/endpointmodel.h @@ -16,6 +16,11 @@ class EndpointModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(AccountState* accountState READ accountState WRITE setAccountState NOTIFY accountStateChanged) Q_PROPERTY(QString localPath READ localPath WRITE setLocalPath NOTIFY localPathChanged) + Q_PROPERTY(QString declarativeUiName READ name WRITE setName NOTIFY responseChanged) + Q_PROPERTY(QString declarativeUiType READ type WRITE setType NOTIFY responseChanged) + Q_PROPERTY(QString declarativeUiLabel READ type WRITE setLabel NOTIFY responseChanged) + Q_PROPERTY(QString declarativeUiUrl READ url WRITE setUrl NOTIFY responseChanged) + Q_PROPERTY(QString declarativeUiText READ text WRITE setText NOTIFY responseChanged) public: explicit EndpointModel(QObject *const parent = nullptr); @@ -34,32 +39,50 @@ class EndpointModel : public QAbstractListModel { }; Q_ENUM(DataRole) + struct Response { + QString name; + QString type; + QString label; + QString url; + QString text; + }; + void parseEndpoints(); void setAccountState(AccountState *accountState); void setLocalPath(const QString &localPath); + void setResponse(const Response &response); [[nodiscard]] AccountState *accountState() const; [[nodiscard]] QString localPath() const; + [[nodiscard]] QString name() const; + void setName(const QString &name); + + [[nodiscard]] QString type() const; + void setType(const QString &type); + + [[nodiscard]] QString label() const; + void setLabel(const QString &label); + + [[nodiscard]] QString url() const; + void setUrl(const QString &url); + + [[nodiscard]] QString text() const; + void setText(const QString &text); + + signals: void endpointModelChanged(); void localPathChanged(); void accountStateChanged(); - void requestDone(); + void responseChanged(); public slots: void createRequest(const int row); void processRequest(const QJsonDocument &json); private: - struct Response { - QString name; - QString type; - QString label; - QString url; - QString text; - }; Response _response; struct Endpoint { diff --git a/src/gui/systray.cpp b/src/gui/systray.cpp index d27837b15a3cd..6c7554bccbf3f 100644 --- a/src/gui/systray.cpp +++ b/src/gui/systray.cpp @@ -503,10 +503,14 @@ void Systray::createFileActionsDialog(const QString &localPath) return; } + QFileInfo localFile{localPath}; + const auto shortLocalPath = localFile.fileName(); const QVariantMap initialProperties{ {"accountState", QVariant::fromValue(folder->accountState())}, - {"localPath", localPath}, + {"shortLocalPath", shortLocalPath}, + {"localPath", localPath} }; + const auto fileActionsDialog = fileActionsQml.createWithInitialProperties(initialProperties); const auto dialog = qobject_cast(fileActionsDialog); if (!dialog) { From 49fba7f3505ef6b70c727d1e3a55a0a724c4aea3 Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Wed, 3 Sep 2025 21:30:13 +0200 Subject: [PATCH 03/22] feat(declarativeui): improve the looks of the file actions Window. Signed-off-by: Camila Ayres --- src/gui/declarativeui/FileActionsWindow.qml | 61 +++++++++++++++++---- 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/src/gui/declarativeui/FileActionsWindow.qml b/src/gui/declarativeui/FileActionsWindow.qml index 10720ab3b6cf0..03fe44431f895 100644 --- a/src/gui/declarativeui/FileActionsWindow.qml +++ b/src/gui/declarativeui/FileActionsWindow.qml @@ -88,17 +88,51 @@ ApplicationWindow { color: Style.accentColor } - Text { - id: response - text: endpointModel.declarativeUiText - textFormat: Text.RichText - color: Style.ncHeaderTextColor - font.pointSize: Style.pixelSize - font.underline: true - MouseArea { + Button { + id: responseButton + visible: response.text !== "" + flat: true + Layout.fillWidth: true + implicitHeight: Style.activityListButtonHeight + + padding: 0 + leftPadding: Style.standardSpacing + rightPadding: Style.standardSpacing + spacing: Style.standardSpacing + + contentItem: Row { anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: Qt.openUrlExternally(endpointModel.declarativeUiUrl) + anchors.margins: Style.smallSpacing + spacing: Style.standardSpacing + + Image { + source: "image://svgimage-custom-color/public.svg/" + palette.windowText + width: Style.minimumActivityItemHeight + height: Style.minimumActivityItemHeight + fillMode: Image.PreserveAspectFit + anchors.verticalCenter: parent.verticalCenter + } + + Text { + id: response + text: endpointModel.declarativeUiText + textFormat: Text.RichText + color: Style.ncHeaderTextColor + font.pointSize: Style.pixelSize + font.underline: true + anchors.verticalCenter: parent.verticalCenter + MouseArea { + id: responseArea + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: Qt.openUrlExternally(endpointModel.declarativeUiUrl) + } + } + } + + ToolTip { + visible: responseButton.hovered + text: qsTr("Download file") } } @@ -123,6 +157,8 @@ ApplicationWindow { required property int index Button { + id: fileActionButton + flat: true Layout.fillWidth: true implicitHeight: Style.activityListButtonHeight @@ -153,6 +189,11 @@ ApplicationWindow { } } + ToolTip { + visible: fileActionButton.hovered + text: name + } + onClicked: endpointModel.createRequest(index) } } From b5f646abb52d1777e479103c02deae7abf51addf Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Thu, 4 Sep 2025 11:01:07 +0200 Subject: [PATCH 04/22] fix(declarativeui/fileactions): file actions context menu. - add helper function to match string to SimpleApiJob::Verb. - filter and display file actions based on the file mimetype. - adjust logic to get context menu by mimeType. - create function to set file id and mime type. - rename Endpoint to FileActions. Signed-off-by: Camila Ayres --- src/gui/CMakeLists.txt | 4 +- src/gui/declarativeui/FileActionsWindow.qml | 23 +- src/gui/declarativeui/declarativeui.h | 2 +- src/gui/declarativeui/endpointmodel.cpp | 233 ----------------- src/gui/declarativeui/fileactionsmodel.cpp | 234 ++++++++++++++++++ .../{endpointmodel.h => fileactionsmodel.h} | 73 +++--- src/gui/owncloudgui.cpp | 2 +- src/gui/socketapi/socketapi.cpp | 11 +- src/gui/socketapi/socketapi.h | 2 +- src/gui/systray.cpp | 2 +- src/libsync/capabilities.cpp | 43 +++- src/libsync/capabilities.h | 3 +- src/libsync/networkjobs.cpp | 17 ++ src/libsync/networkjobs.h | 2 + 14 files changed, 358 insertions(+), 293 deletions(-) delete mode 100644 src/gui/declarativeui/endpointmodel.cpp create mode 100644 src/gui/declarativeui/fileactionsmodel.cpp rename src/gui/declarativeui/{endpointmodel.h => fileactionsmodel.h} (55%) diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 923b17513f853..7306b23675f99 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -262,8 +262,8 @@ set(client_SRCS declarativeui/declarativeuimodel.cpp declarativeui/declarativeui.h declarativeui/declarativeui.cpp - declarativeui/endpointmodel.h - declarativeui/endpointmodel.cpp + declarativeui/fileactionsmodel.h + declarativeui/fileactionsmodel.cpp ) if (NOT DISABLE_ACCOUNT_MIGRATION) diff --git a/src/gui/declarativeui/FileActionsWindow.qml b/src/gui/declarativeui/FileActionsWindow.qml index 03fe44431f895..d1e58c9e9869e 100644 --- a/src/gui/declarativeui/FileActionsWindow.qml +++ b/src/gui/declarativeui/FileActionsWindow.qml @@ -26,8 +26,8 @@ ApplicationWindow { title: qsTr("File actions for %1").arg(root.shortLocalPath) - EndpointModel { - id: endpointModel + FileActionsModel { + id: fileActionModel accountState: root.accountState localPath: root.localPath } @@ -44,6 +44,7 @@ ApplicationWindow { spacing: Style.standardSpacing RowLayout { + id: windowHeader Layout.fillWidth: true spacing: Style.standardSpacing @@ -67,6 +68,7 @@ ApplicationWindow { } Rectangle { + id: lineTop Layout.fillWidth: true height: Style.extraExtraSmallSpacing color: Style.accentColor @@ -74,7 +76,7 @@ ApplicationWindow { ListView { id: fileActionsView - model: endpointModel + model: fileActionModel clip: true spacing: Style.trayHorizontalMargin Layout.fillWidth: true @@ -83,6 +85,7 @@ ApplicationWindow { } Rectangle { + id: lineBottom Layout.fillWidth: true height: Style.extraExtraSmallSpacing color: Style.accentColor @@ -101,6 +104,7 @@ ApplicationWindow { spacing: Style.standardSpacing contentItem: Row { + id: responseContent anchors.fill: parent anchors.margins: Style.smallSpacing spacing: Style.standardSpacing @@ -115,7 +119,7 @@ ApplicationWindow { Text { id: response - text: endpointModel.declarativeUiText + text: fileActionModel.responseLabel textFormat: Text.RichText color: Style.ncHeaderTextColor font.pointSize: Style.pixelSize @@ -125,18 +129,19 @@ ApplicationWindow { id: responseArea anchors.fill: parent cursorShape: Qt.PointingHandCursor - onClicked: Qt.openUrlExternally(endpointModel.declarativeUiUrl) + onClicked: Qt.openUrlExternally(fileActionModel.responseUrl) } } } ToolTip { visible: responseButton.hovered - text: qsTr("Download file") + text: fileActionModel.responseLabel } } Rectangle { + id: repsonseLineBottom visible: response.text != "" Layout.fillWidth: true height: Style.extraExtraSmallSpacing @@ -155,6 +160,7 @@ ApplicationWindow { required property string name required property int index + required property string icon Button { id: fileActionButton @@ -168,12 +174,13 @@ ApplicationWindow { spacing: Style.standardSpacing contentItem: Row { + id: fileActionsContent anchors.fill: parent anchors.margins: Style.smallSpacing spacing: Style.standardSpacing Image { - source: "image://svgimage-custom-color/settings.svg/" + palette.windowText + source: icon width: Style.minimumActivityItemHeight height: Style.minimumActivityItemHeight fillMode: Image.PreserveAspectFit @@ -194,7 +201,7 @@ ApplicationWindow { text: name } - onClicked: endpointModel.createRequest(index) + onClicked: fileActionModel.createRequest(index) } } } diff --git a/src/gui/declarativeui/declarativeui.h b/src/gui/declarativeui/declarativeui.h index 1028a8f688f06..e4dae39f1560f 100644 --- a/src/gui/declarativeui/declarativeui.h +++ b/src/gui/declarativeui/declarativeui.h @@ -10,7 +10,7 @@ #include "accountstate.h" #include "declarativeuimodel.h" -#include "endpointmodel.h" +#include "fileactionsmodel.h" namespace OCC { diff --git a/src/gui/declarativeui/endpointmodel.cpp b/src/gui/declarativeui/endpointmodel.cpp deleted file mode 100644 index 7589cd034c528..0000000000000 --- a/src/gui/declarativeui/endpointmodel.cpp +++ /dev/null @@ -1,233 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#include "endpointmodel.h" -#include "networkjobs.h" -#include "account.h" - -namespace OCC { - -EndpointModel::EndpointModel(QObject *parent) - : QAbstractListModel(parent) -{ -} - -void EndpointModel::parseEndpoints() -{ - if (!_accountState->isConnected()) { - return; - } - - const auto elementsList = _accountState->account()->capabilities().declarativeUiEndpoints(); - for (const auto &element : elementsList) { - const auto elementMap = element.toMap(); - const auto type = elementMap.value("type").toString(); // context-menu, create-new - const auto endpoints = elementMap.value("endpoints").toList(); - for (const auto &endpoint : endpoints) { - const auto element = endpoint.toMap(); - _endpoints.append({element.value("type").toString(), - element.value("name").toString(), - element.value("url").toString(), - element.value("desktop_icon").toString(), - element.value("filter").toString(), - element.value("parameter").toString(), - element.value("verb").toString()}); - } - } - - Q_EMIT endpointModelChanged(); -} - -QVariant EndpointModel::data(const QModelIndex &index, int role) const -{ - Q_ASSERT(checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid)); - const auto row = index.row(); - switch (role) { - case EndpointTypeRole: - return _endpoints.at(row).type; //context-menu, create-new - case EndpointNameRole: - return _endpoints.at(row).name; // Deck board - case EndpointUrlRole: - return _endpoints.at(row).url; // /ocs/v2.php/apps/declarativetest/newDeckBoard - case EndpointIconRole: - return _endpoints.at(row).icon; // zip - case EndpointFilterRole: - return _endpoints.at(row).filter; // image/ - case EndpointParameterRole: - return _endpoints.at(row).parameter; // fileId - case EndpointVerbRole: - return _endpoints.at(row).verb; // POST, GET - } - - return {}; -} - -int EndpointModel::rowCount(const QModelIndex &parent) const -{ - if (parent.isValid()) { - return 0; - } - - return _endpoints.size(); -} - -QHash EndpointModel::roleNames() const -{ - auto roles = QAbstractListModel::roleNames(); - roles[EndpointTypeRole] = "type"; - roles[EndpointNameRole] = "name"; - roles[EndpointUrlRole] = "url"; - roles[EndpointIconRole] = "icon"; - roles[EndpointFilterRole] = "filter"; - roles[EndpointParameterRole] = "parameter"; - roles[EndpointVerbRole] = "verb"; - - return roles; -} - -void EndpointModel::setAccountState(AccountState *accountState) -{ - if (accountState == nullptr) { - return; - } - - if (accountState == _accountState) { - return; - } - - _accountState = accountState; - parseEndpoints(); - Q_EMIT accountStateChanged(); -} - -void EndpointModel::setLocalPath(const QString &localPath) -{ - if (localPath.isEmpty()) { - return; - } - - if (localPath == _localPath) { - return; - } - - _localPath = localPath; - Q_EMIT localPathChanged(); -} - -void EndpointModel::setResponse(const Response &response) -{ - _response = response; - Q_EMIT responseChanged(); -} - -AccountState *EndpointModel::accountState() const -{ - return _accountState; -} - -QString EndpointModel::localPath() const -{ - return _localPath; -} - -QString EndpointModel::name() const -{ - return _response.name; -} - -void EndpointModel::setName(const QString &name) -{ - _response.name = name; -} - -QString EndpointModel::type() const -{ - return _response.type; -} - -void EndpointModel::setType(const QString &type) -{ - _response.type = type; -} - -QString EndpointModel::label() const -{ - return _response.label; -} - -void EndpointModel::setLabel(const QString &label) -{ - _response.label = label; -} - -QString EndpointModel::url() const -{ - return _response.url; -} - -void EndpointModel::setUrl(const QString &url) -{ - _response.url = url; -} - -QString EndpointModel::text() const -{ - return _response.text; -} - -void EndpointModel::setText(const QString &text) -{ - _response.text = text; -} - -void EndpointModel::createRequest(const int row) -{ - if (!_accountState) { - return; - } - - auto job = new JsonApiJob(_accountState->account(), - _endpoints.at(row).url, - this); - connect(job, &JsonApiJob::jsonReceived, - this, &EndpointModel::processRequest); - QUrlQuery params; - params.addQueryItem(_endpoints.at(row).parameter, 0); //fileId - job->addQueryParams(params); - job->setVerb(SimpleApiJob::Verb::Post); //fixit _endpoints.at(row).verb - job->start(); -} - -void EndpointModel::processRequest(const QJsonDocument &json) -{ - const auto root = json.object().value(QStringLiteral("root")).toObject(); - if (root.empty()) { - return; - } - const auto orientation = root.value(QStringLiteral("orientation")).toString(); - const auto rows = root.value(QStringLiteral("rows")).toArray(); - if (rows.empty()) { - 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(); - _response.name = child.value(QStringLiteral("element")).toString(); - _response.type = child.value(QStringLiteral("type")).toString(); - _response.label = child.value(QStringLiteral("label")).toString(); - _response.url = _accountState->account()->url().toString() + - child.value(QStringLiteral("url")).toString(); - _response.text = child.value(QStringLiteral("text")).toString(); - } - } - - Q_EMIT responseChanged(); -} - -} // namespace OCC diff --git a/src/gui/declarativeui/fileactionsmodel.cpp b/src/gui/declarativeui/fileactionsmodel.cpp new file mode 100644 index 0000000000000..04067ae5347c2 --- /dev/null +++ b/src/gui/declarativeui/fileactionsmodel.cpp @@ -0,0 +1,234 @@ +/* + * SPDX-FileCopyrightText: 2022 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" + +namespace OCC { + +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 _fileActions.at(row).params; // filePath + } + + 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"; + + return roles; +} + +AccountState *FileActionsModel::accountState() const +{ + return _accountState; +} + +void FileActionsModel::setAccountState(AccountState *accountState) +{ + if (accountState == nullptr) { + return; + } + + if (accountState == _accountState) { + return; + } + + _accountState = accountState; + 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 localPathChanged(); +} + +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)) { + qDebug() << "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; +} + +QMimeType FileActionsModel::mimeType() const +{ + return _mimeType; +} + +QString FileActionsModel::label() const +{ + return _response.label; +} + +void FileActionsModel::setLabel(const QString &label) +{ + _response.label = label; +} + +QString FileActionsModel::url() const +{ + return _response.url; +} + +void FileActionsModel::setUrl(const QString &url) +{ + _response.url = url; +} + +void FileActionsModel::setResponse(const Response &response) +{ + _response = response; + Q_EMIT responseChanged(); +} + +void FileActionsModel::parseEndpoints() +{ + if (!_accountState->isConnected()) { + return; + } + + if (_fileId.isEmpty()) { + return; + } + + if (!_mimeType.isValid()) { + return; + } + + const auto contextMenuList = _accountState->account()->capabilities().contextMenuByMimeType(_mimeType); + for (const auto &contextMenu : contextMenuList) { + _fileActions.append({_accountState->account()->url().toString() + + contextMenu.value("icon").toString(), + contextMenu.value("name").toString(), + contextMenu.value("url").toString(), + contextMenu.value("method").toString(), + contextMenu.value("params").toStringList()}); + } + + Q_EMIT fileActionModelChanged(); +} + +QString FileActionsModel::parseUrl(const QString &url) const +{ + auto unparsedUrl = url; + const auto parsedUrl = unparsedUrl.replace(QRegularExpression(fileIdUrlC), _fileId); + return parsedUrl; +} + +void FileActionsModel::createRequest(const int row) +{ + if (!_accountState) { + return; + } + + const auto requesturl = parseUrl(_fileActions.at(row).url); + auto job = new JsonApiJob(_accountState->account(), + requesturl, + this); + connect(job, &JsonApiJob::jsonReceived, + this, &FileActionsModel::processRequest); + QUrlQuery params; + for (const auto ¶m : _fileActions.at(row).params) { + if (param == fileIdC) { + params.addQueryItem(param, _fileId); + } + + if (param == filePathC) { + params.addQueryItem(param, _filePath); + } + } + job->addQueryParams(params); + const auto verb = job->stringToVerb(_fileActions.at(row).method); + job->setVerb(verb); + job->start(); +} + +void FileActionsModel::processRequest(const QJsonDocument &json, int statusCode) +{ + Q_UNUSED(json) + auto message = tr("File action succeded, access your instance for the result."); + if (statusCode != 200) { + message = tr("File action did not succeed, access your instance for details."); + return; + } + + _response.label = message; + _response.url = _accountState->account()->url().toString(); + + Q_EMIT responseChanged(); +} + +} // namespace OCC diff --git a/src/gui/declarativeui/endpointmodel.h b/src/gui/declarativeui/fileactionsmodel.h similarity index 55% rename from src/gui/declarativeui/endpointmodel.h rename to src/gui/declarativeui/fileactionsmodel.h index ceec6c5ec30c2..94088046646a4 100644 --- a/src/gui/declarativeui/endpointmodel.h +++ b/src/gui/declarativeui/fileactionsmodel.h @@ -12,55 +12,43 @@ namespace OCC { -class EndpointModel : public QAbstractListModel { +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 localPathChanged) - Q_PROPERTY(QString declarativeUiName READ name WRITE setName NOTIFY responseChanged) - Q_PROPERTY(QString declarativeUiType READ type WRITE setType NOTIFY responseChanged) - Q_PROPERTY(QString declarativeUiLabel READ type WRITE setLabel NOTIFY responseChanged) - Q_PROPERTY(QString declarativeUiUrl READ url WRITE setUrl NOTIFY responseChanged) - Q_PROPERTY(QString declarativeUiText READ text WRITE setText NOTIFY responseChanged) + Q_PROPERTY(QString responseLabel READ label WRITE setLabel NOTIFY responseChanged) + Q_PROPERTY(QString responseUrl READ url WRITE setUrl NOTIFY responseChanged) public: - explicit EndpointModel(QObject *const parent = nullptr); + 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 { - EndpointTypeRole = Qt::UserRole + 1, - EndpointNameRole, - EndpointUrlRole, - EndpointIconRole, - EndpointFilterRole, - EndpointParameterRole, - EndpointVerbRole + FileActionIconRole = Qt::UserRole + 1, + FileActionNameRole, + FileActionUrlRole, + FileActionMethodRole, + FileActionParamsRole }; Q_ENUM(DataRole) struct Response { - QString name; - QString type; QString label; QString url; - QString text; }; - void parseEndpoints(); - + [[nodiscard]] AccountState *accountState() const; void setAccountState(AccountState *accountState); - void setLocalPath(const QString &localPath); - void setResponse(const Response &response); - [[nodiscard]] AccountState *accountState() const; [[nodiscard]] QString localPath() const; + void setLocalPath(const QString &localPath); - [[nodiscard]] QString name() const; - void setName(const QString &name); - - [[nodiscard]] QString type() const; - void setType(const QString &type); + [[nodiscard]] QByteArray fileId() const; + [[nodiscard]] QMimeType mimeType() const; + void setupFileProperties(); [[nodiscard]] QString label() const; void setLabel(const QString &label); @@ -68,35 +56,40 @@ class EndpointModel : public QAbstractListModel { [[nodiscard]] QString url() const; void setUrl(const QString &url); - [[nodiscard]] QString text() const; - void setText(const QString &text); + void setResponse(const Response &response); + void parseEndpoints(); + QString parseUrl(const QString &url) const; signals: - void endpointModelChanged(); - void localPathChanged(); void accountStateChanged(); + void localPathChanged(); void responseChanged(); + void fileActionModelChanged(); public slots: void createRequest(const int row); - void processRequest(const QJsonDocument &json); + void processRequest(const QJsonDocument &json, int statusCode); private: Response _response; - - struct Endpoint { - QString type; + struct FileAction { + QString icon; QString name; QString url; - QString icon; - QString filter; - QString parameter; - QString verb; + QString method; + QList params; }; - QList _endpoints; + QList _fileActions; AccountState *_accountState; QString _localPath; + QByteArray _fileId; + QMimeType _mimeType; + QString _filePath; + + static constexpr char fileIdUrlC[] = "{fileId}"; + static constexpr char fileIdC[] = "fileId"; + static constexpr char filePathC[] = "filePath"; }; } diff --git a/src/gui/owncloudgui.cpp b/src/gui/owncloudgui.cpp index 35b8a6c7d3385..5ecdf4c6d1d9f 100644 --- a/src/gui/owncloudgui.cpp +++ b/src/gui/owncloudgui.cpp @@ -137,7 +137,7 @@ ownCloudGui::ownCloudGui(Application *parent) qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "SortedShareModel"); qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "SyncConflictsModel"); qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "DeclarativeUi"); - qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "EndpointModel"); + qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "FileActionsModel"); qmlRegisterUncreatableType("com.nextcloud.desktopclient", 1, 0, "QAbstractItemModel", "QAbstractItemModel"); qmlRegisterUncreatableType("com.nextcloud.desktopclient", 1, 0, "activity", "Activity"); diff --git a/src/gui/socketapi/socketapi.cpp b/src/gui/socketapi/socketapi.cpp index 4021e18d13085..49725041c04c5 100644 --- a/src/gui/socketapi/socketapi.cpp +++ b/src/gui/socketapi/socketapi.cpp @@ -1135,9 +1135,9 @@ void SocketApi::command_GET_STRINGS(const QString &argument, SocketListener *lis { 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() }, - { "FILE_ACTIONS_MENU_TITLE", tr("File actions") }, { "COPY_PRIVATE_LINK_MENU_TITLE", tr("Copy private link to clipboard") }, { "EMAIL_PRIVATE_LINK_MENU_TITLE", tr("Send private link by email …") }, { "CONTEXT_MENU_ICON", APPLICATION_ICON_NAME }, @@ -1184,6 +1184,14 @@ 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(); + const auto flagString = isOnTheServer ? 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, @@ -1368,6 +1376,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 3ed469baaf7a6..b6cfa102ef7b7 100644 --- a/src/gui/socketapi/socketapi.h +++ b/src/gui/socketapi/socketapi.h @@ -154,7 +154,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 6c7554bccbf3f..6be2fab4e833a 100644 --- a/src/gui/systray.cpp +++ b/src/gui/systray.cpp @@ -508,7 +508,7 @@ void Systray::createFileActionsDialog(const QString &localPath) const QVariantMap initialProperties{ {"accountState", QVariant::fromValue(folder->accountState())}, {"shortLocalPath", shortLocalPath}, - {"localPath", localPath} + {"localPath", localPath}, }; const auto fileActionsDialog = fileActionsQml.createWithInitialProperties(initialProperties); diff --git a/src/libsync/capabilities.cpp b/src/libsync/capabilities.cpp index af8a00a45211a..2ca4aa1ebeba2 100644 --- a/src/libsync/capabilities.cpp +++ b/src/libsync/capabilities.cpp @@ -442,11 +442,46 @@ bool Capabilities::serverHasDeclarativeUi() const return _capabilities[QStringLiteral("declarativeui")].toMap().isEmpty(); } -QVariantList Capabilities::declarativeUiEndpoints() const +QList Capabilities::declarativeUiContextMenu() const +{ + const auto declarativeUiMap = _capabilities.value("declarativeui").toMap(); + QList contextMenu; + for (auto declarativeUiApp : std::as_const(declarativeUiMap)) { + const auto contextMenuMap = declarativeUiApp.toMap(); + if (!contextMenuMap.contains("context-menu")) { + continue; + } + + for (const auto &contextMenuItem : contextMenuMap) { + const auto contextMenuList = contextMenuItem.toList(); + for (const auto &contextMenuMap : contextMenuList) { + contextMenu.append(contextMenuMap.toMap()); + } + } + } + + return contextMenu; +} + +QList Capabilities::contextMenuByMimeType(const QMimeType fileMimeType) const { - const auto declarativeUi = _capabilities.value("declarativeui").toMap(); - const auto hooks = declarativeUi.value("hooks").toList(); - return hooks; + const auto contextMenu = declarativeUiContextMenu(); + const auto fileMimeTypeName = fileMimeType.name(); + const auto fileMimeTypeAliases = fileMimeType.aliases(); + QList contextMenuByMimeType; + for (const auto &contextMenuMap : contextMenu) { + const auto mimetypeFilters = contextMenuMap.value("mimetype_filters").toString(); + const auto filesMimeTypeFilterList = mimetypeFilters.split(",", Qt::SkipEmptyParts); + for (const auto mimeType : filesMimeTypeFilterList) { + auto capabilitiesMimeTypeName = mimeType.trimmed(); + if (fileMimeTypeName.startsWith(capabilitiesMimeTypeName) || fileMimeTypeAliases.contains(capabilitiesMimeTypeName)) { + contextMenuByMimeType.append(contextMenuMap); + break; + } + } + } + + return contextMenuByMimeType; } /*-------------------------------------------------------------------------------------*/ diff --git a/src/libsync/capabilities.h b/src/libsync/capabilities.h index ece247a65be1d..06e43ebb7cf06 100644 --- a/src/libsync/capabilities.h +++ b/src/libsync/capabilities.h @@ -176,7 +176,8 @@ class OWNCLOUDSYNC_EXPORT Capabilities [[nodiscard]] QString desktopEnterpriseChannel() const; [[nodiscard]] bool serverHasDeclarativeUi() const; - [[nodiscard]] QVariantList declarativeUiEndpoints() const; + [[nodiscard]] QList declarativeUiContextMenu() const; + [[nodiscard]] QList contextMenuByMimeType(const QMimeType fileMimeType) const; // Direct Editing void addDirectEditor(DirectEditor* directEditor); 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. From 088bdc73aa8d9ac30714d9f1d7123e1a3808a3e6 Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Wed, 17 Sep 2025 18:01:57 +0200 Subject: [PATCH 05/22] feat(declarativeui): add logging category to FileActionsModel. - improve error handling. - display a different icon for each mimetype. Signed-off-by: Camila Ayres --- src/gui/declarativeui/FileActionsWindow.qml | 6 +-- src/gui/declarativeui/fileactionsmodel.cpp | 50 +++++++++++++++------ src/gui/declarativeui/fileactionsmodel.h | 22 +++++---- src/libsync/capabilities.cpp | 42 +++++++++-------- src/libsync/capabilities.h | 1 - theme.qrc.in | 1 + theme/file-open.svg | 1 + 7 files changed, 79 insertions(+), 44 deletions(-) create mode 100644 theme/file-open.svg diff --git a/src/gui/declarativeui/FileActionsWindow.qml b/src/gui/declarativeui/FileActionsWindow.qml index d1e58c9e9869e..b4bad7616a90b 100644 --- a/src/gui/declarativeui/FileActionsWindow.qml +++ b/src/gui/declarativeui/FileActionsWindow.qml @@ -49,7 +49,7 @@ ApplicationWindow { spacing: Style.standardSpacing Image { - source: "image://svgimage-custom-color/files.svg/" + palette.windowText + source: "image://svgimage-custom-color/file-open.svg/" + palette.windowText width: Style.minimumActivityItemHeight height: Style.minimumActivityItemHeight } @@ -93,7 +93,7 @@ ApplicationWindow { Button { id: responseButton - visible: response.text !== "" + visible: responseText.text !== "" flat: true Layout.fillWidth: true implicitHeight: Style.activityListButtonHeight @@ -118,7 +118,7 @@ ApplicationWindow { } Text { - id: response + id: responseText text: fileActionModel.responseLabel textFormat: Text.RichText color: Style.ncHeaderTextColor diff --git a/src/gui/declarativeui/fileactionsmodel.cpp b/src/gui/declarativeui/fileactionsmodel.cpp index 04067ae5347c2..ed8c7294ea645 100644 --- a/src/gui/declarativeui/fileactionsmodel.cpp +++ b/src/gui/declarativeui/fileactionsmodel.cpp @@ -10,6 +10,8 @@ namespace OCC { +Q_LOGGING_CATEGORY(lcFileActions, "nextcloud.gui.fileactions", QtInfoMsg) + FileActionsModel::FileActionsModel(QObject *parent) : QAbstractListModel(parent) { @@ -72,6 +74,7 @@ void FileActionsModel::setAccountState(AccountState *accountState) } _accountState = accountState; + _accountUrl = _accountState->account()->url().toString(); Q_EMIT accountStateChanged(); } @@ -96,7 +99,7 @@ void FileActionsModel::setLocalPath(const QString &localPath) setupFileProperties(); parseEndpoints(); - Q_EMIT localPathChanged(); + Q_EMIT fileChanged(); } QByteArray FileActionsModel::fileId() const @@ -110,7 +113,7 @@ void FileActionsModel::setupFileProperties() _filePath = _localPath.mid(folderForPath->cleanPath().length() + 1); SyncJournalFileRecord fileRecord; if (!folderForPath->journalDb()->getFileRecord(_filePath, &fileRecord)) { - qDebug() << "Invalid file record for path:" << _localPath; + qCWarning(lcFileActions) << "Invalid file record for path:" << _localPath; return; } @@ -121,6 +124,9 @@ void FileActionsModel::setupFileProperties() QMimeDatabase mimeDb; const auto mimeType = mimeDb.mimeTypeForFile(_localPath, mimeMatchMode); _mimeType = mimeType; + + // TODO: display an icon for each mimeType + _fileIcon = ""; } QMimeType FileActionsModel::mimeType() const @@ -128,22 +134,27 @@ QMimeType FileActionsModel::mimeType() const return _mimeType; } -QString FileActionsModel::label() const +QString FileActionsModel::fileIcon() const +{ + return _fileIcon; +} + +QString FileActionsModel::responseLabel() const { return _response.label; } -void FileActionsModel::setLabel(const QString &label) +void FileActionsModel::setResponseLabel(const QString &label) { _response.label = label; } -QString FileActionsModel::url() const +QString FileActionsModel::responseUrl() const { return _response.url; } -void FileActionsModel::setUrl(const QString &url) +void FileActionsModel::setResponseUrl(const QString &url) { _response.url = url; } @@ -157,18 +168,31 @@ void FileActionsModel::setResponse(const Response &response) 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().contextMenuByMimeType(_mimeType); + //const QList contextMenuList; + 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) { _fileActions.append({_accountState->account()->url().toString() + contextMenu.value("icon").toString(), @@ -178,6 +202,7 @@ void FileActionsModel::parseEndpoints() contextMenu.value("params").toStringList()}); } + qCDebug(lcFileActions) << "File" << _localPath << "has" << _fileActions.size() << "actions available."; Q_EMIT fileActionModelChanged(); } @@ -191,6 +216,7 @@ QString FileActionsModel::parseUrl(const QString &url) const void FileActionsModel::createRequest(const int row) { if (!_accountState) { + qCWarning(lcFileActions) << "No account state for" << _localPath; return; } @@ -210,7 +236,9 @@ void FileActionsModel::createRequest(const int row) params.addQueryItem(param, _filePath); } } - job->addQueryParams(params); + if (!params.isEmpty()) { + job->addQueryParams(params); + } const auto verb = job->stringToVerb(_fileActions.at(row).method); job->setVerb(verb); job->start(); @@ -221,14 +249,10 @@ void FileActionsModel::processRequest(const QJsonDocument &json, int statusCode) Q_UNUSED(json) auto message = tr("File action succeded, access your instance for the result."); if (statusCode != 200) { - message = tr("File action did not succeed, access your instance for details."); - return; + qCWarning(lcFileActions) << "File action did not succeed for" << _localPath; } - _response.label = message; - _response.url = _accountState->account()->url().toString(); - - Q_EMIT responseChanged(); + setResponse({ message, _accountState->account()->url().toString() }); } } // namespace OCC diff --git a/src/gui/declarativeui/fileactionsmodel.h b/src/gui/declarativeui/fileactionsmodel.h index 94088046646a4..4243b5f5b35cd 100644 --- a/src/gui/declarativeui/fileactionsmodel.h +++ b/src/gui/declarativeui/fileactionsmodel.h @@ -12,13 +12,16 @@ 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 localPathChanged) - Q_PROPERTY(QString responseLabel READ label WRITE setLabel NOTIFY responseChanged) - Q_PROPERTY(QString responseUrl READ url WRITE setUrl NOTIFY responseChanged) + 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); @@ -48,13 +51,14 @@ class FileActionsModel : public QAbstractListModel { [[nodiscard]] QByteArray fileId() const; [[nodiscard]] QMimeType mimeType() const; + [[nodiscard]] QString fileIcon() const; void setupFileProperties(); - [[nodiscard]] QString label() const; - void setLabel(const QString &label); + [[nodiscard]] QString responseLabel() const; + void setResponseLabel(const QString &label); - [[nodiscard]] QString url() const; - void setUrl(const QString &url); + [[nodiscard]] QString responseUrl() const; + void setResponseUrl(const QString &url); void setResponse(const Response &response); @@ -63,7 +67,7 @@ class FileActionsModel : public QAbstractListModel { signals: void accountStateChanged(); - void localPathChanged(); + void fileChanged(); void responseChanged(); void fileActionModelChanged(); @@ -86,6 +90,8 @@ public slots: QByteArray _fileId; QMimeType _mimeType; QString _filePath; + QString _accountUrl; + QString _fileIcon; static constexpr char fileIdUrlC[] = "{fileId}"; static constexpr char fileIdC[] = "fileId"; diff --git a/src/libsync/capabilities.cpp b/src/libsync/capabilities.cpp index 2ca4aa1ebeba2..f2aff98165705 100644 --- a/src/libsync/capabilities.cpp +++ b/src/libsync/capabilities.cpp @@ -442,42 +442,46 @@ bool Capabilities::serverHasDeclarativeUi() const return _capabilities[QStringLiteral("declarativeui")].toMap().isEmpty(); } -QList Capabilities::declarativeUiContextMenu() const +QList Capabilities::contextMenuByMimeType(const QMimeType fileMimeType) const { const auto declarativeUiMap = _capabilities.value("declarativeui").toMap(); - QList contextMenu; + QVariantList contextMenuMapList; for (auto declarativeUiApp : std::as_const(declarativeUiMap)) { - const auto contextMenuMap = declarativeUiApp.toMap(); - if (!contextMenuMap.contains("context-menu")) { + const auto declarativeUiContextMenuMap = declarativeUiApp.toMap(); + if (!declarativeUiContextMenuMap.contains("context-menu")) { continue; } - for (const auto &contextMenuItem : contextMenuMap) { - const auto contextMenuList = contextMenuItem.toList(); - for (const auto &contextMenuMap : contextMenuList) { - contextMenu.append(contextMenuMap.toMap()); - } - } + contextMenuMapList.append(declarativeUiContextMenuMap.value("context-menu").toList()); } - return contextMenu; -} + if (contextMenuMapList.empty()) { + qCDebug(lcServerCapabilities) << "There is no context menu available in the capabilities."; + return {}; + } -QList Capabilities::contextMenuByMimeType(const QMimeType fileMimeType) const -{ - const auto contextMenu = declarativeUiContextMenu(); const auto fileMimeTypeName = fileMimeType.name(); + qCDebug(lcServerCapabilities) << "Filtering file actions by mimeType:" << fileMimeTypeName; const auto fileMimeTypeAliases = fileMimeType.aliases(); + qCDebug(lcServerCapabilities) << "File actions mimeType aliases:" << fileMimeTypeAliases; + QList contextMenuByMimeType; - for (const auto &contextMenuMap : contextMenu) { + for (const auto &contextMenu : contextMenuMapList) { + const auto contextMenuMap = contextMenu.toMap(); const auto mimetypeFilters = contextMenuMap.value("mimetype_filters").toString(); const auto filesMimeTypeFilterList = mimetypeFilters.split(",", Qt::SkipEmptyParts); + for (const auto mimeType : filesMimeTypeFilterList) { auto capabilitiesMimeTypeName = mimeType.trimmed(); - if (fileMimeTypeName.startsWith(capabilitiesMimeTypeName) || fileMimeTypeAliases.contains(capabilitiesMimeTypeName)) { - contextMenuByMimeType.append(contextMenuMap); - break; + qCDebug(lcServerCapabilities) << "Context menu for mimeType:" << capabilitiesMimeTypeName; + + if (!fileMimeTypeName.startsWith(capabilitiesMimeTypeName) && !fileMimeTypeAliases.contains(capabilitiesMimeTypeName)) { + continue; } + + qCDebug(lcServerCapabilities) << "Found file action:" << contextMenuMap; + contextMenuByMimeType.append(contextMenuMap); + break; } } diff --git a/src/libsync/capabilities.h b/src/libsync/capabilities.h index 06e43ebb7cf06..85a14fbf4c579 100644 --- a/src/libsync/capabilities.h +++ b/src/libsync/capabilities.h @@ -176,7 +176,6 @@ class OWNCLOUDSYNC_EXPORT Capabilities [[nodiscard]] QString desktopEnterpriseChannel() const; [[nodiscard]] bool serverHasDeclarativeUi() const; - [[nodiscard]] QList declarativeUiContextMenu() const; [[nodiscard]] QList contextMenuByMimeType(const QMimeType fileMimeType) const; // Direct Editing diff --git a/theme.qrc.in b/theme.qrc.in index 4bf77d90807bc..6c71352a012f4 100644 --- a/theme.qrc.in +++ b/theme.qrc.in @@ -291,5 +291,6 @@ theme/chevron-double-up.svg theme/call-notification.wav theme/info.svg + theme/file-open.svg 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 From d14d741a5ab1a725af81cdfd388d8a05680bfdf3 Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Thu, 18 Sep 2025 02:06:58 +0200 Subject: [PATCH 06/22] feat(declarativeui/fileactions): return default icon when server doesn't have one. - improve file actions window UI. Signed-off-by: Camila Ayres --- src/gui/declarativeui/FileActionsWindow.qml | 246 +++++++++++--------- src/gui/declarativeui/fileactionsmodel.cpp | 22 +- src/gui/declarativeui/fileactionsmodel.h | 1 + theme.qrc.in | 2 + theme/backup.svg | 1 + theme/convert_to_text.svg | 1 + 6 files changed, 157 insertions(+), 116 deletions(-) create mode 100644 theme/backup.svg create mode 100644 theme/convert_to_text.svg diff --git a/src/gui/declarativeui/FileActionsWindow.qml b/src/gui/declarativeui/FileActionsWindow.qml index b4bad7616a90b..b1ae487370ea8 100644 --- a/src/gui/declarativeui/FileActionsWindow.qml +++ b/src/gui/declarativeui/FileActionsWindow.qml @@ -7,16 +7,15 @@ import QtQuick import QtQuick.Window import QtQuick.Layouts import QtQuick.Controls +import Qt5Compat.GraphicalEffects import com.nextcloud.desktopclient import Style ApplicationWindow { id: root - width: 400 - height: 300 - minimumWidth: 300 - minimumHeight: 200 - flags: Qt.Dialog + height: Style.trayWindowWidth + width: Systray.useNormalWindow ? Style.trayWindowHeight : Style.trayWindowWidth + flags: Systray.useNormalWindow ? Qt.Window : Qt.Dialog | Qt.FramelessWindowHint visible: true property var accountState: ({}) @@ -32,120 +31,146 @@ ApplicationWindow { localPath: root.localPath } - Rectangle { + background: Rectangle { + //radius: Systray.useNormalWindow ? 0.0 : Style.trayWindowRadius + border.width: Style.trayWindowBorderWidth + border.color: palette.dark + color: palette.window + } + + // TO FIX: OpacityMask { + // anchors.fill: parent + // anchors.margins: Style.trayWindowBorderWidth + // source: ShaderEffectSource { + // sourceItem: windowContent + // hideSource: true + // } + // maskSource: Rectangle { + // width: root.width + // height: root.height + // radius: Systray.useNormalWindow ? 0.0 : Style.trayWindowRadius + // } + // } + + ColumnLayout { + id: windowContent anchors.fill: parent - color: Style.infoBoxBackgroundColor - //radius: Style.trayWindowRadius - border.color: Style.accentColor + anchors.margins: Style.standardSpacing - ColumnLayout { - anchors.fill: parent - anchors.margins: Style.standardSpacing + RowLayout { + id: windowHeader + Layout.fillWidth: true spacing: Style.standardSpacing - RowLayout { - id: windowHeader - Layout.fillWidth: true - spacing: Style.standardSpacing - - Image { - source: "image://svgimage-custom-color/file-open.svg/" + palette.windowText - width: Style.minimumActivityItemHeight - height: Style.minimumActivityItemHeight - } - - ColumnLayout { - Layout.fillWidth: true - spacing: Style.extraSmallSpacing - - Label { - text: root.shortLocalPath - font.bold: true - font.pixelSize: Style.pixelSize - color: Style.ncHeaderTextColor - } - } + Image { + source: "image://svgimage-custom-color/file-open.svg/" + palette.windowText + width: Style.minimumActivityItemHeight + height: Style.minimumActivityItemHeight + Layout.alignment: Qt.AlignVCenter + Layout.margins: Style.extraSmallSpacing } - Rectangle { - id: lineTop + Label { + id: headerLocalPath + text: root.shortLocalPath + elide: Text.ElideRight + font.bold: true + font.pixelSize: Style.pixelSize + color: palette.text Layout.fillWidth: true - height: Style.extraExtraSmallSpacing - color: Style.accentColor + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft } - ListView { - id: fileActionsView - model: fileActionModel - clip: true - spacing: Style.trayHorizontalMargin - Layout.fillWidth: true - Layout.fillHeight: true - delegate: fileActionsDelegate + Button { + id: closeButton + flat: true + padding: 0 + 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: 0 + border.width: closeButton.hovered ? Style.trayWindowBorderWidth : 0 + border.color: palette.dark + anchors.fill: parent + Layout.margins: Style.extraSmallSpacing + } } + } - Rectangle { - id: lineBottom - Layout.fillWidth: true - height: Style.extraExtraSmallSpacing - color: Style.accentColor - } + Rectangle { + id: lineTop + Layout.fillWidth: true + height: Style.extraExtraSmallSpacing + color: palette.dark + } - Button { - id: responseButton - visible: responseText.text !== "" - flat: true - Layout.fillWidth: true - implicitHeight: Style.activityListButtonHeight + ListView { + id: fileActionsView + model: fileActionModel + clip: true + spacing: Style.trayHorizontalMargin + Layout.fillWidth: true + Layout.fillHeight: true + delegate: fileActionsDelegate + } - padding: 0 - leftPadding: Style.standardSpacing - rightPadding: Style.standardSpacing - spacing: Style.standardSpacing + Button { + id: responseButton + visible: responseText.text !== "" + flat: true + Layout.fillWidth: true + implicitHeight: Style.activityListButtonHeight - contentItem: Row { - id: responseContent - anchors.fill: parent - anchors.margins: Style.smallSpacing - spacing: Style.standardSpacing + padding: 0 + leftPadding: Style.smallSpacing + rightPadding: Style.smallSpacing + spacing: Style.standardSpacing - Image { - source: "image://svgimage-custom-color/public.svg/" + palette.windowText - width: Style.minimumActivityItemHeight - height: Style.minimumActivityItemHeight - fillMode: Image.PreserveAspectFit - anchors.verticalCenter: parent.verticalCenter - } + background: Rectangle { + //radius: Systray.useNormalWindow ? 0.0 : Style.trayWindowRadius + border.width: Style.trayWindowBorderWidth + border.color: palette.dark + color: palette.window + } - Text { - id: responseText - text: fileActionModel.responseLabel - textFormat: Text.RichText - color: Style.ncHeaderTextColor - font.pointSize: Style.pixelSize - font.underline: true - anchors.verticalCenter: parent.verticalCenter - MouseArea { - id: responseArea - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: Qt.openUrlExternally(fileActionModel.responseUrl) - } - } + contentItem: Row { + id: responseContent + anchors.fill: parent + anchors.margins: Style.smallSpacing + spacing: Style.halfTrayWindowRadius + Layout.fillWidth: true + + Image { + source: "image://svgimage-custom-color/backup.svg/" + palette.windowText + width: Style.accountAvatarStateIndicatorSize + height: Style.accountAvatarStateIndicatorSize + fillMode: Image.PreserveAspectFit + anchors.verticalCenter: parent.verticalCenter } - ToolTip { - visible: responseButton.hovered + Text { + id: responseText text: fileActionModel.responseLabel + textFormat: Text.RichText + color: palette.text + font.pointSize: Style.pixelSize + font.underline: true + anchors.verticalCenter: parent.verticalCenter } } - Rectangle { - id: repsonseLineBottom - visible: response.text != "" - Layout.fillWidth: true - height: Style.extraExtraSmallSpacing - color: Style.accentColor + MouseArea { + id: responseArea + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: Qt.openUrlExternally(fileActionModel.responseUrl) } } } @@ -155,8 +180,10 @@ ApplicationWindow { RowLayout { Layout.fillWidth: true + Layout.margins: Style.extraSmallSpacing spacing: Style.standardSpacing height: implicitHeight + width: implicitWidth required property string name required property int index @@ -169,8 +196,8 @@ ApplicationWindow { implicitHeight: Style.activityListButtonHeight padding: 0 - leftPadding: Style.standardSpacing - rightPadding: Style.standardSpacing + leftPadding: Style.smallSpacing + rightPadding: Style.smallSpacing spacing: Style.standardSpacing contentItem: Row { @@ -178,30 +205,31 @@ ApplicationWindow { anchors.fill: parent anchors.margins: Style.smallSpacing spacing: Style.standardSpacing + Layout.fillWidth: true Image { - source: icon - width: Style.minimumActivityItemHeight - height: Style.minimumActivityItemHeight + source: icon + palette.windowText + width: Style.activityListButtonHeight + height: Style.activityListButtonHeight fillMode: Image.PreserveAspectFit anchors.verticalCenter: parent.verticalCenter } Label { text: name - color: Style.ncHeaderTextColor - font.pixelSize: Style.pixelSize + color: palette.text + font.pixelSize: Style.defaultFontPtSize verticalAlignment: Text.AlignVCenter anchors.verticalCenter: parent.verticalCenter } } - ToolTip { - visible: fileActionButton.hovered - text: name + MouseArea { + id: fileActionMouseArea + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: fileActionModel.createRequest(index) } - - onClicked: fileActionModel.createRequest(index) } } } diff --git a/src/gui/declarativeui/fileactionsmodel.cpp b/src/gui/declarativeui/fileactionsmodel.cpp index ed8c7294ea645..8d01e95359fbc 100644 --- a/src/gui/declarativeui/fileactionsmodel.cpp +++ b/src/gui/declarativeui/fileactionsmodel.cpp @@ -194,12 +194,11 @@ void FileActionsModel::parseEndpoints() } for (const auto &contextMenu : contextMenuList) { - _fileActions.append({_accountState->account()->url().toString() - + contextMenu.value("icon").toString(), - contextMenu.value("name").toString(), - contextMenu.value("url").toString(), - contextMenu.value("method").toString(), - contextMenu.value("params").toStringList()}); + _fileActions.append({ parseIcon(contextMenu.value("icon").toString()), + contextMenu.value("name").toString(), + contextMenu.value("url").toString(), + contextMenu.value("method").toString(), + contextMenu.value("params").toStringList() }); } qCDebug(lcFileActions) << "File" << _localPath << "has" << _fileActions.size() << "actions available."; @@ -213,6 +212,15 @@ QString FileActionsModel::parseUrl(const QString &url) const 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) { @@ -251,7 +259,7 @@ void FileActionsModel::processRequest(const QJsonDocument &json, int statusCode) if (statusCode != 200) { qCWarning(lcFileActions) << "File action did not succeed for" << _localPath; } - + const auto folderForPath = FolderMan::instance()->folderForPath(_localPath); setResponse({ message, _accountState->account()->url().toString() }); } diff --git a/src/gui/declarativeui/fileactionsmodel.h b/src/gui/declarativeui/fileactionsmodel.h index 4243b5f5b35cd..37a4a9c1c5e2f 100644 --- a/src/gui/declarativeui/fileactionsmodel.h +++ b/src/gui/declarativeui/fileactionsmodel.h @@ -64,6 +64,7 @@ class FileActionsModel : public QAbstractListModel { void parseEndpoints(); QString parseUrl(const QString &url) const; + QString parseIcon(const QString &icon) const; signals: void accountStateChanged(); diff --git a/theme.qrc.in b/theme.qrc.in index 6c71352a012f4..15581d0db340e 100644 --- a/theme.qrc.in +++ b/theme.qrc.in @@ -292,5 +292,7 @@ theme/call-notification.wav theme/info.svg theme/file-open.svg + theme/backup.svg + theme/convert_to_text.svg 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 From ea59b6bc166cd2414c2a7ad24461fdbacb46ad5d Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Tue, 30 Sep 2025 12:44:07 +0200 Subject: [PATCH 07/22] fix(declarativeui/fileactions): parse response with declarative UI elements like url. - add opacity mask and more spacing. - implement hover for the file action buttons. - fix logic to display error/success messages. - improve text for error/success messages. Signed-off-by: Camila Ayres --- src/gui/declarativeui/FileActionsWindow.qml | 260 +++++++++++--------- src/gui/declarativeui/fileactionsmodel.cpp | 40 ++- src/gui/declarativeui/fileactionsmodel.h | 1 + 3 files changed, 174 insertions(+), 127 deletions(-) diff --git a/src/gui/declarativeui/FileActionsWindow.qml b/src/gui/declarativeui/FileActionsWindow.qml index b1ae487370ea8..420c7e6d98a3b 100644 --- a/src/gui/declarativeui/FileActionsWindow.qml +++ b/src/gui/declarativeui/FileActionsWindow.qml @@ -17,12 +17,15 @@ ApplicationWindow { width: Systray.useNormalWindow ? Style.trayWindowHeight : Style.trayWindowWidth flags: Systray.useNormalWindow ? Qt.Window : Qt.Dialog | Qt.FramelessWindowHint visible: true + color: "transparent" property var accountState: ({}) property string localPath: "" property string shortLocalPath: "" property var response: ({}) + readonly property int windowRadius: Systray.useNormalWindow ? 0.0 : Style.trayWindowRadius + title: qsTr("File actions for %1").arg(root.shortLocalPath) FileActionsModel { @@ -32,146 +35,150 @@ ApplicationWindow { } background: Rectangle { - //radius: Systray.useNormalWindow ? 0.0 : Style.trayWindowRadius + id: maskSource + radius: root.windowRadius border.width: Style.trayWindowBorderWidth border.color: palette.dark color: palette.window } - // TO FIX: OpacityMask { - // anchors.fill: parent - // anchors.margins: Style.trayWindowBorderWidth - // source: ShaderEffectSource { - // sourceItem: windowContent - // hideSource: true - // } - // maskSource: Rectangle { - // width: root.width - // height: root.height - // radius: Systray.useNormalWindow ? 0.0 : Style.trayWindowRadius - // } - // } - - ColumnLayout { - id: windowContent + OpacityMask { anchors.fill: parent - anchors.margins: Style.standardSpacing + anchors.margins: Style.trayWindowBorderWidth + source: maskSourceItem + maskSource: maskSource + } - RowLayout { - id: windowHeader - Layout.fillWidth: true - spacing: Style.standardSpacing + 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: "image://svgimage-custom-color/file-open.svg/" + palette.windowText + width: Style.minimumActivityItemHeight + height: Style.minimumActivityItemHeight + Layout.alignment: Qt.AlignVCenter + Layout.margins: Style.extraSmallSpacing + } + + Label { + 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 + } + } + } - Image { - source: "image://svgimage-custom-color/file-open.svg/" + palette.windowText - width: Style.minimumActivityItemHeight - height: Style.minimumActivityItemHeight - Layout.alignment: Qt.AlignVCenter - Layout.margins: Style.extraSmallSpacing + Rectangle { + id: lineTop + Layout.fillWidth: true + height: Style.extraExtraSmallSpacing + color: palette.dark } - Label { - id: headerLocalPath - text: root.shortLocalPath - elide: Text.ElideRight - font.bold: true - font.pixelSize: Style.pixelSize - color: palette.text + ListView { + id: fileActionsView + model: fileActionModel + clip: true + spacing: Style.trayHorizontalMargin Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + Layout.fillHeight: true + delegate: fileActionsDelegate } Button { - id: closeButton + id: responseButton + visible: responseText.text !== "" flat: true - padding: 0 - 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() + Layout.fillWidth: true + implicitHeight: Style.activityListButtonHeight + + padding: Style.standardSpacing + leftPadding: Style.standardSpacing + rightPadding: Style.standardSpacing + spacing: Style.standardSpacing + background: Rectangle { - color: "transparent" - radius: 0 - border.width: closeButton.hovered ? Style.trayWindowBorderWidth : 0 + radius: root.windowRadius + border.width: Style.trayWindowBorderWidth border.color: palette.dark - anchors.fill: parent - Layout.margins: Style.extraSmallSpacing + color: palette.window } - } - } - - Rectangle { - id: lineTop - Layout.fillWidth: true - height: 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: Style.activityListButtonHeight - - padding: 0 - leftPadding: Style.smallSpacing - rightPadding: Style.smallSpacing - spacing: Style.standardSpacing - - background: Rectangle { - //radius: Systray.useNormalWindow ? 0.0 : Style.trayWindowRadius - border.width: Style.trayWindowBorderWidth - border.color: palette.dark - color: palette.window - } + contentItem: Row { + id: responseContent + anchors.fill: parent + anchors.margins: Style.smallSpacing + spacing: Style.standardSpacing + padding: Style.standardSpacing + Layout.fillWidth: true - contentItem: Row { - id: responseContent - anchors.fill: parent - anchors.margins: Style.smallSpacing - spacing: Style.halfTrayWindowRadius - Layout.fillWidth: true + Image { + source: "image://svgimage-custom-color/backup.svg/" + palette.windowText + width: Style.accountAvatarStateIndicatorSize + height: Style.accountAvatarStateIndicatorSize + fillMode: Image.PreserveAspectFit + anchors.verticalCenter: parent.verticalCenter + } - Image { - source: "image://svgimage-custom-color/backup.svg/" + palette.windowText - width: Style.accountAvatarStateIndicatorSize - height: Style.accountAvatarStateIndicatorSize - fillMode: Image.PreserveAspectFit - anchors.verticalCenter: parent.verticalCenter + Text { + id: responseText + text: fileActionModel.responseLabel + textFormat: Text.RichText + color: palette.text + font.pointSize: Style.pixelSize + font.underline: true + anchors.verticalCenter: parent.verticalCenter + } } - Text { - id: responseText - text: fileActionModel.responseLabel - textFormat: Text.RichText - color: palette.text - font.pointSize: Style.pixelSize - font.underline: true - anchors.verticalCenter: parent.verticalCenter + MouseArea { + id: responseArea + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: Qt.openUrlExternally(fileActionModel.responseUrl) } } - - MouseArea { - id: responseArea - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: Qt.openUrlExternally(fileActionModel.responseUrl) - } } } @@ -180,10 +187,10 @@ ApplicationWindow { RowLayout { Layout.fillWidth: true - Layout.margins: Style.extraSmallSpacing + Layout.margins: Style.standardSpacing spacing: Style.standardSpacing height: implicitHeight - width: implicitWidth + width: parent.width required property string name required property int index @@ -195,15 +202,13 @@ ApplicationWindow { Layout.fillWidth: true implicitHeight: Style.activityListButtonHeight - padding: 0 - leftPadding: Style.smallSpacing - rightPadding: Style.smallSpacing + padding: Style.standardSpacing spacing: Style.standardSpacing contentItem: Row { id: fileActionsContent anchors.fill: parent - anchors.margins: Style.smallSpacing + anchors.margins: Style.standardSpacing spacing: Style.standardSpacing Layout.fillWidth: true @@ -224,11 +229,22 @@ ApplicationWindow { } } + background: Rectangle { + color: "transparent" + radius: root.windowRadius + border.width: parent.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(index) + onClicked: fileActionModel.createRequest(index) } } } diff --git a/src/gui/declarativeui/fileactionsmodel.cpp b/src/gui/declarativeui/fileactionsmodel.cpp index 8d01e95359fbc..44ed83ad42c6c 100644 --- a/src/gui/declarativeui/fileactionsmodel.cpp +++ b/src/gui/declarativeui/fileactionsmodel.cpp @@ -207,8 +207,8 @@ void FileActionsModel::parseEndpoints() QString FileActionsModel::parseUrl(const QString &url) const { - auto unparsedUrl = url; - const auto parsedUrl = unparsedUrl.replace(QRegularExpression(fileIdUrlC), _fileId); + auto parsedUrl = url; + parsedUrl.replace(fileIdUrlC, _fileId); return parsedUrl; } @@ -249,18 +249,48 @@ void FileActionsModel::createRequest(const int row) } 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) { - Q_UNUSED(json) - auto message = tr("File action succeded, access your instance for the result."); + 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); - setResponse({ message, _accountState->account()->url().toString() }); + 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 orientation = root.value(QStringLiteral("orientation")).toString(); + 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/declarativeui/fileactionsmodel.h b/src/gui/declarativeui/fileactionsmodel.h index 37a4a9c1c5e2f..8b159d61cd363 100644 --- a/src/gui/declarativeui/fileactionsmodel.h +++ b/src/gui/declarativeui/fileactionsmodel.h @@ -97,6 +97,7 @@ public slots: static constexpr char fileIdUrlC[] = "{fileId}"; static constexpr char fileIdC[] = "fileId"; static constexpr char filePathC[] = "filePath"; + static constexpr char rowC[] = "row"; }; } From abd44e9b00d13c524a653eddc69e5c0e6a19ecf7 Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Wed, 1 Oct 2025 16:13:29 +0200 Subject: [PATCH 08/22] refactor(declarativeui): remove unused DeclarativeUi class. The declarative ui in fact will be returned in the response from file actions. Signed-off-by: Camila Ayres --- src/gui/application.cpp | 3 - src/gui/declarativeui/DeclarativeUiWindow.qml | 90 ------------------- src/gui/owncloudgui.cpp | 8 +- src/gui/owncloudgui.h | 1 - src/gui/systray.cpp | 47 ---------- src/gui/systray.h | 3 - 6 files changed, 1 insertion(+), 151 deletions(-) delete mode 100644 src/gui/declarativeui/DeclarativeUiWindow.qml diff --git a/src/gui/application.cpp b/src/gui/application.cpp index 802701d9e0484..db3ad8a3ec064 100644 --- a/src/gui/application.cpp +++ b/src/gui/application.cpp @@ -435,9 +435,6 @@ Application::Application(int &argc, char **argv) connect(FolderMan::instance()->socketApi(), &SocketApi::fileActionsCommandReceived, _gui.data(), &ownCloudGui::slotShowFileActionsDialog); - connect(FolderMan::instance()->socketApi(), &SocketApi::declarativeUiCommandReceived, - _gui.data(), &ownCloudGui::slotShowDeclarativeUiDialog); - // startup procedure. connect(&_checkConnectionTimer, &QTimer::timeout, this, &Application::slotCheckConnection); _checkConnectionTimer.setInterval(ConnectionValidator::DefaultCallingIntervalMsec); // check for connection every 32 seconds. diff --git a/src/gui/declarativeui/DeclarativeUiWindow.qml b/src/gui/declarativeui/DeclarativeUiWindow.qml deleted file mode 100644 index d08f05543e79d..0000000000000 --- a/src/gui/declarativeui/DeclarativeUiWindow.qml +++ /dev/null @@ -1,90 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -import QtQuick -import QtQuick.Window -import QtQuick.Layouts -import QtQuick.Controls - -import com.nextcloud.desktopclient -import Style - -ApplicationWindow { - id: root - width: 400 - height: 500 - minimumWidth: 300 - minimumHeight: 300 - LayoutMirroring.childrenInherit: true - LayoutMirroring.enabled: Application.layoutDirection === Qt.RightToLeft - - property var accountState: ({}) - property string localPath: "" - - title: qsTr("Declarative UI for %1").arg(root.localPath) - - Component { - id: declarativeUiDelegate - - Item { - id: declarativeUiItem - width: parent.width - height: 40 - - required property string name - required property string type - required property string label - required property string url - required property string text - - Row { - anchors.fill: parent - anchors.margins: 8 - spacing: 5 - height: implicitHeight - - Text { - text: declarativeUiItem.text - color: Style.accentColor - font.pixelSize: Style.pixelSize - verticalAlignment: Text.AlignVCenter - visible: declarativeUiItem.name == "Text" - } - - Image { - source: declarativeUiItem.url - width: 50 - height: 50 - verticalAlignment: Text.AlignVCenter - visible: declarativeUiItem.name == "Image" - } - - Button { - text: declarativeUiItem.label - width: 120 - height: 30 - visible: declarativeUiItem.name == "Button" - } - } - } - } - - DeclarativeUi { - id: declarativeUi - accountState: root.accountState - localPath: root.localPath - } - - ListView { - id: declarativeUiView - model: declarativeUi.declarativeUiModel - delegate: declarativeUiDelegate - - anchors.fill: parent - anchors.margins: 10 - } - - -} diff --git a/src/gui/owncloudgui.cpp b/src/gui/owncloudgui.cpp index 5ecdf4c6d1d9f..7eaa44d6f10d0 100644 --- a/src/gui/owncloudgui.cpp +++ b/src/gui/owncloudgui.cpp @@ -33,7 +33,7 @@ #include "tray/sortedactivitylistmodel.h" #include "tray/syncstatussummary.h" #include "tray/unifiedsearchresultslistmodel.h" -#include "declarativeui/declarativeui.h" +#include "declarativeui/fileactionsmodel.h" #include "filesystem.h" #ifdef WITH_LIBCLOUDPROVIDERS @@ -136,7 +136,6 @@ 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, "DeclarativeUi"); qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "FileActionsModel"); qmlRegisterUncreatableType("com.nextcloud.desktopclient", 1, 0, "QAbstractItemModel", "QAbstractItemModel"); @@ -719,11 +718,6 @@ void ownCloudGui::slotShowFileActivityDialog(const QString &localPath) const _tray->createFileActivityDialog(localPath); } -void ownCloudGui::slotShowDeclarativeUiDialog(const QString &localPath) const -{ - _tray->showDeclarativeUiDialog(localPath); -} - void ownCloudGui::slotShowFileActionsDialog(const QString &localPath) const { _tray->showFileActionsDialog(localPath); diff --git a/src/gui/owncloudgui.h b/src/gui/owncloudgui.h index b069a900c2645..187fbbee0271e 100644 --- a/src/gui/owncloudgui.h +++ b/src/gui/owncloudgui.h @@ -95,7 +95,6 @@ public slots: */ void slotShowShareDialog(const QString &localPath) const; void slotShowFileActivityDialog(const QString &localPath) const; - void slotShowDeclarativeUiDialog(const QString &localPath) const; void slotShowFileActionsDialog(const QString &localPath) const; void slotNewAccountWizard(); diff --git a/src/gui/systray.cpp b/src/gui/systray.cpp index 6be2fab4e833a..84e1707ec88e5 100644 --- a/src/gui/systray.cpp +++ b/src/gui/systray.cpp @@ -437,52 +437,11 @@ void Systray::createFileActivityDialog(const QString &localPath) Q_EMIT showFileDetailsPage(localPath, FileDetailsPage::Activity); } -void Systray::showDeclarativeUiDialog(const QString &localPath) -{ - createDeclarativeUiDialog(localPath); -} - void Systray::showFileActionsDialog(const QString &localPath) { createFileActionsDialog(localPath); } -void Systray::createDeclarativeUiDialog(const QString &localPath) -{ - if (!_trayEngine) { - qCWarning(lcSystray) << "Could not open declarative UI dialog for" << localPath << "as no tray engine was available"; - return; - } - - const auto folder = FolderMan::instance()->folderForPath(localPath); - if (!folder) { - qCWarning(lcSystray) << "Could not open declarative UI dialog for" << localPath << "no responsible folder found"; - return; - } - - QQmlComponent declarativeUiQml(trayEngine(), QStringLiteral("qrc:/qml/src/gui/declarativeui/DeclarativeUiWindow.qml")); - if (declarativeUiQml.isError()) { - qCWarning(lcSystray) << declarativeUiQml.errorString(); - qCWarning(lcSystray) << declarativeUiQml.errors(); - return; - } - - const QVariantMap initialProperties{ - {"accountState", QVariant::fromValue(folder->accountState())}, - {"localPath", localPath}, - }; - const auto declarativeUiDialog = declarativeUiQml.createWithInitialProperties(initialProperties); - const auto dialog = qobject_cast(declarativeUiDialog); - if (!dialog) { - qCWarning(lcSystray) << "Declarative UI dialog window resulted in creation of object that was not a window!"; - return; - } - - dialog->show(); - dialog->raise(); - dialog->requestActivate(); -} - void Systray::createFileActionsDialog(const QString &localPath) { if (!_trayEngine) { @@ -535,12 +494,6 @@ void Systray::presentShareViewInTray(const QString &localPath) Q_EMIT showFileDetails(folder->accountState(), localPath, FileDetailsPage::Sharing); } -void Systray::presentDeclarativeUiViewInSystray(const QString &localPath) -{ - qCDebug(lcSystray) << "Opening declarative ui view in tray for " << localPath; - createDeclarativeUiDialog(localPath); -} - void Systray::presentFileActionsViewInSystray(const QString &localPath) { qCDebug(lcSystray) << "Opening file actions view in tray for " << localPath; diff --git a/src/gui/systray.h b/src/gui/systray.h index cc17353503dce..4cc3e4b0a2b65 100644 --- a/src/gui/systray.h +++ b/src/gui/systray.h @@ -146,11 +146,9 @@ public slots: void createShareDialog(const QString &localPath); void createFileActivityDialog(const QString &localPath); - void showDeclarativeUiDialog(const QString &localPath); void showFileActionsDialog(const QString &localPath); void presentShareViewInTray(const QString &localPath); - void presentDeclarativeUiViewInSystray(const QString &localPath); void presentFileActionsViewInSystray(const QString &localPath); private slots: @@ -169,7 +167,6 @@ private slots: void setupContextMenu(); void createFileDetailsDialog(const QString &localPath); - void createDeclarativeUiDialog(const QString &localPath); void createFileActionsDialog(const QString &localPath); [[nodiscard]] QScreen *currentScreen() const; From 50621a57402e58041c16d3cc4820f1f592a4b64b Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Wed, 1 Oct 2025 16:35:08 +0200 Subject: [PATCH 09/22] fix(declarativeui): files license. Signed-off-by: Camila Ayres --- REUSE.toml | 2 +- src/gui/declarativeui/FileActionsWindow.qml | 2 +- src/gui/declarativeui/fileactionsmodel.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/src/gui/declarativeui/FileActionsWindow.qml b/src/gui/declarativeui/FileActionsWindow.qml index 420c7e6d98a3b..871c2a89d886f 100644 --- a/src/gui/declarativeui/FileActionsWindow.qml +++ b/src/gui/declarativeui/FileActionsWindow.qml @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: GPL-2.0-or-later */ diff --git a/src/gui/declarativeui/fileactionsmodel.cpp b/src/gui/declarativeui/fileactionsmodel.cpp index 44ed83ad42c6c..4e46019f04bcb 100644 --- a/src/gui/declarativeui/fileactionsmodel.cpp +++ b/src/gui/declarativeui/fileactionsmodel.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: GPL-2.0-or-later */ From 8707fe52abc0164cf07acf612da5a7de3310fbca Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Wed, 1 Oct 2025 17:23:24 +0200 Subject: [PATCH 10/22] refactor(declarativeui): remove Declarative UI menu item. - use reference type in for loop. - fix spaces and sizes in the file actions window. - remove missing file from resources. Signed-off-by: Camila Ayres --- resources.qrc | 1 - src/gui/declarativeui/FileActionsWindow.qml | 55 ++++++++++++--------- src/gui/tray/ActivityItemContent.qml | 8 --- src/libsync/capabilities.cpp | 6 +-- theme/Style/Style.qml | 2 + 5 files changed, 38 insertions(+), 34 deletions(-) diff --git a/resources.qrc b/resources.qrc index 0ce9ba9810fd4..4d6ff74aef0c3 100644 --- a/resources.qrc +++ b/resources.qrc @@ -61,7 +61,6 @@ src/gui/ConflictItemFileInfo.qml src/gui/macOS/ui/FileProviderSettings.qml src/gui/macOS/ui/FileProviderFileDelegate.qml - src/gui/declarativeui/DeclarativeUiWindow.qml src/gui/declarativeui/FileActionsWindow.qml diff --git a/src/gui/declarativeui/FileActionsWindow.qml b/src/gui/declarativeui/FileActionsWindow.qml index 871c2a89d886f..47280fff6cff6 100644 --- a/src/gui/declarativeui/FileActionsWindow.qml +++ b/src/gui/declarativeui/FileActionsWindow.qml @@ -3,6 +3,8 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ +pragma ComponentBehavior: Bound + import QtQuick import QtQuick.Window import QtQuick.Layouts @@ -13,8 +15,8 @@ import Style ApplicationWindow { id: root - height: Style.trayWindowWidth - width: Systray.useNormalWindow ? Style.trayWindowHeight : Style.trayWindowWidth + height: Style.filesActionsHeight + width: Style.filesActionsWidth flags: Systray.useNormalWindow ? Qt.Window : Qt.Dialog | Qt.FramelessWindowHint visible: true color: "transparent" @@ -69,8 +71,8 @@ ApplicationWindow { Image { source: "image://svgimage-custom-color/file-open.svg/" + palette.windowText - width: Style.minimumActivityItemHeight - height: Style.minimumActivityItemHeight + Layout.maximumWidth: Style.minimumActivityItemHeight + Layout.maximumHeight: Style.minimumActivityItemHeight Layout.alignment: Qt.AlignVCenter Layout.margins: Style.extraSmallSpacing } @@ -112,7 +114,7 @@ ApplicationWindow { Rectangle { id: lineTop Layout.fillWidth: true - height: Style.extraExtraSmallSpacing + Layout.minimumHeight: Style.extraExtraSmallSpacing color: palette.dark } @@ -131,7 +133,7 @@ ApplicationWindow { visible: responseText.text !== "" flat: true Layout.fillWidth: true - implicitHeight: Style.activityListButtonHeight + implicitHeight: responseContent.implicitHeight padding: Style.standardSpacing leftPadding: Style.standardSpacing @@ -139,26 +141,31 @@ ApplicationWindow { 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: Row { + contentItem: RowLayout { id: responseContent anchors.fill: parent anchors.margins: Style.smallSpacing spacing: Style.standardSpacing - padding: Style.standardSpacing Layout.fillWidth: true + Layout.minimumHeight: Style.accountAvatarStateIndicatorSize Image { source: "image://svgimage-custom-color/backup.svg/" + palette.windowText - width: Style.accountAvatarStateIndicatorSize - height: Style.accountAvatarStateIndicatorSize + // Layout.preferredWidth: Style.accountAvatarStateIndicatorSize + // Layout.preferredHeight: Style.accountAvatarStateIndicatorSize + Layout.minimumWidth: Style.accountAvatarStateIndicatorSize + Layout.minimumHeight: Style.accountAvatarStateIndicatorSize fillMode: Image.PreserveAspectFit - anchors.verticalCenter: parent.verticalCenter + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + Layout.leftMargin: Style.standardSpacing } Text { @@ -168,7 +175,10 @@ ApplicationWindow { color: palette.text font.pointSize: Style.pixelSize font.underline: true - anchors.verticalCenter: parent.verticalCenter + wrapMode: Text.WordWrap + Layout.fillWidth: true + bottomPadding: Style.standardSpacing + Layout.alignment: Qt.AlignVCenter } } @@ -186,9 +196,8 @@ ApplicationWindow { id: fileActionsDelegate RowLayout { + id: fileAction Layout.fillWidth: true - Layout.margins: Style.standardSpacing - spacing: Style.standardSpacing height: implicitHeight width: parent.width @@ -203,25 +212,27 @@ ApplicationWindow { implicitHeight: Style.activityListButtonHeight padding: Style.standardSpacing - spacing: Style.standardSpacing contentItem: Row { id: fileActionsContent anchors.fill: parent - anchors.margins: Style.standardSpacing + anchors.topMargin: Style.standardSpacing + anchors.rightMargin: Style.standardSpacing + anchors.bottomMargin: Style.standardSpacing + anchors.leftMargin: Style.smallSpacing spacing: Style.standardSpacing Layout.fillWidth: true Image { - source: icon + palette.windowText - width: Style.activityListButtonHeight - height: Style.activityListButtonHeight + source: fileAction.icon + palette.windowText + width: Style.activityListButtonIconSize + height: Style.activityListButtonIconSize fillMode: Image.PreserveAspectFit anchors.verticalCenter: parent.verticalCenter } Label { - text: name + text: fileAction.name color: palette.text font.pixelSize: Style.defaultFontPtSize verticalAlignment: Text.AlignVCenter @@ -232,7 +243,7 @@ ApplicationWindow { background: Rectangle { color: "transparent" radius: root.windowRadius - border.width: parent.hovered ? Style.trayWindowBorderWidth : 0 + border.width: fileActionButton.hovered ? Style.trayWindowBorderWidth : 0 border.color: palette.dark anchors.margins: Style.standardSpacing height: parent.height @@ -244,7 +255,7 @@ ApplicationWindow { anchors.fill: parent anchors.margins: Style.standardSpacing cursorShape: Qt.PointingHandCursor - onClicked: fileActionModel.createRequest(index) + onClicked: fileActionModel.createRequest(fileAction.index) } } } diff --git a/src/gui/tray/ActivityItemContent.qml b/src/gui/tray/ActivityItemContent.qml index 6bbf27ae5e95e..747483b9a7897 100644 --- a/src/gui/tray/ActivityItemContent.qml +++ b/src/gui/tray/ActivityItemContent.qml @@ -206,14 +206,6 @@ RowLayout { hoverEnabled: true onClicked: Systray.presentFileActionsViewInSystray(model.openablePath) } - - MenuItem { - height: visible ? implicitHeight : 0 - text: qsTr("Declarative UI") - font.pixelSize: Style.topLinePixelSize - hoverEnabled: true - onClicked: Systray.presentDeclarativeUiViewInSystray(model.openablePath) - } } } diff --git a/src/libsync/capabilities.cpp b/src/libsync/capabilities.cpp index f2aff98165705..b999a797f8fa1 100644 --- a/src/libsync/capabilities.cpp +++ b/src/libsync/capabilities.cpp @@ -446,7 +446,7 @@ QList Capabilities::contextMenuByMimeType(const QMimeType fileMimeT { const auto declarativeUiMap = _capabilities.value("declarativeui").toMap(); QVariantList contextMenuMapList; - for (auto declarativeUiApp : std::as_const(declarativeUiMap)) { + for (const auto &declarativeUiApp : std::as_const(declarativeUiMap)) { const auto declarativeUiContextMenuMap = declarativeUiApp.toMap(); if (!declarativeUiContextMenuMap.contains("context-menu")) { continue; @@ -466,12 +466,12 @@ QList Capabilities::contextMenuByMimeType(const QMimeType fileMimeT qCDebug(lcServerCapabilities) << "File actions mimeType aliases:" << fileMimeTypeAliases; QList contextMenuByMimeType; - for (const auto &contextMenu : contextMenuMapList) { + 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); - for (const auto mimeType : filesMimeTypeFilterList) { + for (const auto &mimeType : std::as_const(filesMimeTypeFilterList)) { auto capabilitiesMimeTypeName = mimeType.trimmed(); qCDebug(lcServerCapabilities) << "Context menu for mimeType:" << capabilitiesMimeTypeName; 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%. From 776a5b13b79cb0f40b1322a2ca451d4415a61cff Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Tue, 20 Jan 2026 20:10:29 +0100 Subject: [PATCH 11/22] fix(capabilities): update capabilities with new json key + adjust mimetype check. Signed-off-by: Camila Ayres --- src/libsync/capabilities.cpp | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/libsync/capabilities.cpp b/src/libsync/capabilities.cpp index b999a797f8fa1..1091a2f7e07b9 100644 --- a/src/libsync/capabilities.cpp +++ b/src/libsync/capabilities.cpp @@ -439,12 +439,12 @@ QStringList Capabilities::forbiddenFilenameExtensions() const bool Capabilities::serverHasDeclarativeUi() const { - return _capabilities[QStringLiteral("declarativeui")].toMap().isEmpty(); + return _capabilities[QStringLiteral("client_integration")].toMap().isEmpty(); } QList Capabilities::contextMenuByMimeType(const QMimeType fileMimeType) const { - const auto declarativeUiMap = _capabilities.value("declarativeui").toMap(); + const auto declarativeUiMap = _capabilities.value("client_integration").toMap(); QVariantList contextMenuMapList; for (const auto &declarativeUiApp : std::as_const(declarativeUiMap)) { const auto declarativeUiContextMenuMap = declarativeUiApp.toMap(); @@ -464,6 +464,8 @@ QList Capabilities::contextMenuByMimeType(const QMimeType fileMimeT qCDebug(lcServerCapabilities) << "Filtering file actions by mimeType:" << fileMimeTypeName; const auto fileMimeTypeAliases = fileMimeType.aliases(); qCDebug(lcServerCapabilities) << "File actions mimeType aliases:" << fileMimeTypeAliases; + const auto fileMimeTypeParents = fileMimeType.parentMimeTypes(); + qCDebug(lcServerCapabilities) << "File actions parent mimeTypes:" << fileMimeTypeParents; QList contextMenuByMimeType; for (const auto &contextMenu : std::as_const(contextMenuMapList)) { @@ -472,10 +474,24 @@ QList Capabilities::contextMenuByMimeType(const QMimeType fileMimeT const auto filesMimeTypeFilterList = mimetypeFilters.split(",", Qt::SkipEmptyParts); for (const auto &mimeType : std::as_const(filesMimeTypeFilterList)) { - auto capabilitiesMimeTypeName = mimeType.trimmed(); - qCDebug(lcServerCapabilities) << "Context menu for mimeType:" << capabilitiesMimeTypeName; + 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 (!fileMimeTypeName.startsWith(capabilitiesMimeTypeName) && !fileMimeTypeAliases.contains(capabilitiesMimeTypeName)) { + if (!fileMimeTypeName.startsWith(capabilitiesMimeType) + && !fileMimeTypeName.contains(capabilitiesMimeType) + && !fileMimeType.inherits(capabilitiesMimeType) && + !fileMimeTypeName.startsWith(mimeTypeAlias) + && !fileMimeTypeName.contains(mimeTypeAlias) + && !fileMimeType.inherits(mimeTypeAlias)) { continue; } From cd70372feb3b567757c45a02592c5469eb9a1184 Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Wed, 21 Jan 2026 12:12:11 +0100 Subject: [PATCH 12/22] fix(declarativeui): use EnforcedPlainTextLabel instead of Label. Signed-off-by: Camila Ayres --- src/gui/declarativeui/FileActionsWindow.qml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/gui/declarativeui/FileActionsWindow.qml b/src/gui/declarativeui/FileActionsWindow.qml index 47280fff6cff6..2ef9f9987b15a 100644 --- a/src/gui/declarativeui/FileActionsWindow.qml +++ b/src/gui/declarativeui/FileActionsWindow.qml @@ -12,6 +12,7 @@ import QtQuick.Controls import Qt5Compat.GraphicalEffects import com.nextcloud.desktopclient import Style +import "./../tray" ApplicationWindow { id: root @@ -77,7 +78,7 @@ ApplicationWindow { Layout.margins: Style.extraSmallSpacing } - Label { + EnforcedPlainTextLabel { id: headerLocalPath text: root.shortLocalPath elide: Text.ElideRight @@ -231,7 +232,7 @@ ApplicationWindow { anchors.verticalCenter: parent.verticalCenter } - Label { + EnforcedPlainTextLabel { text: fileAction.name color: palette.text font.pixelSize: Style.defaultFontPtSize From 804cb9cbef9f936527a8b778a9fbfb1af8536428 Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Wed, 21 Jan 2026 12:20:32 +0100 Subject: [PATCH 13/22] fix(capabilities): an empty mimetype_filters means the action is available to all mimetypes. Signed-off-by: Camila Ayres --- src/libsync/capabilities.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/libsync/capabilities.cpp b/src/libsync/capabilities.cpp index 1091a2f7e07b9..32a188e911b4c 100644 --- a/src/libsync/capabilities.cpp +++ b/src/libsync/capabilities.cpp @@ -473,6 +473,12 @@ QList Capabilities::contextMenuByMimeType(const QMimeType fileMimeT 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; + contextMenuByMimeType.append(contextMenuMap); + continue; + } + for (const auto &mimeType : std::as_const(filesMimeTypeFilterList)) { auto capabilitiesMimeType = mimeType.trimmed(); QString mimeTypeAlias; @@ -486,7 +492,8 @@ QList Capabilities::contextMenuByMimeType(const QMimeType fileMimeT qCDebug(lcServerCapabilities) << fileMimeTypeName << "inherits" << capabilitiesMimeType << "?" << fileMimeType.inherits(capabilitiesMimeType); - if (!fileMimeTypeName.startsWith(capabilitiesMimeType) + if (!capabilitiesMimeType.isEmpty() + && !fileMimeTypeName.startsWith(capabilitiesMimeType) && !fileMimeTypeName.contains(capabilitiesMimeType) && !fileMimeType.inherits(capabilitiesMimeType) && !fileMimeTypeName.startsWith(mimeTypeAlias) @@ -497,7 +504,6 @@ QList Capabilities::contextMenuByMimeType(const QMimeType fileMimeT qCDebug(lcServerCapabilities) << "Found file action:" << contextMenuMap; contextMenuByMimeType.append(contextMenuMap); - break; } } From fc1a41ceec00fdf902f4b9530077c8f40c9524d8 Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Wed, 21 Jan 2026 15:49:28 +0100 Subject: [PATCH 14/22] fix(declarativeui/fileactions): handle the new params format. Signed-off-by: Camila Ayres --- src/gui/declarativeui/fileactionsmodel.cpp | 36 ++++++++++++++-------- src/gui/declarativeui/fileactionsmodel.h | 12 ++++++-- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/gui/declarativeui/fileactionsmodel.cpp b/src/gui/declarativeui/fileactionsmodel.cpp index 4e46019f04bcb..983f71a1bb7c1 100644 --- a/src/gui/declarativeui/fileactionsmodel.cpp +++ b/src/gui/declarativeui/fileactionsmodel.cpp @@ -31,7 +31,7 @@ QVariant FileActionsModel::data(const QModelIndex &index, int role) const case FileActionMethodRole: return _fileActions.at(row).method; // GET case FileActionParamsRole: - return _fileActions.at(row).params; // filePath + return QVariant::fromValue>(_fileActions.at(row).params); } return {}; @@ -194,11 +194,29 @@ void FileActionsModel::parseEndpoints() } for (const auto &contextMenu : contextMenuList) { + ParamsList queryParams; + 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()) { + queryParams.append( QueryItem{ name, value } ); + } + } + _fileActions.append({ parseIcon(contextMenu.value("icon").toString()), contextMenu.value("name").toString(), contextMenu.value("url").toString(), contextMenu.value("method").toString(), - contextMenu.value("params").toStringList() }); + queryParams }); } qCDebug(lcFileActions) << "File" << _localPath << "has" << _fileActions.size() << "actions available."; @@ -234,18 +252,10 @@ void FileActionsModel::createRequest(const int row) this); connect(job, &JsonApiJob::jsonReceived, this, &FileActionsModel::processRequest); - QUrlQuery params; for (const auto ¶m : _fileActions.at(row).params) { - if (param == fileIdC) { - params.addQueryItem(param, _fileId); - } - - if (param == filePathC) { - params.addQueryItem(param, _filePath); - } - } - if (!params.isEmpty()) { - job->addQueryParams(params); + QUrlQuery query; + query.addQueryItem(param.name, param.value); + job->addQueryParams(query); } const auto verb = job->stringToVerb(_fileActions.at(row).method); job->setVerb(verb); diff --git a/src/gui/declarativeui/fileactionsmodel.h b/src/gui/declarativeui/fileactionsmodel.h index 8b159d61cd363..0545eafdfae5f 100644 --- a/src/gui/declarativeui/fileactionsmodel.h +++ b/src/gui/declarativeui/fileactionsmodel.h @@ -43,6 +43,12 @@ class FileActionsModel : public QAbstractListModel { QString url; }; + struct QueryItem { + QString name; + QByteArray value; + }; + using ParamsList = QList; + [[nodiscard]] AccountState *accountState() const; void setAccountState(AccountState *accountState); @@ -83,7 +89,7 @@ public slots: QString name; QString url; QString method; - QList params; + ParamsList params; }; QList _fileActions; AccountState *_accountState; @@ -99,5 +105,7 @@ public slots: static constexpr char filePathC[] = "filePath"; static constexpr char rowC[] = "row"; }; - } + +Q_DECLARE_METATYPE(OCC::FileActionsModel::ParamsList) +Q_DECLARE_METATYPE(OCC::FileActionsModel::QueryItem) From d681705dec900118c96cd7245149c42f3ce5bbba Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Sat, 31 Jan 2026 09:13:20 +0100 Subject: [PATCH 15/22] style(integration): rename from declarativeui to integration. - remove DeclarativeUi files. - add return default value in switch. - add const. Signed-off-by: Camila Ayres --- resources.qrc | 2 +- src/gui/CMakeLists.txt | 8 +-- src/gui/declarativeui/declarativeui.cpp | 68 ------------------- src/gui/declarativeui/declarativeui.h | 51 -------------- .../FileActionsWindow.qml | 2 - .../declarativeuimodel.cpp | 0 .../declarativeuimodel.h | 0 .../fileactionsmodel.cpp | 54 ++++++++++----- .../fileactionsmodel.h | 28 ++++---- src/gui/owncloudgui.cpp | 2 +- src/gui/socketapi/socketapi.cpp | 6 -- src/gui/socketapi/socketapi.h | 2 - src/gui/systray.cpp | 3 +- src/libsync/capabilities.cpp | 33 ++++----- src/libsync/capabilities.h | 4 +- 15 files changed, 73 insertions(+), 190 deletions(-) delete mode 100644 src/gui/declarativeui/declarativeui.cpp delete mode 100644 src/gui/declarativeui/declarativeui.h rename src/gui/{declarativeui => integration}/FileActionsWindow.qml (98%) rename src/gui/{declarativeui => integration}/declarativeuimodel.cpp (100%) rename src/gui/{declarativeui => integration}/declarativeuimodel.h (100%) rename src/gui/{declarativeui => integration}/fileactionsmodel.cpp (84%) rename src/gui/{declarativeui => integration}/fileactionsmodel.h (89%) diff --git a/resources.qrc b/resources.qrc index 4d6ff74aef0c3..f26f90192acfe 100644 --- a/resources.qrc +++ b/resources.qrc @@ -61,6 +61,6 @@ src/gui/ConflictItemFileInfo.qml src/gui/macOS/ui/FileProviderSettings.qml src/gui/macOS/ui/FileProviderFileDelegate.qml - src/gui/declarativeui/FileActionsWindow.qml + src/gui/integration/FileActionsWindow.qml diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 7306b23675f99..40f11aeb961e0 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -258,12 +258,8 @@ set(client_SRCS wizard/linklabel.cpp wizard/wizardproxysettingsdialog.h wizard/wizardproxysettingsdialog.cpp - declarativeui/declarativeuimodel.h - declarativeui/declarativeuimodel.cpp - declarativeui/declarativeui.h - declarativeui/declarativeui.cpp - declarativeui/fileactionsmodel.h - declarativeui/fileactionsmodel.cpp + integration/fileactionsmodel.h + integration/fileactionsmodel.cpp ) if (NOT DISABLE_ACCOUNT_MIGRATION) diff --git a/src/gui/declarativeui/declarativeui.cpp b/src/gui/declarativeui/declarativeui.cpp deleted file mode 100644 index ab46603ab9230..0000000000000 --- a/src/gui/declarativeui/declarativeui.cpp +++ /dev/null @@ -1,68 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#include "declarativeui.h" -#include "networkjobs.h" -#include "accountfwd.h" -#include "account.h" - -namespace OCC { - -Q_LOGGING_CATEGORY(lcDeclarativeUi, "nextcloud.gui.declarativeui", QtInfoMsg) - -DeclarativeUi::DeclarativeUi(QObject *parent) - : QObject(parent) -{ -} - -void DeclarativeUi::setAccountState(AccountState *accountState) -{ - if (accountState == nullptr) { - return; - } - - if (accountState == _accountState) { - return; - } - - _accountState = accountState; - _declarativeUiModel = std::make_unique(_accountState->account(), this); - connect(_declarativeUiModel.get(), &DeclarativeUiModel::pageFetched, - this, &DeclarativeUi::declarativeUiFetched); - connect(this, &DeclarativeUi::declarativeUiFetched, - this, &DeclarativeUi::declarativeUiModelChanged); - - Q_EMIT accountStateChanged(); -} - -void DeclarativeUi::setLocalPath(const QString &localPath) -{ - if (localPath.isEmpty()) { - return; - } - - if (localPath == _localPath) { - return; - } - - _localPath = localPath; - Q_EMIT localPathChanged(); -} - -AccountState *DeclarativeUi::accountState() const -{ - return _accountState; -} - -QString DeclarativeUi::localPath() const -{ - return _localPath; -} - -DeclarativeUiModel *DeclarativeUi::declarativeUiModel() const { - return _declarativeUiModel.get(); -} - -} diff --git a/src/gui/declarativeui/declarativeui.h b/src/gui/declarativeui/declarativeui.h deleted file mode 100644 index e4dae39f1560f..0000000000000 --- a/src/gui/declarativeui/declarativeui.h +++ /dev/null @@ -1,51 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#pragma once - -#include -#include - -#include "accountstate.h" -#include "declarativeuimodel.h" -#include "fileactionsmodel.h" - -namespace OCC { - -Q_DECLARE_LOGGING_CATEGORY(lcDeclarativeUi) -class JsonApiJob; - -class DeclarativeUi : public QObject -{ - Q_OBJECT - Q_PROPERTY(AccountState* accountState READ accountState WRITE setAccountState NOTIFY accountStateChanged) - Q_PROPERTY(QString localPath READ localPath WRITE setLocalPath NOTIFY localPathChanged) - Q_PROPERTY(DeclarativeUiModel* declarativeUiModel READ declarativeUiModel NOTIFY declarativeUiModelChanged) - -public: - DeclarativeUi(QObject *parent = nullptr); - - void setAccountState(AccountState *accountState); - void setLocalPath(const QString &localPath); - - [[nodiscard]] AccountState *accountState() const; - [[nodiscard]] QString localPath() const; - [[nodiscard]] DeclarativeUiModel *declarativeUiModel() const; - -signals: - void declarativeUiFetched(); - void endpointsParsed(); - void localPathChanged(); - void accountStateChanged(); - void declarativeUiModelChanged(); - -private: - AccountState *_accountState; - QString _localPath; - - std::unique_ptr _declarativeUiModel; -}; - -} diff --git a/src/gui/declarativeui/FileActionsWindow.qml b/src/gui/integration/FileActionsWindow.qml similarity index 98% rename from src/gui/declarativeui/FileActionsWindow.qml rename to src/gui/integration/FileActionsWindow.qml index 2ef9f9987b15a..f63159808c5ff 100644 --- a/src/gui/declarativeui/FileActionsWindow.qml +++ b/src/gui/integration/FileActionsWindow.qml @@ -160,8 +160,6 @@ ApplicationWindow { Image { source: "image://svgimage-custom-color/backup.svg/" + palette.windowText - // Layout.preferredWidth: Style.accountAvatarStateIndicatorSize - // Layout.preferredHeight: Style.accountAvatarStateIndicatorSize Layout.minimumWidth: Style.accountAvatarStateIndicatorSize Layout.minimumHeight: Style.accountAvatarStateIndicatorSize fillMode: Image.PreserveAspectFit diff --git a/src/gui/declarativeui/declarativeuimodel.cpp b/src/gui/integration/declarativeuimodel.cpp similarity index 100% rename from src/gui/declarativeui/declarativeuimodel.cpp rename to src/gui/integration/declarativeuimodel.cpp diff --git a/src/gui/declarativeui/declarativeuimodel.h b/src/gui/integration/declarativeuimodel.h similarity index 100% rename from src/gui/declarativeui/declarativeuimodel.h rename to src/gui/integration/declarativeuimodel.h diff --git a/src/gui/declarativeui/fileactionsmodel.cpp b/src/gui/integration/fileactionsmodel.cpp similarity index 84% rename from src/gui/declarativeui/fileactionsmodel.cpp rename to src/gui/integration/fileactionsmodel.cpp index 983f71a1bb7c1..4a70bff2d3513 100644 --- a/src/gui/declarativeui/fileactionsmodel.cpp +++ b/src/gui/integration/fileactionsmodel.cpp @@ -31,7 +31,13 @@ QVariant FileActionsModel::data(const QModelIndex &index, int role) const case FileActionMethodRole: return _fileActions.at(row).method; // GET case FileActionParamsRole: - return QVariant::fromValue>(_fileActions.at(row).params); + return QVariant::fromValue(_fileActions.at(row).params); + case FileActionResponseLabelRole: + return _response.label; + case FileActionResponseUrlRole: + return _response.url; + default: + return QVariant(); } return {}; @@ -54,6 +60,8 @@ QHash FileActionsModel::roleNames() const roles[FileActionUrlRole] = "url"; roles[FileActionMethodRole] = "method"; roles[FileActionParamsRole] = "params"; + roles[FileActionResponseLabelRole] = "responseLabel"; + roles[FileActionResponseUrlRole] = "responseUrl"; return roles; } @@ -83,7 +91,6 @@ QString FileActionsModel::localPath() const return _localPath; } - void FileActionsModel::setLocalPath(const QString &localPath) { if (localPath.isEmpty()) { @@ -168,33 +175,41 @@ void FileActionsModel::setResponse(const Response &response) 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 }); + 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 }); + 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 }); + 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().contextMenuByMimeType(_mimeType); - //const QList contextMenuList; + 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 }); + 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) { - ParamsList queryParams; + 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(); @@ -208,7 +223,7 @@ void FileActionsModel::parseEndpoints() } if (!value.isEmpty()) { - queryParams.append( QueryItem{ name, value } ); + queryList.append( QueryItem{ name, value } ); } } @@ -216,10 +231,12 @@ void FileActionsModel::parseEndpoints() contextMenu.value("name").toString(), contextMenu.value("url").toString(), contextMenu.value("method").toString(), - queryParams }); + queryList }); } - qCDebug(lcFileActions) << "File" << _localPath << "has" << _fileActions.size() << "actions available."; + qCDebug(lcFileActions) << "File" << _localPath << "has" + << _fileActions.size() + << "actions available."; Q_EMIT fileActionModelChanged(); } @@ -242,7 +259,8 @@ QString FileActionsModel::parseIcon(const QString &icon) const void FileActionsModel::createRequest(const int row) { if (!_accountState) { - qCWarning(lcFileActions) << "No account state for" << _localPath; + qCWarning(lcFileActions) << "No account state for" + << _localPath; return; } @@ -252,7 +270,7 @@ void FileActionsModel::createRequest(const int row) this); connect(job, &JsonApiJob::jsonReceived, this, &FileActionsModel::processRequest); - for (const auto ¶m : _fileActions.at(row).params) { + for (const auto ¶m : std::as_const(_fileActions.at(row).params)) { QUrlQuery query; query.addQueryItem(param.name, param.value); job->addQueryParams(query); diff --git a/src/gui/declarativeui/fileactionsmodel.h b/src/gui/integration/fileactionsmodel.h similarity index 89% rename from src/gui/declarativeui/fileactionsmodel.h rename to src/gui/integration/fileactionsmodel.h index 0545eafdfae5f..086c68310600d 100644 --- a/src/gui/declarativeui/fileactionsmodel.h +++ b/src/gui/integration/fileactionsmodel.h @@ -34,7 +34,9 @@ class FileActionsModel : public QAbstractListModel { FileActionNameRole, FileActionUrlRole, FileActionMethodRole, - FileActionParamsRole + FileActionParamsRole, + FileActionResponseLabelRole, + FileActionResponseUrlRole }; Q_ENUM(DataRole) @@ -47,7 +49,16 @@ class FileActionsModel : public QAbstractListModel { QString name; QByteArray value; }; - using ParamsList = QList; + 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); @@ -84,14 +95,7 @@ public slots: private: Response _response; - struct FileAction { - QString icon; - QString name; - QString url; - QString method; - ParamsList params; - }; - QList _fileActions; + ActionList _fileActions; AccountState *_accountState; QString _localPath; QByteArray _fileId; @@ -107,5 +111,5 @@ public slots: }; } -Q_DECLARE_METATYPE(OCC::FileActionsModel::ParamsList) -Q_DECLARE_METATYPE(OCC::FileActionsModel::QueryItem) +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 7eaa44d6f10d0..1fe337b4433d4 100644 --- a/src/gui/owncloudgui.cpp +++ b/src/gui/owncloudgui.cpp @@ -33,7 +33,7 @@ #include "tray/sortedactivitylistmodel.h" #include "tray/syncstatussummary.h" #include "tray/unifiedsearchresultslistmodel.h" -#include "declarativeui/fileactionsmodel.h" +#include "integration/fileactionsmodel.h" #include "filesystem.h" #ifdef WITH_LIBCLOUDPROVIDERS diff --git a/src/gui/socketapi/socketapi.cpp b/src/gui/socketapi/socketapi.cpp index 49725041c04c5..7eb49139be181 100644 --- a/src/gui/socketapi/socketapi.cpp +++ b/src/gui/socketapi/socketapi.cpp @@ -621,12 +621,6 @@ void SocketApi::processFileActionsRequest(const QString &localFile) emit fileActionsCommandReceived(fileData.localPath); } -void SocketApi::processDeclarativeUiRequest(const QString &localFile) -{ - const auto fileData = FileData::get(localFile); - emit declarativeUiCommandReceived(fileData.localPath); -} - void SocketApi::broadcastStatusPushMessage(const QString &systemPath, SyncFileStatus fileStatus) { QString msg = buildMessage(QLatin1String("STATUS"), systemPath, fileStatus.toSocketAPIString()); diff --git a/src/gui/socketapi/socketapi.h b/src/gui/socketapi/socketapi.h index b6cfa102ef7b7..98c86b659abdc 100644 --- a/src/gui/socketapi/socketapi.h +++ b/src/gui/socketapi/socketapi.h @@ -65,7 +65,6 @@ public slots: void shareCommandReceived(const QString &localPath); void fileActivityCommandReceived(const QString &localPath); void fileActionsCommandReceived(const QString &localPath); - void declarativeUiCommandReceived(const QString &localPath); private slots: void slotNewConnection(); @@ -110,7 +109,6 @@ private slots: void processFileActivityRequest(const QString &localFile); void processEncryptRequest(const QString &localFile); void processFileActionsRequest(const QString &localFile); - void processDeclarativeUiRequest(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); diff --git a/src/gui/systray.cpp b/src/gui/systray.cpp index 84e1707ec88e5..39ba5a42b66e4 100644 --- a/src/gui/systray.cpp +++ b/src/gui/systray.cpp @@ -16,7 +16,6 @@ #include "configfile.h" #include "accessmanager.h" #include "callstatechecker.h" -#include "declarativeui/declarativeui.h" #include #include @@ -455,7 +454,7 @@ void Systray::createFileActionsDialog(const QString &localPath) return; } - QQmlComponent fileActionsQml(trayEngine(), QStringLiteral("qrc:/qml/src/gui/declarativeui/FileActionsWindow.qml")); + QQmlComponent fileActionsQml(trayEngine(), QStringLiteral("qrc:/qml/src/gui/integration/FileActionsWindow.qml")); if (fileActionsQml.isError()) { qCWarning(lcSystray) << fileActionsQml.errorString(); qCWarning(lcSystray) << fileActionsQml.errors(); diff --git a/src/libsync/capabilities.cpp b/src/libsync/capabilities.cpp index 32a188e911b4c..0753777cfd0f7 100644 --- a/src/libsync/capabilities.cpp +++ b/src/libsync/capabilities.cpp @@ -437,22 +437,22 @@ QStringList Capabilities::forbiddenFilenameExtensions() const return _capabilities["files"].toMap()["forbidden_filename_extensions"].toStringList(); } -bool Capabilities::serverHasDeclarativeUi() const +bool Capabilities::serverHasClientIntegration() const { return _capabilities[QStringLiteral("client_integration")].toMap().isEmpty(); } -QList Capabilities::contextMenuByMimeType(const QMimeType fileMimeType) const +QList Capabilities::fileActionsByMimeType(const QMimeType fileMimeType) const { - const auto declarativeUiMap = _capabilities.value("client_integration").toMap(); + const auto fileActionsMap = _capabilities.value("client_integration").toMap(); QVariantList contextMenuMapList; - for (const auto &declarativeUiApp : std::as_const(declarativeUiMap)) { - const auto declarativeUiContextMenuMap = declarativeUiApp.toMap(); - if (!declarativeUiContextMenuMap.contains("context-menu")) { + for (const auto &fileContextMenu : fileActionsMap) { + const auto fileContextMenuMap = fileContextMenu.toMap(); + if (!fileContextMenuMap.contains("context-menu")) { continue; } - contextMenuMapList.append(declarativeUiContextMenuMap.value("context-menu").toList()); + contextMenuMapList.append(fileContextMenuMap.value("context-menu").toList()); } if (contextMenuMapList.empty()) { @@ -462,12 +462,8 @@ QList Capabilities::contextMenuByMimeType(const QMimeType fileMimeT const auto fileMimeTypeName = fileMimeType.name(); qCDebug(lcServerCapabilities) << "Filtering file actions by mimeType:" << fileMimeTypeName; - const auto fileMimeTypeAliases = fileMimeType.aliases(); - qCDebug(lcServerCapabilities) << "File actions mimeType aliases:" << fileMimeTypeAliases; - const auto fileMimeTypeParents = fileMimeType.parentMimeTypes(); - qCDebug(lcServerCapabilities) << "File actions parent mimeTypes:" << fileMimeTypeParents; - QList contextMenuByMimeType; + QList fileActionsByMimeType; for (const auto &contextMenu : std::as_const(contextMenuMapList)) { const auto contextMenuMap = contextMenu.toMap(); const auto mimetypeFilters = contextMenuMap.value("mimetype_filters").toString(); @@ -475,20 +471,19 @@ QList Capabilities::contextMenuByMimeType(const QMimeType fileMimeT if (filesMimeTypeFilterList.isEmpty()) { qCDebug(lcServerCapabilities) << "Found file action for all mimetypes:" << contextMenuMap; - contextMenuByMimeType.append(contextMenuMap); + fileActionsByMimeType.append(contextMenuMap); continue; } - for (const auto &mimeType : std::as_const(filesMimeTypeFilterList)) { - auto capabilitiesMimeType = mimeType.trimmed(); + for (const auto &mimeType : filesMimeTypeFilterList) { + const auto capabilitiesMimeType = mimeType.trimmed(); QString mimeTypeAlias; - if(const auto capabilitiesMimeTypeSplit = capabilitiesMimeType.split("/"); + 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); @@ -503,11 +498,11 @@ QList Capabilities::contextMenuByMimeType(const QMimeType fileMimeT } qCDebug(lcServerCapabilities) << "Found file action:" << contextMenuMap; - contextMenuByMimeType.append(contextMenuMap); + fileActionsByMimeType.append(contextMenuMap); } } - return contextMenuByMimeType; + return fileActionsByMimeType; } /*-------------------------------------------------------------------------------------*/ diff --git a/src/libsync/capabilities.h b/src/libsync/capabilities.h index 85a14fbf4c579..232afb68bb37f 100644 --- a/src/libsync/capabilities.h +++ b/src/libsync/capabilities.h @@ -175,8 +175,8 @@ class OWNCLOUDSYNC_EXPORT Capabilities [[nodiscard]] bool serverHasValidSubscription() const; [[nodiscard]] QString desktopEnterpriseChannel() const; - [[nodiscard]] bool serverHasDeclarativeUi() const; - [[nodiscard]] QList contextMenuByMimeType(const QMimeType fileMimeType) const; + [[nodiscard]] bool serverHasClientIntegration() const; + [[nodiscard]] QList fileActionsByMimeType(const QMimeType fileMimeType) const; // Direct Editing void addDirectEditor(DirectEditor* directEditor); From 55aba16c6fa36dc62ae1a8661bd285390100cbcf Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Sun, 1 Feb 2026 15:01:36 +0100 Subject: [PATCH 16/22] fix(integratio): return true if there isn't an empty client_integration map. Signed-off-by: Camila Ayres --- src/libsync/capabilities.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libsync/capabilities.cpp b/src/libsync/capabilities.cpp index 0753777cfd0f7..34efb2eb80ebf 100644 --- a/src/libsync/capabilities.cpp +++ b/src/libsync/capabilities.cpp @@ -439,7 +439,7 @@ QStringList Capabilities::forbiddenFilenameExtensions() const bool Capabilities::serverHasClientIntegration() const { - return _capabilities[QStringLiteral("client_integration")].toMap().isEmpty(); + return !_capabilities[QStringLiteral("client_integration")].toMap().isEmpty(); } QList Capabilities::fileActionsByMimeType(const QMimeType fileMimeType) const From b5a78c2d313171357f57bb01e5d19419ef3161f1 Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Sun, 1 Feb 2026 15:03:40 +0100 Subject: [PATCH 17/22] feat(integration): do not list file actions if the server doesn't support it. Signed-off-by: Camila Ayres --- src/gui/socketapi/socketapi.cpp | 11 ++++++++++- src/gui/tray/ActivityItemContent.qml | 1 + src/gui/tray/activitylistmodel.cpp | 3 +++ src/gui/tray/activitylistmodel.h | 1 + src/libsync/account.cpp | 11 +++++++++++ src/libsync/account.h | 6 ++++++ 6 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/gui/socketapi/socketapi.cpp b/src/gui/socketapi/socketapi.cpp index 7eb49139be181..c2d2120944b07 100644 --- a/src/gui/socketapi/socketapi.cpp +++ b/src/gui/socketapi/socketapi.cpp @@ -1182,7 +1182,16 @@ void SocketApi::sendFileActionsContextMenuOptions(const FileData &fileData, Sock { const auto record = fileData.journalRecord(); const auto isOnTheServer = record.isValid(); - const auto flagString = isOnTheServer ? QLatin1String("::") : QLatin1String(":d:"); + 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")); } diff --git a/src/gui/tray/ActivityItemContent.qml b/src/gui/tray/ActivityItemContent.qml index 747483b9a7897..f1b09c765e0ef 100644 --- a/src/gui/tray/ActivityItemContent.qml +++ b/src/gui/tray/ActivityItemContent.qml @@ -200,6 +200,7 @@ RowLayout { } MenuItem { + visible: model.serverHasIntegration height: visible ? implicitHeight : 0 text: qsTr("File actions") font.pixelSize: Style.topLinePixelSize diff --git a/src/gui/tray/activitylistmodel.cpp b/src/gui/tray/activitylistmodel.cpp index 45f319b5e1931..c861d8d36e145 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; } @@ -377,6 +378,8 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const return index.row(); case ActivityRole: return QVariant::fromValue(a); + case ActivityIntegrationRole: + return ast->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! * From 1a1e4930d9d79b92db1ab7598e0de02cd8828d21 Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Sun, 1 Feb 2026 15:19:59 +0100 Subject: [PATCH 18/22] style(tray): modernize ActivityListModel::data. Signed-off-by: Camila Ayres --- src/gui/tray/activitylistmodel.cpp | 150 +++++++++++++++-------------- 1 file changed, 78 insertions(+), 72 deletions(-) diff --git a/src/gui/tray/activitylistmodel.cpp b/src/gui/tray/activitylistmodel.cpp index c861d8d36e145..20ece0431aeb6 100644 --- a/src/gui/tray/activitylistmodel.cpp +++ b/src/gui/tray/activitylistmodel.cpp @@ -141,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(); @@ -172,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('\\')) { @@ -222,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); } }; @@ -274,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: @@ -313,73 +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 ast->account()->serverHasIntegration(); + return accountState->account()->serverHasIntegration(); } return {}; From 705eb8e033d0abbd949808f42cc99cb758a1de5f Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Mon, 2 Feb 2026 19:44:56 +0100 Subject: [PATCH 19/22] fix(integration): use ActivityData function to retrieve server icon. Signed-off-by: Camila Ayres --- src/gui/integration/fileactionsmodel.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/gui/integration/fileactionsmodel.cpp b/src/gui/integration/fileactionsmodel.cpp index 4a70bff2d3513..ed2021e741e6d 100644 --- a/src/gui/integration/fileactionsmodel.cpp +++ b/src/gui/integration/fileactionsmodel.cpp @@ -7,6 +7,8 @@ #include "networkjobs.h" #include "account.h" #include "folderman.h" +#include "common/utility.h" +#include "tray/activitydata.h" namespace OCC { @@ -132,8 +134,7 @@ void FileActionsModel::setupFileProperties() const auto mimeType = mimeDb.mimeTypeForFile(_localPath, mimeMatchMode); _mimeType = mimeType; - // TODO: display an icon for each mimeType - _fileIcon = ""; + _fileIcon = _accountUrl + Activity::relativeServerFileTypeIconPath(_mimeType); } QMimeType FileActionsModel::mimeType() const From 5580efc5f7b73706ab2010d2533822a49eb1b1b2 Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Mon, 2 Feb 2026 19:53:13 +0100 Subject: [PATCH 20/22] refactor(tray): modernize Activity::relativeServerFileTypeIconPath. Signed-off-by: Camila Ayres --- src/gui/tray/activitydata.cpp | 111 +++++++++++++++++++++------------- 1 file changed, 68 insertions(+), 43 deletions(-) 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; } } From 83aa5242e2dbe544627c81a15f3e4206d9990189 Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Mon, 2 Feb 2026 19:54:06 +0100 Subject: [PATCH 21/22] feat(integration): align icons and make sure they all have the same size. Signed-off-by: Camila Ayres --- src/gui/integration/FileActionsWindow.qml | 24 +++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/gui/integration/FileActionsWindow.qml b/src/gui/integration/FileActionsWindow.qml index f63159808c5ff..94c4a239313c3 100644 --- a/src/gui/integration/FileActionsWindow.qml +++ b/src/gui/integration/FileActionsWindow.qml @@ -71,9 +71,9 @@ ApplicationWindow { spacing: Style.standardSpacing Image { - source: "image://svgimage-custom-color/file-open.svg/" + palette.windowText - Layout.maximumWidth: Style.minimumActivityItemHeight - Layout.maximumHeight: Style.minimumActivityItemHeight + source: fileActionModel.fileIcon + palette.windowText + Layout.preferredWidth: Style.minimumActivityItemHeight + Layout.preferredHeight: Style.minimumActivityItemHeight Layout.alignment: Qt.AlignVCenter Layout.margins: Style.extraSmallSpacing } @@ -160,11 +160,10 @@ ApplicationWindow { Image { source: "image://svgimage-custom-color/backup.svg/" + palette.windowText - Layout.minimumWidth: Style.accountAvatarStateIndicatorSize - Layout.minimumHeight: Style.accountAvatarStateIndicatorSize - fillMode: Image.PreserveAspectFit - Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft - Layout.leftMargin: Style.standardSpacing + Layout.preferredWidth: Style.minimumActivityItemHeight + Layout.preferredHeight: Style.minimumActivityItemHeight + Layout.alignment: Qt.AlignLeft + Layout.bottomMargin: Style.standardSpacing } Text { @@ -177,7 +176,6 @@ ApplicationWindow { wrapMode: Text.WordWrap Layout.fillWidth: true bottomPadding: Style.standardSpacing - Layout.alignment: Qt.AlignVCenter } } @@ -218,15 +216,17 @@ ApplicationWindow { anchors.topMargin: Style.standardSpacing anchors.rightMargin: Style.standardSpacing anchors.bottomMargin: Style.standardSpacing - anchors.leftMargin: Style.smallSpacing + anchors.leftMargin: Style.extraSmallSpacing spacing: Style.standardSpacing Layout.fillWidth: true Image { source: fileAction.icon + palette.windowText - width: Style.activityListButtonIconSize - height: Style.activityListButtonIconSize + width: Style.minimumActivityItemHeight + height: Style.minimumActivityItemHeight fillMode: Image.PreserveAspectFit + Layout.preferredWidth: Style.minimumActivityItemHeight + Layout.preferredHeight: Style.minimumActivityItemHeight anchors.verticalCenter: parent.verticalCenter } From 441eda58adb51d1d6133230b4ccc4f18f0a85f72 Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Tue, 3 Feb 2026 12:29:12 +0100 Subject: [PATCH 22/22] fix(integration): address review's comments. - Removed unused fileas and variables. - Add reference on non-trivial type. - Add [[nodiscard]] to functions declaration. Signed-off-by: Camila Ayres --- src/gui/integration/FileActionsWindow.qml | 1 - src/gui/integration/declarativeuimodel.cpp | 108 --------------------- src/gui/integration/declarativeuimodel.h | 57 ----------- src/gui/integration/fileactionsmodel.cpp | 1 - src/gui/integration/fileactionsmodel.h | 4 +- src/libsync/capabilities.cpp | 2 +- src/libsync/capabilities.h | 2 +- 7 files changed, 4 insertions(+), 171 deletions(-) delete mode 100644 src/gui/integration/declarativeuimodel.cpp delete mode 100644 src/gui/integration/declarativeuimodel.h diff --git a/src/gui/integration/FileActionsWindow.qml b/src/gui/integration/FileActionsWindow.qml index 94c4a239313c3..e526fbff6dc25 100644 --- a/src/gui/integration/FileActionsWindow.qml +++ b/src/gui/integration/FileActionsWindow.qml @@ -25,7 +25,6 @@ ApplicationWindow { property var accountState: ({}) property string localPath: "" property string shortLocalPath: "" - property var response: ({}) readonly property int windowRadius: Systray.useNormalWindow ? 0.0 : Style.trayWindowRadius diff --git a/src/gui/integration/declarativeuimodel.cpp b/src/gui/integration/declarativeuimodel.cpp deleted file mode 100644 index e3216a6db1de2..0000000000000 --- a/src/gui/integration/declarativeuimodel.cpp +++ /dev/null @@ -1,108 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#include "declarativeuimodel.h" -#include "networkjobs.h" - -namespace OCC { - -DeclarativeUiModel::DeclarativeUiModel(const AccountPtr &account, QObject *parent) - : QAbstractListModel(parent) - , _account(account) -{ - fetchPage(); -} - -void DeclarativeUiModel::fetchPage() -{ - if (!_account) { - return; - } - - auto job = new JsonApiJob(_account, - QLatin1String("ocs/v2.php/apps/declarativetest/version1"), - this); - connect(job, &JsonApiJob::jsonReceived, - this, &DeclarativeUiModel::slotPageFetched); - job->start(); -} - -void DeclarativeUiModel::slotPageFetched(const QJsonDocument &json) -{ - const auto root = json.object().value(QStringLiteral("root")).toObject(); - if (root.empty()) { - return; - } - const auto orientation = root.value(QStringLiteral("orientation")).toString(); - const auto rows = root.value(QStringLiteral("rows")).toArray(); - if (rows.empty()) { - 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(); - Element element; - element.name = child.value(QStringLiteral("element")).toString(); - element.type = child.value(QStringLiteral("type")).toString(); - element.label = child.value(QStringLiteral("label")).toString(); - element.url = _account->url().toString() + child.value(QStringLiteral("url")).toString(); - element.text = child.value(QStringLiteral("text")).toString(); - _page.append(element); - } - } - - Q_EMIT pageFetched(); -} - -QVariant DeclarativeUiModel::data(const QModelIndex &index, int role) const -{ - Q_ASSERT(checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid)); - switch (role) { - case ElementNameRole: - return _page.at(index.row()).name; // Button, Text, Image - case ElementTypeRole: - return _page.at(index.row()).type; // Primary, Secondarys - case ElementLabelRole: - return _page.at(index.row()).label; // Cancel, Submit - case ElementUrlRole: - return _page.at(index.row()).url; // /core/img/logo/log.png - case ElementTextRole: - return _page.at(index.row()).text; // String - } - - return {}; -} - -int DeclarativeUiModel::rowCount(const QModelIndex &parent) const -{ - if (parent.isValid()) { - return 0; - } - - return _page.size(); -} - -QHash DeclarativeUiModel::roleNames() const -{ - auto roles = QAbstractListModel::roleNames(); - roles[ElementNameRole] = "name"; - roles[ElementTypeRole] = "type"; - roles[ElementLabelRole] = "label"; - roles[ElementUrlRole] = "url"; - roles[ElementTextRole] = "text"; - - return roles; -} - -QString DeclarativeUiModel::pageOrientation() const -{ - return _pageOrientation; -} - -} diff --git a/src/gui/integration/declarativeuimodel.h b/src/gui/integration/declarativeuimodel.h deleted file mode 100644 index f0f85047e3d0a..0000000000000 --- a/src/gui/integration/declarativeuimodel.h +++ /dev/null @@ -1,57 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#pragma once - -#include -#include "libsync/account.h" - -namespace OCC { - -class JsonApiJob; - -class DeclarativeUiModel : public QAbstractListModel -{ - Q_OBJECT - -public: - explicit DeclarativeUiModel(const AccountPtr &accountState, QObject *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 { - ElementNameRole = Qt::UserRole + 1, - ElementTypeRole, - ElementLabelRole, - ElementTextRole, - ElementUrlRole - }; - Q_ENUM(DataRole) - - [[nodiscard]] QString pageOrientation() const; - void fetchPage(); - -signals: - void pageFetched(); - -public slots: - void slotPageFetched(const QJsonDocument &json); - -private: - struct Element { - QString name; - QString type; - QString label; - QString url; - QString text; - }; - QList _page; - QString _pageOrientation; - - AccountPtr _account; -}; - -} diff --git a/src/gui/integration/fileactionsmodel.cpp b/src/gui/integration/fileactionsmodel.cpp index ed2021e741e6d..1d8d913ca0c2e 100644 --- a/src/gui/integration/fileactionsmodel.cpp +++ b/src/gui/integration/fileactionsmodel.cpp @@ -304,7 +304,6 @@ void FileActionsModel::processRequest(const QJsonDocument &json, int statusCode) return; } - const auto orientation = root.value(QStringLiteral("orientation")).toString(); const auto rows = root.value(QStringLiteral("rows")).toArray(); if (rows.empty()) { setResponse({ successMessage, remoteFolderPath }); diff --git a/src/gui/integration/fileactionsmodel.h b/src/gui/integration/fileactionsmodel.h index 086c68310600d..222478a62479d 100644 --- a/src/gui/integration/fileactionsmodel.h +++ b/src/gui/integration/fileactionsmodel.h @@ -80,8 +80,8 @@ class FileActionsModel : public QAbstractListModel { void setResponse(const Response &response); void parseEndpoints(); - QString parseUrl(const QString &url) const; - QString parseIcon(const QString &icon) const; + [[nodiscard]] QString parseUrl(const QString &url) const; + [[nodiscard]] QString parseIcon(const QString &icon) const; signals: void accountStateChanged(); diff --git a/src/libsync/capabilities.cpp b/src/libsync/capabilities.cpp index 34efb2eb80ebf..73b796fa2b212 100644 --- a/src/libsync/capabilities.cpp +++ b/src/libsync/capabilities.cpp @@ -442,7 +442,7 @@ bool Capabilities::serverHasClientIntegration() const return !_capabilities[QStringLiteral("client_integration")].toMap().isEmpty(); } -QList Capabilities::fileActionsByMimeType(const QMimeType fileMimeType) const +QList Capabilities::fileActionsByMimeType(const QMimeType &fileMimeType) const { const auto fileActionsMap = _capabilities.value("client_integration").toMap(); QVariantList contextMenuMapList; diff --git a/src/libsync/capabilities.h b/src/libsync/capabilities.h index 232afb68bb37f..a06d4a1036761 100644 --- a/src/libsync/capabilities.h +++ b/src/libsync/capabilities.h @@ -176,7 +176,7 @@ class OWNCLOUDSYNC_EXPORT Capabilities [[nodiscard]] QString desktopEnterpriseChannel() const; [[nodiscard]] bool serverHasClientIntegration() const; - [[nodiscard]] QList fileActionsByMimeType(const QMimeType fileMimeType) const; + [[nodiscard]] QList fileActionsByMimeType(const QMimeType &fileMimeType) const; // Direct Editing void addDirectEditor(DirectEditor* directEditor);