<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>
--- /dev/null
+/*
+ * 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
+ }
+}
--- /dev/null
+/*
+ * 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
+ }
+ }
+}
--- /dev/null
+/*
+ * 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
+ }
+ }
+ }
+}
property color titleColor: Style.ncTextColor
property color sublineColor: Style.ncSecondaryTextColor
+
Accessible.role: Accessible.ListItem
Accessible.name: resultTitle
Accessible.onPressAction: unifiedSearchResultMouseArea.clicked()
}
}
- 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, " ")
}
}
if(Systray.isOpen) {\r
accountMenu.close();\r
appsMenu.close();\r
+ openLocalFolderButton.closeMenu()\r
}\r
}\r
\r
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
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
\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
}
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)
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);
}
}
+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);
}
}
+const QVariantList &User::groupFolders() const
+{
+ return _trayFolderInfos;
+}
+
void User::slotItemCompleted(const QString &folder, const SyncFileItemPtr &item)
{
auto folderInstance = FolderMan::instance()->folder(folder);
}
}
+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();
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;
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());
const auto id = std::distance(std::cbegin(_users), it);
return id;
}
-
/*-------------------------------------------------------------------------------------*/
ImageProvider::ImageProvider()
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
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);
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;
[[nodiscard]] QUrl statusIcon() const;
[[nodiscard]] QString statusEmoji() const;
void processCompletedSyncItem(const Folder *folder, const SyncFileItemPtr &item);
+ [[nodiscard]] const QVariantList &groupFolders() const;
signals:
void nameChanged();
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);
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();
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);
void showDesktopTalkNotification(const OCC::Activity &activity);
private:
+ void prePendGroupFoldersWithLocalFolder();
+ void parseNewGroupFolderPath(const QString &path);
void connectPushNotifications() const;
[[nodiscard]] bool checkPushNotificationsAreReady() const;
ActivityListModel *_activityModel;
UnifiedSearchResultsListModel *_unifiedSearchResultsModel;
ActivityList _blacklistedNotifications;
+
+ QVariantList _trayFolderInfos;
QTimer _expiredActivitiesCheckTimer;
QTimer _notificationCheckTimer;
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;
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);
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();
*/
[[nodiscard]] bool uploadConflictFiles() const;
+ [[nodiscard]] bool groupFoldersAvailable() const;
+
// Direct Editing
void addDirectEditor(DirectEditor* directEditor);
DirectEditor* getDirectEditorForMimetype(const QMimeType &mimeType);
<< "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)) {
<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>
\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
\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
--- /dev/null
+<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>