]> dgit.raspbian.org Git - nextcloud-desktop.git/commitdiff
Group folder visibility improvements. Show dropdown in tray window.
authoralex-z <blackslayer4@gmail.com>
Fri, 21 Apr 2023 15:48:44 +0000 (17:48 +0200)
committeralex-z <blackslayer4@gmail.com>
Tue, 9 May 2023 10:25:05 +0000 (12:25 +0200)
Signed-off-by: alex-z <blackslayer4@gmail.com>
14 files changed:
resources.qrc
src/gui/tray/ListItemLineAndSubline.qml [new file with mode: 0644]
src/gui/tray/TrayFolderListItem.qml [new file with mode: 0644]
src/gui/tray/TrayFoldersMenuButton.qml [new file with mode: 0644]
src/gui/tray/UnifiedSearchResultItem.qml
src/gui/tray/Window.qml
src/gui/tray/usermodel.cpp
src/gui/tray/usermodel.h
src/libsync/capabilities.cpp
src/libsync/capabilities.h
src/libsync/discoveryphase.cpp
theme.qrc.in
theme/Style/Style.qml
theme/black/folder-group.svg [new file with mode: 0644]

index 976c534b77792be83bf8ab9165b33b8a92e6c3a8..1ec7ad1fa0b6ef725488d61fe818aad778895118 100644 (file)
@@ -52,5 +52,8 @@
         <file>theme/Style/Style.qml</file>
         <file>theme/Style/qmldir</file>
         <file>src/gui/filedetails/NCRadioButton.qml</file>
+        <file>src/gui/tray/ListItemLineAndSubline.qml</file>
+        <file>src/gui/tray/TrayFoldersMenuButton.qml</file>
+        <file>src/gui/tray/TrayFolderListItem.qml</file>
     </qresource>
 </RCC>
diff --git a/src/gui/tray/ListItemLineAndSubline.qml b/src/gui/tray/ListItemLineAndSubline.qml
new file mode 100644 (file)
index 0000000..a226d8b
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2023 by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+import QtQml 2.15
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+
+import Style 1.0
+
+ColumnLayout {
+    id: root
+
+    spacing: Style.standardSpacing
+
+    property string lineText: ""
+    property string sublineText: ""
+
+    property int titleFontSize: Style.unifiedSearchResultTitleFontSize
+    property int sublineFontSize: Style.unifiedSearchResultSublineFontSize
+
+    property color titleColor: Style.ncTextColor
+    property color sublineColor: Style.ncSecondaryTextColor
+
+    EnforcedPlainTextLabel {
+        id: title
+        Layout.fillWidth: true
+        text: root.lineText
+        elide: Text.ElideRight
+        font.pixelSize: root.titleFontSize
+        color: root.titleColor
+    }
+    EnforcedPlainTextLabel {
+        id: subline
+        Layout.fillWidth: true
+        text: root.sublineText
+        visible: text !== ""
+        elide: Text.ElideRight
+        font.pixelSize: root.sublineFontSize
+        color: root.sublineColor
+    }
+}
diff --git a/src/gui/tray/TrayFolderListItem.qml b/src/gui/tray/TrayFolderListItem.qml
new file mode 100644 (file)
index 0000000..f9325ae
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2023 by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+import QtQml 2.15
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+import Style 1.0
+
+MenuItem {
+    id: root
+
+    property string subline: ""
+    property string iconSource: "image://svgimage-custom-color/folder-group.svg/" + Style.ncTextColor
+    property string toolTipText: root.text
+
+    NCToolTip {
+        visible: root.hovered && root.toolTipText !== ""
+        text: root.toolTipText
+    }
+
+    background: Item {
+        height: parent.height
+        width: parent.width
+        Rectangle {
+            anchors.fill: parent
+            anchors.margins: Style.normalBorderWidth
+            color: parent.parent.hovered || parent.parent.visualFocus ? Style.lightHover : "transparent"
+        }
+    }
+
+    contentItem: RowLayout {
+        anchors.fill: parent
+        anchors.leftMargin: Style.trayWindowMenuEntriesMargin
+        anchors.rightMargin: Style.trayWindowMenuEntriesMargin
+        spacing: Style.trayHorizontalMargin
+
+        Image {
+            source: root.iconSource
+            cache: true
+            sourceSize.width: root.height * Style.smallIconScaleFactor
+            sourceSize.height: root.height * Style.smallIconScaleFactor
+            verticalAlignment: Qt.AlignVCenter
+            horizontalAlignment: Qt.AlignHCenter
+
+            Layout.preferredHeight: root.height * Style.smallIconScaleFactor
+            Layout.preferredWidth: root.height * Style.smallIconScaleFactor
+            Layout.alignment: Qt.AlignVCenter
+        }
+
+        ListItemLineAndSubline {
+            lineText: root.text
+            sublineText: root.subline
+
+            spacing: Style.extraSmallSpacing
+
+            Layout.fillWidth: true
+        }
+    }
+}
diff --git a/src/gui/tray/TrayFoldersMenuButton.qml b/src/gui/tray/TrayFoldersMenuButton.qml
new file mode 100644 (file)
index 0000000..20fb3d8
--- /dev/null
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2023 by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+import QtGraphicalEffects 1.0
+import Style 1.0
+
+HeaderButton {
+    id: root
+
+    signal folderEntryTriggered(string fullFolderPath, bool isGroupFolder)
+
+    required property var currentUser
+    property bool userHasGroupFolders: currentUser.groupFolders.length > 0
+
+    function openMenu() {
+        foldersMenuLoader.openMenu()
+    }
+
+    function closeMenu() {
+        foldersMenuLoader.closeMenu()
+    }
+
+    function toggleMenuOpen() {
+        if (foldersMenuLoader.isMenuVisible) {
+            closeMenu()
+        } else {
+            openMenu()
+        }
+    }
+
+    visible: currentUser.hasLocalFolder
+    display: AbstractButton.IconOnly
+    flat: true
+    palette: Style.systemPalette
+
+    Accessible.role: root.userHasGroupFolders ? Accessible.ButtonMenu : Accessible.Button
+    Accessible.name: tooltip.text
+    Accessible.onPressAction: root.clicked()
+
+    NCToolTip {
+        id: tooltip
+        visible: root.hovered && !foldersMenuLoader.isMenuVisible
+        text: root.userHasGroupFolders ? qsTr("Open local or group folders") : qsTr("Open local folder")
+    }
+
+    Image {
+        id: folderStateIndicator
+        visible: root.currentUser.hasLocalFolder
+        source: root.currentUser.isConnected ? Style.stateOnlineImageSource : Style.stateOfflineImageSource
+        cache: false
+
+        anchors.top: root.verticalCenter
+        anchors.left: root.horizontalCenter
+        sourceSize.width: Style.folderStateIndicatorSize
+        sourceSize.height: Style.folderStateIndicatorSize
+
+        Accessible.role: Accessible.Indicator
+        Accessible.name: root.currentUser.isConnected ? qsTr("Connected") : qsTr("Disconnected")
+        z: 1
+
+        Rectangle {
+            id: folderStateIndicatorBackground
+            width: Style.folderStateIndicatorSize + Style.trayFolderStatusIndicatorSizeOffset
+            height: width
+            anchors.centerIn: parent
+            color: Style.currentUserHeaderColor
+            radius: width * Style.trayFolderStatusIndicatorRadiusFactor
+            z: -2
+        }
+
+        Rectangle {
+            id: folderStateIndicatorBackgroundMouseHover
+            width: Style.folderStateIndicatorSize + Style.trayFolderStatusIndicatorSizeOffset
+            height: width
+            anchors.centerIn: parent
+            color: root.hovered ? Style.currentUserHeaderTextColor : "transparent"
+            opacity: Style.trayFolderStatusIndicatorMouseHoverOpacityFactor
+            radius: width * Style.trayFolderStatusIndicatorRadiusFactor
+            z: -1
+        }
+    }
+
+    RowLayout {
+        id: openLocalFolderButtonRowLayout
+
+        anchors.fill: parent
+        spacing: 0
+
+        Image {
+            id: openLocalFolderButtonIcon
+            cache: false
+            source: "qrc:///client/theme/white/folder.svg"
+
+            verticalAlignment: Qt.AlignCenter
+
+            Accessible.role: Accessible.Graphic
+            Accessible.name: qsTr("Group folder button")
+            Layout.leftMargin: Style.trayHorizontalMargin
+        }
+
+        Loader {
+            id: openLocalFolderButtonCaretIconLoader
+
+            active: root.userHasGroupFolders
+            visible: active
+
+            sourceComponent: ColorOverlay {
+                width: source.width
+                height: source.height
+                cached: true
+                color: Style.currentUserHeaderTextColor
+                source: Image {
+                    source: "qrc:///client/theme/white/caret-down.svg"
+                    sourceSize.width: Style.accountDropDownCaretSize
+                    sourceSize.height: Style.accountDropDownCaretSize
+
+                    verticalAlignment: Qt.AlignCenter
+
+                    Layout.alignment: Qt.AlignRight
+                    Layout.margins: Style.accountDropDownCaretMargin
+                }
+            }
+        }
+    }
+
+    Loader {
+        id: foldersMenuLoader
+
+        property var openMenu: function(){}
+        property var closeMenu: function(){}
+        property bool isMenuVisible: false
+
+        anchors.fill: parent
+        active: root.userHasGroupFolders
+        visible: active
+
+        sourceComponent: AutoSizingMenu {
+            id: foldersMenu
+
+            x: Style.trayWindowMenuOffsetX
+            y: (root.y + root.height + Style.trayWindowMenuOffsetY)
+            width: Style.trayWindowWidth * Style.trayWindowMenuWidthFactor
+            height: implicitHeight + y > Style.trayWindowHeight ? Style.trayWindowHeight - y : implicitHeight
+            closePolicy: Menu.CloseOnPressOutsideParent | Menu.CloseOnEscape
+
+            contentItem: ScrollView {
+                id: foldersMenuScrollView
+
+                ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
+
+                data: WheelHandler {
+                    target: foldersMenuScrollView.contentItem
+                }
+
+                ListView {
+                    id: foldersMenuListView
+
+                    implicitHeight: contentHeight
+                    model: root.currentUser.groupFolders
+                    interactive: true
+                    clip: true
+                    currentIndex: foldersMenu.currentIndex
+                    anchors.left: parent.left
+                    anchors.right: parent.right
+
+                    delegate: TrayFolderListItem {
+                        id: groupFoldersEntry
+
+                        property bool isGroupFolder: model.modelData.isGroupFolder
+
+                        text: model.modelData.name
+                        toolTipText: !isGroupFolder ? qsTr("Open local folder \"%1\"").arg(model.modelData.fullPath) : qsTr("Open groupfolder \"%1\"").arg(model.modelData.fullPath)
+                        subline: model.modelData.parentPath
+                        width: foldersMenuListView.width
+                        height: Style.standardPrimaryButtonHeight
+                        iconSource: !isGroupFolder ? "image://svgimage-custom-color/folder.svg/" + Style.ncTextColor : "image://svgimage-custom-color/folder-group.svg/" + Style.ncTextColor
+
+                        onTriggered: {
+                            foldersMenu.close();
+                            root.folderEntryTriggered(model.modelData.fullPath, isGroupFolder);
+                        }
+
+                        Accessible.role: Accessible.MenuItem
+                        Accessible.name: qsTr("Open %1 in file explorer").arg(title)
+                        Accessible.onPressAction: groupFoldersEntry.triggered()
+                    }
+
+                    Accessible.role: Accessible.PopupMenu
+                    Accessible.name: qsTr("User group and local folders menu")
+                }
+            }
+
+            Component.onCompleted: {
+                foldersMenuLoader.openMenu = open
+                foldersMenuLoader.closeMenu = close
+            }
+
+            Connections {
+                onVisibleChanged: foldersMenuLoader.isMenuVisible = visible
+            }
+        }
+    }
+}
index 8909a372d15c0188da959e296c44f456a923a981..3a3c2edfd25722ef19c316a326c5c574ae75649d 100644 (file)
@@ -24,6 +24,7 @@ RowLayout {
     property color titleColor: Style.ncTextColor
     property color sublineColor: Style.ncSecondaryTextColor
 
+
     Accessible.role: Accessible.ListItem
     Accessible.name: resultTitle
     Accessible.onPressAction: unifiedSearchResultMouseArea.clicked()
@@ -79,29 +80,16 @@ RowLayout {
         }
     }
 
-    ColumnLayout {
+    ListItemLineAndSubline {
         id: unifiedSearchResultTextContainer
 
+        spacing: Style.standardSpacing
+
         Layout.fillWidth: true
         Layout.rightMargin: Style.trayHorizontalMargin
-        spacing: Style.standardSpacing
 
-        EnforcedPlainTextLabel {
-            id: unifiedSearchResultTitleText
-            Layout.fillWidth: true
-            text: unifiedSearchResultItemDetails.title.replace(/[\r\n]+/g, " ")
-            elide: Text.ElideRight
-            font.pixelSize: unifiedSearchResultItemDetails.titleFontSize
-            color: unifiedSearchResultItemDetails.titleColor
-        }
-        EnforcedPlainTextLabel {
-            id: unifiedSearchResultTextSubline
-            Layout.fillWidth: true
-            text: unifiedSearchResultItemDetails.subline.replace(/[\r\n]+/g, " ")
-            elide: Text.ElideRight
-            font.pixelSize: unifiedSearchResultItemDetails.sublineFontSize
-            color: unifiedSearchResultItemDetails.sublineColor
-        }
+        lineText: unifiedSearchResultItemDetails.title.replace(/[\r\n]+/g, " ")
+        sublineText: unifiedSearchResultItemDetails.subline.replace(/[\r\n]+/g, " ")
     }
 
 }
index fadd3a7296d7f48a1a3e9bd7e6701189a22eab48..84bb841c3e6e6c7b598d4241cd1be26d70ed3fb8 100644 (file)
@@ -83,6 +83,7 @@ ApplicationWindow {
             if(Systray.isOpen) {\r
                 accountMenu.close();\r
                 appsMenu.close();\r
+                openLocalFolderButton.closeMenu()\r
             }\r
         }\r
 \r
@@ -466,25 +467,25 @@ ApplicationWindow {
                                 id: currentAccountStatusIndicatorBackground\r
                                 visible: UserModel.currentUser.isConnected\r
                                          && UserModel.currentUser.serverHasUserStatus\r
-                                width: Style.accountAvatarStateIndicatorSize + 2\r
+                                width: Style.accountAvatarStateIndicatorSize +  + Style.trayFolderStatusIndicatorSizeOffset\r
                                 height: width\r
                                 anchors.bottom: currentAccountAvatar.bottom\r
                                 anchors.right: currentAccountAvatar.right\r
                                 color: Style.currentUserHeaderColor\r
-                                radius: width*0.5\r
+                                radius: width * Style.trayFolderStatusIndicatorRadiusFactor\r
                             }\r
 \r
                             Rectangle {\r
                                 id: currentAccountStatusIndicatorMouseHover\r
                                 visible: UserModel.currentUser.isConnected\r
                                          && UserModel.currentUser.serverHasUserStatus\r
-                                width: Style.accountAvatarStateIndicatorSize + 2\r
+                                width: Style.accountAvatarStateIndicatorSize +  + Style.trayFolderStatusIndicatorSizeOffset\r
                                 height: width\r
                                 anchors.bottom: currentAccountAvatar.bottom\r
                                 anchors.right: currentAccountAvatar.right\r
                                 color: currentAccountButton.hovered ? Style.currentUserHeaderTextColor : "transparent"\r
-                                opacity: 0.2\r
-                                radius: width*0.5\r
+                                opacity: Style.trayFolderStatusIndicatorMouseHoverOpacityFactor\r
+                                radius: width * Style.trayFolderStatusIndicatorRadiusFactor\r
                             }\r
 \r
                             Image {\r
@@ -586,62 +587,18 @@ ApplicationWindow {
                     Layout.fillWidth: true\r
                 }\r
 \r
-                RowLayout {\r
-                    id: openLocalFolderRowLayout\r
-                    spacing: 0\r
-                    Layout.preferredWidth:  Style.trayWindowHeaderHeight\r
-                    Layout.preferredHeight: Style.trayWindowHeaderHeight\r
-                    Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter\r
-\r
-                    Accessible.role: Accessible.Button\r
-                    Accessible.name: qsTr("Open local folder of current account")\r
-\r
-                    HeaderButton {\r
-                        id: openLocalFolderButton\r
-                        visible: UserModel.currentUser.hasLocalFolder\r
-                        icon.source: "qrc:///client/theme/white/folder.svg"\r
-                        icon.color: Style.currentUserHeaderTextColor\r
-                        onClicked: UserModel.openCurrentAccountLocalFolder()\r
+                TrayFoldersMenuButton {\r
+                    id: openLocalFolderButton\r
 \r
-                        Image {\r
-                            id: folderStateIndicator\r
-                            visible: UserModel.currentUser.hasLocalFolder\r
-                            source: UserModel.currentUser.isConnected\r
-                                    ? Style.stateOnlineImageSource\r
-                                    : Style.stateOfflineImageSource\r
-                            cache: false\r
+                    visible: currentUser.hasLocalFolder\r
+                    currentUser: UserModel.currentUser\r
 \r
-                            anchors.top: openLocalFolderButton.verticalCenter\r
-                            anchors.left: openLocalFolderButton.horizontalCenter\r
-                            sourceSize.width: Style.folderStateIndicatorSize\r
-                            sourceSize.height: Style.folderStateIndicatorSize\r
+                    Layout.preferredWidth:  Style.iconButtonWidth * Style.trayFolderListButtonWidthScaleFactor\r
+                    Layout.alignment: Qt.AlignHCenter\r
 \r
-                            Accessible.role: Accessible.Indicator\r
-                            Accessible.name: UserModel.currentUser.isConnected ? qsTr("Connected") : qsTr("Disconnected")\r
-                            z: 1\r
-\r
-                            Rectangle {\r
-                                id: folderStateIndicatorBackground\r
-                                width: Style.folderStateIndicatorSize + 2\r
-                                height: width\r
-                                anchors.centerIn: parent\r
-                                color: Style.currentUserHeaderColor\r
-                                radius: width*0.5\r
-                                z: -2\r
-                            }\r
+                    onClicked: openLocalFolderButton.userHasGroupFolders ? openLocalFolderButton.toggleMenuOpen() : UserModel.openCurrentAccountLocalFolder()\r
 \r
-                            Rectangle {\r
-                                id: folderStateIndicatorBackgroundMouseHover\r
-                                width: Style.folderStateIndicatorSize + 2\r
-                                height: width\r
-                                anchors.centerIn: parent\r
-                                color: openLocalFolderButton.hovered ? Style.currentUserHeaderTextColor : "transparent"\r
-                                opacity: 0.2\r
-                                radius: width*0.5\r
-                                z: -1\r
-                            }\r
-                        }\r
-                    }\r
+                    onFolderEntryTriggered: isGroupFolder ? UserModel.openCurrentAccountFolderFromTrayInfo(fullFolderPath) : UserModel.openCurrentAccountLocalFolder()\r
                 }\r
 \r
                 HeaderButton {\r
@@ -678,9 +635,9 @@ ApplicationWindow {
 \r
                     Menu {\r
                         id: appsMenu\r
-                        x: -2\r
-                        y: (trayWindowAppsButton.y + trayWindowAppsButton.height + 2)\r
-                        width: Style.trayWindowWidth * 0.35\r
+                        x: Style.trayWindowMenuOffsetX\r
+                        y: (trayWindowAppsButton.y + trayWindowAppsButton.height + Style.trayWindowMenuOffsetY)\r
+                        width: Style.trayWindowWidth * Style.trayWindowMenuWidthFactor\r
                         height: implicitHeight + y > Style.trayWindowHeight ? Style.trayWindowHeight - y : implicitHeight\r
                         closePolicy: Menu.CloseOnPressOutsideParent | Menu.CloseOnEscape\r
 \r
index deae25c513d332c096b65a61e5fa1dc13a5f6c9b..f3dc30c0870a1aae5296a1898563ef6dda574b10 100644 (file)
@@ -36,6 +36,19 @@ constexpr qint64 activityDefaultExpirationTimeMsecs = 1000 * 60 * 10;
 }
 
 namespace OCC {
+    
+TrayFolderInfo::TrayFolderInfo(const QString &name, const QString &parentPath, const QString &fullPath, FolderType folderType)
+    : _name(name)
+    , _parentPath(parentPath)
+    , _fullPath(fullPath)
+    , _folderType(folderType)
+{
+}
+
+bool TrayFolderInfo::isGroupFolder() const
+{
+    return _folderType == GroupFolder;
+}
 
 User::User(AccountStatePtr &account, const bool &isCurrent, QObject *parent)
     : QObject(parent)
@@ -76,6 +89,8 @@ User::User(AccountStatePtr &account, const bool &isCurrent, QObject *parent)
     connect(_account->account().data(), &Account::capabilitiesChanged, this, &User::headerTextColorChanged);
     connect(_account->account().data(), &Account::capabilitiesChanged, this, &User::accentColorChanged);
 
+    connect(_account->account().data(), &Account::capabilitiesChanged, this, &User::slotAccountCapabilitiesChangedRefreshGroupFolders);
+
     connect(_activityModel, &ActivityListModel::sendNotificationRequest, this, &User::slotSendNotificationRequest);
     
     connect(this, &User::sendReplyMessage, this, &User::slotSendReplyMessage);
@@ -299,6 +314,36 @@ void User::slotCheckExpiredActivities()
     }
 }
 
+void User::parseNewGroupFolderPath(const QString &mountPoint)
+{
+    if (mountPoint.isEmpty()) {
+        return;
+    }
+    auto mountPointSplit = mountPoint.split(QLatin1Char('/'), Qt::SkipEmptyParts);
+
+    if (mountPointSplit.isEmpty()) {
+        return;
+    }
+
+    const auto groupFolderName = mountPointSplit.takeLast();
+    const auto parentPath = mountPointSplit.join(QLatin1Char('/'));
+    _trayFolderInfos.push_back(QVariant::fromValue(TrayFolderInfo{groupFolderName, parentPath, mountPoint, TrayFolderInfo::GroupFolder}));
+}
+
+void User::prePendGroupFoldersWithLocalFolder()
+{
+    if (!_trayFolderInfos.isEmpty() && !_trayFolderInfos.first().value<TrayFolderInfo>().isGroupFolder()) {
+        return;
+    }
+    const auto localFolderName = getFolder()->shortGuiLocalPath();
+    auto localFolderPathSplit = getFolder()->path().split(QLatin1Char('/'), Qt::SkipEmptyParts);
+    if (!localFolderPathSplit.isEmpty()) {
+        localFolderPathSplit.removeLast();
+    }
+    const auto localFolderParentPath = !localFolderPathSplit.isEmpty() ? localFolderPathSplit.join(QLatin1Char('/')) : "/";
+    _trayFolderInfos.push_front(QVariant::fromValue(TrayFolderInfo{localFolderName, localFolderParentPath, getFolder()->path(), TrayFolderInfo::Folder}));
+}
+
 void User::connectPushNotifications() const
 {
     connect(_account->account().data(), &Account::pushNotificationsDisabled, this, &User::slotDisconnectPushNotifications, Qt::UniqueConnection);
@@ -747,6 +792,11 @@ void User::processCompletedSyncItem(const Folder *folder, const SyncFileItemPtr
     }
 }
 
+const QVariantList &User::groupFolders() const
+{
+    return _trayFolderInfos;
+}
+
 void User::slotItemCompleted(const QString &folder, const SyncFileItemPtr &item)
 {
     auto folderInstance = FolderMan::instance()->folder(folder);
@@ -804,6 +854,42 @@ void User::openLocalFolder()
     }
 }
 
+void User::openFolderLocallyOrInBrowser(const QString &fullRemotePath)
+{
+    const auto folder = getFolder();
+
+    if (!folder) {
+        return;
+    }
+
+    // remove remote path prefix and leading slash
+    auto fullRemotePathToPathInDb = folder->remotePath() != QStringLiteral("/") ? fullRemotePath.mid(folder->remotePathTrailingSlash().size()) : fullRemotePath;
+    if (fullRemotePathToPathInDb.startsWith("/")) {
+        fullRemotePathToPathInDb = fullRemotePathToPathInDb.mid(1);
+    }
+
+    SyncJournalFileRecord rec;
+    if (folder->journalDb()->getFileRecord(fullRemotePathToPathInDb, &rec) && rec.isValid()) {
+        // found folder locally, going to open
+        qCInfo(lcActivity) << "Opening locally a folder" << fullRemotePath;
+        QDesktopServices::openUrl(QUrl::fromLocalFile(folder->path() + rec.path()));
+        return;
+    }
+
+    // try to open it in browser
+    auto folderUrlForBrowser = Utility::concatUrlPath(_account->account()->url(), QStringLiteral("/index.php/apps/files/"));
+    QUrlQuery urlQuery;
+    urlQuery.addQueryItem(QStringLiteral("dir"), fullRemotePath);
+    folderUrlForBrowser.setQuery(urlQuery);
+    if (!folderUrlForBrowser.scheme().startsWith(QStringLiteral("http"))) {
+        folderUrlForBrowser.setScheme(QStringLiteral("https"));
+    }
+    // open https://server.com/index.php/apps/files/?dir=/group_folder/path
+    qCInfo(lcActivity) << "Opening in browser a folder" << fullRemotePath;
+    Utility::openBrowser(folderUrlForBrowser);
+    return;
+}
+
 void User::login() const
 {
     _account->account()->resetRejectedCertificates();
@@ -945,6 +1031,99 @@ void User::forceSyncNow() const
     FolderMan::instance()->forceSyncForFolder(getFolder());
 }
 
+void User::slotAccountCapabilitiesChangedRefreshGroupFolders()
+{
+    if (!_account->account()->capabilities().groupFoldersAvailable()) {
+        if (!_trayFolderInfos.isEmpty()) {
+            _trayFolderInfos.clear();
+            emit groupFoldersChanged();
+        }
+        return;
+    }
+
+    slotFetchGroupFolders();
+}
+
+void User::slotFetchGroupFolders()
+{
+    QNetworkRequest req;
+    req.setRawHeader(QByteArrayLiteral("OCS-APIREQUEST"), QByteArrayLiteral("true"));
+    QUrlQuery query;
+    query.addQueryItem(QLatin1String("format"), QLatin1String("json"));
+    query.addQueryItem(QLatin1String("applicable"), QLatin1String("1"));
+    QUrl groupFolderListUrl = Utility::concatUrlPath(_account->account()->url(), QStringLiteral("/index.php/apps/groupfolders/folders"));
+    groupFolderListUrl.setQuery(query);
+
+    const auto groupFolderListJob = _account->account()->sendRequest(QByteArrayLiteral("GET"), groupFolderListUrl, req);
+    connect(groupFolderListJob, &SimpleNetworkJob::finishedSignal, this, &User::slotGroupFoldersFetched);
+}
+
+void User::slotGroupFoldersFetched(QNetworkReply *reply)
+{
+    Q_ASSERT(reply);
+    if (!reply) {
+        qCWarning(lcActivity) << "Group folders fetch error";
+        return;
+    }
+
+    const auto oldSize = _trayFolderInfos.size();
+    const auto oldTrayFolderInfos = _trayFolderInfos;
+    _trayFolderInfos.clear();
+
+    const auto replyData = reply->readAll();
+    if (reply->error() != QNetworkReply::NoError) {
+        if (oldSize != _trayFolderInfos.size()) {
+            emit groupFoldersChanged();
+        }
+        qCWarning(lcActivity) << "Group folders fetch error" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() << replyData;
+        return;
+    }
+
+    QJsonParseError jsonParseError{};
+    const auto json = QJsonDocument::fromJson(replyData, &jsonParseError);
+
+    if (jsonParseError.error != QJsonParseError::NoError) {
+        qCWarning(lcActivity) << "Group folders JSON parse error" << jsonParseError.error << jsonParseError.errorString();
+        if (oldSize != _trayFolderInfos.size()) {
+            emit groupFoldersChanged();
+        }
+        return;
+    }
+
+    const auto obj = json.object().toVariantMap();
+    const auto groupFolders = obj["ocs"].toMap()["data"].toMap();
+
+    for (const auto &groupFolder : groupFolders.values()) {
+        const auto groupFolderInfo = groupFolder.toMap();
+        const auto mountPoint = groupFolderInfo.value(QStringLiteral("mount_point"), {}).toString();
+        parseNewGroupFolderPath(mountPoint);
+    }
+    std::sort(std::begin(_trayFolderInfos), std::end(_trayFolderInfos), [](const auto &leftVariant, const auto &rightVariant) {
+        const auto folderInfoA = leftVariant.template value<TrayFolderInfo>();
+        const auto folderInfoB = rightVariant.template value<TrayFolderInfo>();
+        return folderInfoA._fullPath < folderInfoB._fullPath;
+    });
+
+    if (!_trayFolderInfos.isEmpty()) {
+        if (hasLocalFolder()) {
+            prePendGroupFoldersWithLocalFolder();
+        }
+    }
+
+    if (oldSize != _trayFolderInfos.size()) {
+        emit groupFoldersChanged();
+    } else {
+        for (int i = 0; i < oldTrayFolderInfos.size(); ++i) {
+            const auto oldFolderInfo = oldTrayFolderInfos.at(i).template value<TrayFolderInfo>();
+            const auto newFolderInfo = _trayFolderInfos.at(i).template value<TrayFolderInfo>();
+            if (oldFolderInfo._folderType != newFolderInfo._folderType || oldFolderInfo._fullPath != newFolderInfo._fullPath) {
+                break;
+                emit groupFoldersChanged();
+            }
+        }
+    }
+}
+
 /*-------------------------------------------------------------------------------------*/
 
 UserModel *UserModel::_instance = nullptr;
@@ -1105,6 +1284,15 @@ void UserModel::openCurrentAccountServer()
     QDesktopServices::openUrl(url);
 }
 
+void UserModel::openCurrentAccountFolderFromTrayInfo(const QString &fullRemotePath)
+{
+    if (_currentUserId < 0 || _currentUserId >= _users.size()) {
+        return;
+    }
+
+    _users[_currentUserId]->openFolderLocallyOrInBrowser(fullRemotePath);
+}
+
 void UserModel::setCurrentUserId(const int id)
 {
     Q_ASSERT(id < _users.size());
@@ -1296,7 +1484,6 @@ int UserModel::findUserIdForAccount(AccountState *account) const
     const auto id = std::distance(std::cbegin(_users), it);
     return id;
 }
-
 /*-------------------------------------------------------------------------------------*/
 
 ImageProvider::ImageProvider()
index c287fe82c66e5515c2f962cabd3791b84369bcaa..e17059969f348ca4e6212a3242c8b2e2709941c4 100644 (file)
 namespace OCC {
 class UnifiedSearchResultsListModel;
 
+
+class TrayFolderInfo
+{
+    Q_GADGET
+
+    Q_PROPERTY(QString name MEMBER _name)
+    Q_PROPERTY(QString parentPath MEMBER _parentPath)
+    Q_PROPERTY(QString fullPath MEMBER _fullPath)
+    Q_PROPERTY(bool isGroupFolder READ isGroupFolder CONSTANT)
+public:
+    enum FolderType { Folder, GroupFolder };
+
+    TrayFolderInfo(const QString &name, const QString &parentPath, const QString &fullPath, FolderType folderType);
+    TrayFolderInfo() = default;
+    [[nodiscard]] bool isGroupFolder() const;
+
+    QString _name;
+    QString _parentPath;
+    QString _fullPath;
+    FolderType _folderType = Folder;
+};
+
 class User : public QObject
 {
     Q_OBJECT
@@ -37,6 +59,7 @@ class User : public QObject
     Q_PROPERTY(QString avatar READ avatarUrl NOTIFY avatarChanged)
     Q_PROPERTY(bool isConnected READ isConnected NOTIFY accountStateChanged)
     Q_PROPERTY(UnifiedSearchResultsListModel* unifiedSearchResultsListModel READ getUnifiedSearchResultsListModel CONSTANT)
+    Q_PROPERTY(QVariantList groupFolders READ groupFolders NOTIFY groupFoldersChanged)
 
 public:
     User(AccountStatePtr &account, const bool &isCurrent = false, QObject *parent = nullptr);
@@ -51,6 +74,7 @@ public:
     ActivityListModel *getActivityModel();
     [[nodiscard]] UnifiedSearchResultsListModel *getUnifiedSearchResultsListModel() const;
     void openLocalFolder();
+    void openFolderLocallyOrInBrowser(const QString &fullRemotePath);
     [[nodiscard]] QString name() const;
     [[nodiscard]] QString server(bool shortened = true) const;
     [[nodiscard]] bool hasLocalFolder() const;
@@ -73,6 +97,7 @@ public:
     [[nodiscard]] QUrl statusIcon() const;
     [[nodiscard]] QString statusEmoji() const;
     void processCompletedSyncItem(const Folder *folder, const SyncFileItemPtr &item);
+    [[nodiscard]] const QVariantList &groupFolders() const;
 
 signals:
     void nameChanged();
@@ -86,6 +111,7 @@ signals:
     void headerTextColorChanged();
     void accentColorChanged();
     void sendReplyMessage(const int activityIndex, const QString &conversationToken, const QString &message, const QString &replyTo);
+    void groupFoldersChanged();
 
 public slots:
     void slotItemCompleted(const QString &folder, const OCC::SyncFileItemPtr &item);
@@ -109,6 +135,8 @@ public slots:
     void slotRebuildNavigationAppList();
     void slotSendReplyMessage(const int activityIndex, const QString &conversationToken, const QString &message, const QString &replyTo);
     void forceSyncNow() const;
+    void slotAccountCapabilitiesChangedRefreshGroupFolders();
+    void slotFetchGroupFolders();
 
 private slots:
     void slotPushNotificationsReady();
@@ -116,7 +144,7 @@ private slots:
     void slotReceivedPushNotification(OCC::Account *account);
     void slotReceivedPushActivity(OCC::Account *account);
     void slotCheckExpiredActivities();
-
+    void slotGroupFoldersFetched(QNetworkReply *reply);
     void checkNotifiedNotifications();
     void showDesktopNotification(const QString &title, const QString &message, const long notificationId);
     void showDesktopNotification(const OCC::Activity &activity);
@@ -124,6 +152,8 @@ private slots:
     void showDesktopTalkNotification(const OCC::Activity &activity);
 
 private:
+    void prePendGroupFoldersWithLocalFolder();
+    void parseNewGroupFolderPath(const QString &path);
     void connectPushNotifications() const;
     [[nodiscard]] bool checkPushNotificationsAreReady() const;
 
@@ -138,6 +168,8 @@ private:
     ActivityListModel *_activityModel;
     UnifiedSearchResultsListModel *_unifiedSearchResultsModel;
     ActivityList _blacklistedNotifications;
+    
+    QVariantList _trayFolderInfos;
 
     QTimer _expiredActivitiesCheckTimer;
     QTimer _notificationCheckTimer;
@@ -158,6 +190,7 @@ class UserModel : public QAbstractListModel
     Q_PROPERTY(User* currentUser READ currentUser NOTIFY currentUserChanged)
     Q_PROPERTY(int currentUserId READ currentUserId WRITE setCurrentUserId NOTIFY currentUserChanged)
 public:
+
     static UserModel *instance();
     ~UserModel() override = default;
 
@@ -208,6 +241,7 @@ public slots:
     void openCurrentAccountLocalFolder();
     void openCurrentAccountTalk();
     void openCurrentAccountServer();
+    void openCurrentAccountFolderFromTrayInfo(const QString &fullRemotePath);
     void setCurrentUserId(const int id);
     void login(const int id);
     void logout(const int id);
index 12b049823e04ecd37c7c8b8d82bff2b7fd96a55a..386880b12ab031de159d3466d5eb64718853da04 100644 (file)
@@ -350,6 +350,11 @@ bool Capabilities::uploadConflictFiles() const
     return _capabilities[QStringLiteral("uploadConflictFiles")].toBool();
 }
 
+bool Capabilities::groupFoldersAvailable() const
+{
+    return _capabilities[QStringLiteral("groupfolders")].toMap().value(QStringLiteral("hasGroupFolders"), false).toBool();
+}
+
 QStringList Capabilities::blacklistedFiles() const
 {
     return _capabilities["files"].toMap()["blacklisted_files"].toStringList();
index a65329de00f7cad1c2f93f9761a0d4f1da28a7f2..826d8e6b91072575a1b650d7b17b7fc7b1869915 100644 (file)
@@ -167,6 +167,8 @@ public:
      */
     [[nodiscard]] bool uploadConflictFiles() const;
 
+    [[nodiscard]] bool groupFoldersAvailable() const;
+
     // Direct Editing
     void addDirectEditor(DirectEditor* directEditor);
     DirectEditor* getDirectEditorForMimetype(const QMimeType &mimeType);
index 54e8e4c38ab78a6c732acd1e8faa9ee69dca4086..bc83426e0ba76de264565726dced400d25d253a9 100644 (file)
@@ -368,6 +368,7 @@ void DiscoverySingleDirectoryJob::start()
           << "http://owncloud.org/ns:dDC"
           << "http://owncloud.org/ns:permissions"
           << "http://owncloud.org/ns:checksums";
+
     if (_isRootPath)
         props << "http://owncloud.org/ns:data-fingerprint";
     if (_account->serverVersionInt() >= Account::makeServerVersion(10, 0, 0)) {
index cdc669023517f42d2d1fb3e4d4f4cc69df64ae05..8d6996aeea74b653ebf537caa8f8248ac2726a09 100644 (file)
@@ -86,6 +86,7 @@
         <file>theme/colored/state-warning-256.png</file>
         <file>theme/black/folder.png</file>
         <file>theme/black/folder.svg</file>
+        <file>theme/black/folder-group.svg</file>
         <file>theme/black/folder@2x.png</file>
         <file>theme/white/folder.png</file>
         <file>theme/white/folder@2x.png</file>
index 1d788c55d72a62510af1cfd00d4839ca83aa4afd..7d2370b99ac78f60b18d696611cbcdc3a849aa9d 100644 (file)
@@ -51,6 +51,7 @@ QtObject {
 \r
     property int standardSpacing: 10\r
     property int smallSpacing: 5\r
+    property int extraSmallSpacing: 2\r
 \r
     property int iconButtonWidth: 36\r
     property int standardPrimaryButtonHeight: 40\r
@@ -131,6 +132,20 @@ QtObject {
 \r
     readonly property int activityContentSpace: 4\r
 \r
+    readonly property double smallIconScaleFactor: 0.6\r
+\r
+    readonly property double trayFolderListButtonWidthScaleFactor: 1.75\r
+    readonly property int trayFolderStatusIndicatorSizeOffset: 2\r
+    readonly property double trayFolderStatusIndicatorRadiusFactor: 0.5\r
+    readonly property double trayFolderStatusIndicatorMouseHoverOpacityFactor: 0.2\r
+\r
+    readonly property double trayWindowMenuWidthFactor: 0.35\r
+\r
+    readonly property int trayWindowMenuOffsetX: -2\r
+    readonly property int trayWindowMenuOffsetY: 2\r
+\r
+    readonly property int trayWindowMenuEntriesMargin: 6\r
+\r
     function variableSize(size) {\r
         return size * (1 + Math.min(pixelSize / 100, 1));\r
     }\r
diff --git a/theme/black/folder-group.svg b/theme/black/folder-group.svg
new file mode 100644 (file)
index 0000000..7862905
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="32" viewBox="0 0 32 32" width="32" version="1.1"><path style=" stroke:none;fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 2.921875 4 C 2.421875 4 2 4.421875 2 4.921875 L 2 27.078125 C 2 27.59375 2.40625 28 2.921875 28 L 29.078125 28 C 29.59375 28 30 27.59375 30 27.078125 L 30 8.925781 C 29.996094 8.417969 29.585938 8.003906 29.078125 7.992188 L 16 7.992188 L 12 4 Z M 18.085938 11.640625 C 19.429688 11.640625 20.566406 12.621094 20.566406 13.886719 L 20.566406 13.914062 C 20.550781 14.308594 20.550781 14.761719 20.28125 15.851562 L 20.257812 15.875 C 20.183594 16.0625 20.078125 16.238281 19.945312 16.394531 C 19.8125 16.566406 19.65625 16.738281 19.507812 16.910156 L 19.328125 17.117188 C 19.300781 17.21875 19.269531 17.324219 19.25 17.425781 C 19.175781 17.800781 19.210938 18.042969 19.25 18.125 C 19.914062 18.40625 20.585938 18.660156 21.160156 18.902344 C 21.789062 19.167969 22.328125 19.429688 22.6875 19.859375 C 22.738281 19.917969 22.773438 19.988281 22.789062 20.066406 C 22.828125 20.375 22.859375 20.898438 22.894531 21.433594 C 22.929688 21.96875 22.964844 22.515625 22.996094 22.753906 C 23.015625 22.921875 22.933594 23.082031 22.789062 23.167969 C 21.65625 23.769531 19.875 24.035156 18.085938 24.042969 C 16.296875 24.050781 14.5 23.800781 13.332031 23.167969 C 13.1875 23.082031 13.105469 22.921875 13.125 22.753906 C 13.167969 22.410156 13.210938 21.984375 13.261719 21.558594 L 13.21875 21.5625 C 11.988281 21.566406 10.757812 21.394531 9.957031 20.960938 C 9.859375 20.902344 9.804688 20.789062 9.816406 20.675781 C 9.882812 20.148438 9.96875 19.363281 10.027344 18.832031 C 10.027344 18.820312 10.027344 18.808594 10.027344 18.796875 C 10.089844 18.539062 10.269531 18.449219 10.453125 18.335938 C 10.636719 18.222656 10.863281 18.125 11.109375 18.019531 C 11.550781 17.828125 12.042969 17.660156 12.4375 17.523438 C 12.457031 17.359375 12.445312 17.191406 12.402344 17.027344 L 12.332031 16.722656 L 12.296875 16.6875 C 12.179688 16.566406 12.066406 16.445312 11.960938 16.316406 C 11.859375 16.199219 11.785156 16.113281 11.730469 15.960938 L 11.710938 15.945312 L 11.710938 15.925781 C 11.597656 15.492188 11.53125 15.046875 11.515625 14.597656 C 11.515625 13.726562 12.296875 13.054688 13.21875 13.054688 C 14.136719 13.054688 14.921875 13.726562 14.921875 14.597656 L 14.921875 14.617188 C 14.910156 14.886719 14.910156 15.199219 14.726562 15.945312 L 14.707031 15.964844 C 14.65625 16.09375 14.585938 16.210938 14.496094 16.316406 C 14.40625 16.433594 14.296875 16.554688 14.195312 16.671875 L 14.070312 16.8125 C 14.050781 16.882812 14.03125 16.960938 14.015625 17.027344 C 13.972656 17.183594 13.972656 17.347656 14.015625 17.503906 C 14.472656 17.699219 14.9375 17.871094 15.328125 18.035156 C 15.628906 18.160156 15.882812 18.304688 16.101562 18.46875 C 16.390625 18.367188 16.695312 18.238281 16.949219 18.152344 C 16.976562 17.910156 16.957031 17.664062 16.898438 17.429688 C 16.867188 17.289062 16.824219 17.132812 16.792969 16.988281 L 16.742188 16.9375 C 16.570312 16.761719 16.40625 16.582031 16.25 16.394531 C 16.101562 16.222656 15.988281 16.097656 15.914062 15.878906 L 15.890625 15.851562 L 15.890625 15.824219 C 15.648438 14.78125 15.613281 14.289062 15.605469 13.886719 C 15.605469 12.621094 16.742188 11.640625 18.085938 11.640625 Z M 18.085938 11.640625 "/></svg>