Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
46df1cf
feat(socketapi): add file actions option to list of options for files.
camilasan Jun 23, 2025
a33ea24
feat(declarativeui): add basic class to retrieve json from server.
camilasan Aug 17, 2025
f37e24d
feat(declarativeui): fetch delcarativeui endpoints in capabilities.
camilasan Sep 2, 2025
5a64346
feat(declarativeui): display application window with declarative ui.
camilasan Sep 2, 2025
394f531
feat(declarativeui): list declarative ui and file actions in context …
camilasan Sep 2, 2025
7f1127f
feat(declarativeui): list declarative ui and file actions in the tray…
camilasan Sep 2, 2025
b99f3ea
fix(declarativeui): remove log messages.
camilasan Sep 2, 2025
5e429c2
feat(declarativeui): add icon and filter to endpoint model and UI.
camilasan Sep 2, 2025
12a628f
feat: style file actions window.
camilasan Sep 3, 2025
af8c1b2
feat: display response from request from file actions.
camilasan Sep 3, 2025
609be60
feat(declarativeui): improve the looks of the file actions Window.
camilasan Sep 3, 2025
a1d0ff2
fix: context menu.
camilasan Sep 4, 2025
eb5cb87
fix: update datat parsing to new API format.
camilasan Sep 16, 2025
a103167
feat: add helper function to match string to SimpleApiJob::Verb.
camilasan Sep 16, 2025
ea23761
fix: handle file action response.
camilasan Sep 17, 2025
de2c088
refactor: create function to set file id and mime type.
camilasan Sep 17, 2025
7e23ef2
feat: add logging category to FileActionsModel.
camilasan Sep 17, 2025
633add2
refactor: improve error handling.
camilasan Sep 17, 2025
b85be63
feat: return default icon when server doesn't have one.
camilasan Sep 18, 2025
a271e3e
fix: parse response with declarative UI elements like url.
camilasan Sep 30, 2025
538296c
refactor: remove unused DeclarativeUi class.
camilasan Oct 1, 2025
ea43cd1
fix: files license.
camilasan Oct 1, 2025
fa0b1dd
refactor: remove Declarative UI menu item.
camilasan Oct 1, 2025
e1c82df
fix: use reference type in for loop.
camilasan Oct 1, 2025
c1df938
fix: spaces and sizes in the file actions window.
camilasan Oct 1, 2025
f1caf41
fix: remove missing file from resources.
camilasan Jan 20, 2026
3c3dd1f
fix: update capabilities with new json key + adjust mimetype check.
camilasan Jan 20, 2026
1e4b1ef
fix: use EnforcedPlainTextLabel instead of Label.
camilasan Jan 21, 2026
3522e47
fix: an empty mimetype_filters means the action is available to all m…
camilasan Jan 21, 2026
2554ede
fix: handle the new params format.
camilasan Jan 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
1 change: 1 addition & 0 deletions resources.qrc
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,6 @@
<file>src/gui/ConflictItemFileInfo.qml</file>
<file>src/gui/macOS/ui/FileProviderSettings.qml</file>
<file>src/gui/macOS/ui/FileProviderFileDelegate.qml</file>
<file>src/gui/declarativeui/FileActionsWindow.qml</file>
</qresource>
</RCC>
6 changes: 6 additions & 0 deletions src/gui/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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/fileactionsmodel.h
declarativeui/fileactionsmodel.cpp
)

if (NOT DISABLE_ACCOUNT_MIGRATION)
Expand Down
3 changes: 3 additions & 0 deletions src/gui/application.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
/*
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2014 ownCloud GmbH
Expand Down Expand Up @@ -431,6 +431,9 @@
connect(FolderMan::instance()->socketApi(), &SocketApi::fileActivityCommandReceived,
_gui.data(), &ownCloudGui::slotShowFileActivityDialog);

connect(FolderMan::instance()->socketApi(), &SocketApi::fileActionsCommandReceived,
_gui.data(), &ownCloudGui::slotShowFileActionsDialog);

// startup procedure.
connect(&_checkConnectionTimer, &QTimer::timeout, this, &Application::slotCheckConnection);
_checkConnectionTimer.setInterval(ConnectionValidator::DefaultCallingIntervalMsec); // check for connection every 32 seconds.
Expand Down
264 changes: 264 additions & 0 deletions src/gui/declarativeui/FileActionsWindow.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
/*
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-2.0-or-later
*/

pragma ComponentBehavior: Bound

import QtQuick
import QtQuick.Window
import QtQuick.Layouts
import QtQuick.Controls
import Qt5Compat.GraphicalEffects
import com.nextcloud.desktopclient
import Style
import "./../tray"

ApplicationWindow {
id: root
height: Style.filesActionsHeight
width: Style.filesActionsWidth
flags: Systray.useNormalWindow ? Qt.Window : Qt.Dialog | Qt.FramelessWindowHint
visible: true
color: "transparent"

property var accountState: ({})
property string localPath: ""
property string shortLocalPath: ""
property var response: ({})

readonly property int windowRadius: Systray.useNormalWindow ? 0.0 : Style.trayWindowRadius

title: qsTr("File actions for %1").arg(root.shortLocalPath)

FileActionsModel {
id: fileActionModel
accountState: root.accountState
localPath: root.localPath
}

background: Rectangle {
id: maskSource
radius: root.windowRadius
border.width: Style.trayWindowBorderWidth
border.color: palette.dark
color: palette.window
}

OpacityMask {
anchors.fill: parent
anchors.margins: Style.trayWindowBorderWidth
source: maskSourceItem
maskSource: maskSource
}

Rectangle {
id: maskSourceItem
anchors.fill: parent
anchors.margins: Style.standardSpacing
radius: root.windowRadius
clip: true
color: Style.colorWithoutTransparency(palette.base)

ColumnLayout {
id: windowContent
anchors.fill: parent
anchors.margins: Style.standardSpacing

RowLayout {
id: windowHeader
Layout.fillWidth: true
spacing: Style.standardSpacing

Image {
source: "image://svgimage-custom-color/file-open.svg/" + palette.windowText
Layout.maximumWidth: Style.minimumActivityItemHeight
Layout.maximumHeight: Style.minimumActivityItemHeight
Layout.alignment: Qt.AlignVCenter
Layout.margins: Style.extraSmallSpacing
}

EnforcedPlainTextLabel {
id: headerLocalPath
text: root.shortLocalPath
elide: Text.ElideRight
font.bold: true
font.pixelSize: Style.pixelSize
color: palette.text
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
}

Button {
id: closeButton
flat: true
padding: Style.extraSmallSpacing
spacing: 0
icon.source: "image://svgimage-custom-color/close.svg/" + palette.windowText
icon.width: Style.extraSmallIconSize
icon.height: Style.extraSmallIconSize
Layout.alignment: Qt.AlignTop | Qt.AlignRight
Layout.rightMargin: Style.extraSmallSpacing
Layout.topMargin: Style.extraSmallSpacing
onClicked: root.close()
background: Rectangle {
color: "transparent"
radius: root.windowRadius
border.width: closeButton.hovered ? Style.trayWindowBorderWidth : 0
border.color: palette.dark
anchors.fill: parent
Layout.margins: Style.extraSmallSpacing
}
}
}

Rectangle {
id: lineTop
Layout.fillWidth: true
Layout.minimumHeight: Style.extraExtraSmallSpacing
color: palette.dark
}

ListView {
id: fileActionsView
model: fileActionModel
clip: true
spacing: Style.trayHorizontalMargin
Layout.fillWidth: true
Layout.fillHeight: true
delegate: fileActionsDelegate
}

Button {
id: responseButton
visible: responseText.text !== ""
flat: true
Layout.fillWidth: true
implicitHeight: responseContent.implicitHeight

padding: Style.standardSpacing
leftPadding: Style.standardSpacing
rightPadding: Style.standardSpacing
spacing: Style.standardSpacing

background: Rectangle {
id: responseBorder
radius: root.windowRadius
border.width: Style.trayWindowBorderWidth
border.color: palette.dark
color: palette.window
Layout.fillWidth: true
}

contentItem: RowLayout {
id: responseContent
anchors.fill: parent
anchors.margins: Style.smallSpacing
spacing: Style.standardSpacing
Layout.fillWidth: true
Layout.minimumHeight: Style.accountAvatarStateIndicatorSize

Image {
source: "image://svgimage-custom-color/backup.svg/" + palette.windowText
// Layout.preferredWidth: Style.accountAvatarStateIndicatorSize
// Layout.preferredHeight: Style.accountAvatarStateIndicatorSize
Layout.minimumWidth: Style.accountAvatarStateIndicatorSize
Layout.minimumHeight: Style.accountAvatarStateIndicatorSize
fillMode: Image.PreserveAspectFit
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
Layout.leftMargin: Style.standardSpacing
}

Text {
id: responseText
text: fileActionModel.responseLabel
textFormat: Text.RichText
color: palette.text
font.pointSize: Style.pixelSize
font.underline: true
wrapMode: Text.WordWrap
Layout.fillWidth: true
bottomPadding: Style.standardSpacing
Layout.alignment: Qt.AlignVCenter
}
}

MouseArea {
id: responseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: Qt.openUrlExternally(fileActionModel.responseUrl)
}
}
}
}

Component {
id: fileActionsDelegate

RowLayout {
id: fileAction
Layout.fillWidth: true
height: implicitHeight
width: parent.width

required property string name
required property int index
required property string icon

Button {
id: fileActionButton
flat: true
Layout.fillWidth: true
implicitHeight: Style.activityListButtonHeight

padding: Style.standardSpacing

contentItem: Row {
id: fileActionsContent
anchors.fill: parent
anchors.topMargin: Style.standardSpacing
anchors.rightMargin: Style.standardSpacing
anchors.bottomMargin: Style.standardSpacing
anchors.leftMargin: Style.smallSpacing
spacing: Style.standardSpacing
Layout.fillWidth: true

Image {
source: fileAction.icon + palette.windowText
width: Style.activityListButtonIconSize
height: Style.activityListButtonIconSize
fillMode: Image.PreserveAspectFit
anchors.verticalCenter: parent.verticalCenter
}

EnforcedPlainTextLabel {
text: fileAction.name
color: palette.text
font.pixelSize: Style.defaultFontPtSize
verticalAlignment: Text.AlignVCenter
anchors.verticalCenter: parent.verticalCenter
}
}

background: Rectangle {
color: "transparent"
radius: root.windowRadius
border.width: fileActionButton.hovered ? Style.trayWindowBorderWidth : 0
border.color: palette.dark
anchors.margins: Style.standardSpacing
height: parent.height
width: parent.width
}

MouseArea {
id: fileActionMouseArea
anchors.fill: parent
anchors.margins: Style.standardSpacing
cursorShape: Qt.PointingHandCursor
onClicked: fileActionModel.createRequest(fileAction.index)
}
}
}
}
}
68 changes: 68 additions & 0 deletions src/gui/declarativeui/declarativeui.cpp
Original file line number Diff line number Diff line change
@@ -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<DeclarativeUiModel>(_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();
}

}
Loading
Loading