Group folder visibility improvements. Show dropdown in tray window.

Signed-off-by: alex-z <blackslayer4@gmail.com>
This commit is contained in:
alex-z 2023-04-21 17:48:44 +02:00
parent b0a2150327
commit 3be820d9a3
14 changed files with 612 additions and 80 deletions

View file

@ -52,5 +52,8 @@
<file>theme/Style/Style.qml</file> <file>theme/Style/Style.qml</file>
<file>theme/Style/qmldir</file> <file>theme/Style/qmldir</file>
<file>src/gui/filedetails/NCRadioButton.qml</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> </qresource>
</RCC> </RCC>

View file

@ -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
}
}

View file

@ -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
}
}
}

View file

@ -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
}
}
}
}

View file

@ -24,6 +24,7 @@ RowLayout {
property color titleColor: Style.ncTextColor property color titleColor: Style.ncTextColor
property color sublineColor: Style.ncSecondaryTextColor property color sublineColor: Style.ncSecondaryTextColor
Accessible.role: Accessible.ListItem Accessible.role: Accessible.ListItem
Accessible.name: resultTitle Accessible.name: resultTitle
Accessible.onPressAction: unifiedSearchResultMouseArea.clicked() Accessible.onPressAction: unifiedSearchResultMouseArea.clicked()
@ -79,29 +80,16 @@ RowLayout {
} }
} }
ColumnLayout { ListItemLineAndSubline {
id: unifiedSearchResultTextContainer id: unifiedSearchResultTextContainer
spacing: Style.standardSpacing
Layout.fillWidth: true Layout.fillWidth: true
Layout.rightMargin: Style.trayHorizontalMargin Layout.rightMargin: Style.trayHorizontalMargin
spacing: Style.standardSpacing
EnforcedPlainTextLabel { lineText: unifiedSearchResultItemDetails.title.replace(/[\r\n]+/g, " ")
id: unifiedSearchResultTitleText sublineText: unifiedSearchResultItemDetails.subline.replace(/[\r\n]+/g, " ")
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
}
} }
} }

View file

@ -83,6 +83,7 @@ ApplicationWindow {
if(Systray.isOpen) { if(Systray.isOpen) {
accountMenu.close(); accountMenu.close();
appsMenu.close(); appsMenu.close();
openLocalFolderButton.closeMenu()
} }
} }
@ -466,25 +467,25 @@ ApplicationWindow {
id: currentAccountStatusIndicatorBackground id: currentAccountStatusIndicatorBackground
visible: UserModel.currentUser.isConnected visible: UserModel.currentUser.isConnected
&& UserModel.currentUser.serverHasUserStatus && UserModel.currentUser.serverHasUserStatus
width: Style.accountAvatarStateIndicatorSize + 2 width: Style.accountAvatarStateIndicatorSize + + Style.trayFolderStatusIndicatorSizeOffset
height: width height: width
anchors.bottom: currentAccountAvatar.bottom anchors.bottom: currentAccountAvatar.bottom
anchors.right: currentAccountAvatar.right anchors.right: currentAccountAvatar.right
color: Style.currentUserHeaderColor color: Style.currentUserHeaderColor
radius: width*0.5 radius: width * Style.trayFolderStatusIndicatorRadiusFactor
} }
Rectangle { Rectangle {
id: currentAccountStatusIndicatorMouseHover id: currentAccountStatusIndicatorMouseHover
visible: UserModel.currentUser.isConnected visible: UserModel.currentUser.isConnected
&& UserModel.currentUser.serverHasUserStatus && UserModel.currentUser.serverHasUserStatus
width: Style.accountAvatarStateIndicatorSize + 2 width: Style.accountAvatarStateIndicatorSize + + Style.trayFolderStatusIndicatorSizeOffset
height: width height: width
anchors.bottom: currentAccountAvatar.bottom anchors.bottom: currentAccountAvatar.bottom
anchors.right: currentAccountAvatar.right anchors.right: currentAccountAvatar.right
color: currentAccountButton.hovered ? Style.currentUserHeaderTextColor : "transparent" color: currentAccountButton.hovered ? Style.currentUserHeaderTextColor : "transparent"
opacity: 0.2 opacity: Style.trayFolderStatusIndicatorMouseHoverOpacityFactor
radius: width*0.5 radius: width * Style.trayFolderStatusIndicatorRadiusFactor
} }
Image { Image {
@ -586,62 +587,18 @@ ApplicationWindow {
Layout.fillWidth: true Layout.fillWidth: true
} }
RowLayout { TrayFoldersMenuButton {
id: openLocalFolderRowLayout id: openLocalFolderButton
spacing: 0
Layout.preferredWidth: Style.trayWindowHeaderHeight
Layout.preferredHeight: Style.trayWindowHeaderHeight
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
Accessible.role: Accessible.Button visible: currentUser.hasLocalFolder
Accessible.name: qsTr("Open local folder of current account") currentUser: UserModel.currentUser
HeaderButton { Layout.preferredWidth: Style.iconButtonWidth * Style.trayFolderListButtonWidthScaleFactor
id: openLocalFolderButton Layout.alignment: Qt.AlignHCenter
visible: UserModel.currentUser.hasLocalFolder
icon.source: "qrc:///client/theme/white/folder.svg"
icon.color: Style.currentUserHeaderTextColor
onClicked: UserModel.openCurrentAccountLocalFolder()
Image { onClicked: openLocalFolderButton.userHasGroupFolders ? openLocalFolderButton.toggleMenuOpen() : UserModel.openCurrentAccountLocalFolder()
id: folderStateIndicator
visible: UserModel.currentUser.hasLocalFolder
source: UserModel.currentUser.isConnected
? Style.stateOnlineImageSource
: Style.stateOfflineImageSource
cache: false
anchors.top: openLocalFolderButton.verticalCenter onFolderEntryTriggered: isGroupFolder ? UserModel.openCurrentAccountFolderFromTrayInfo(fullFolderPath) : UserModel.openCurrentAccountLocalFolder()
anchors.left: openLocalFolderButton.horizontalCenter
sourceSize.width: Style.folderStateIndicatorSize
sourceSize.height: Style.folderStateIndicatorSize
Accessible.role: Accessible.Indicator
Accessible.name: UserModel.currentUser.isConnected ? qsTr("Connected") : qsTr("Disconnected")
z: 1
Rectangle {
id: folderStateIndicatorBackground
width: Style.folderStateIndicatorSize + 2
height: width
anchors.centerIn: parent
color: Style.currentUserHeaderColor
radius: width*0.5
z: -2
}
Rectangle {
id: folderStateIndicatorBackgroundMouseHover
width: Style.folderStateIndicatorSize + 2
height: width
anchors.centerIn: parent
color: openLocalFolderButton.hovered ? Style.currentUserHeaderTextColor : "transparent"
opacity: 0.2
radius: width*0.5
z: -1
}
}
}
} }
HeaderButton { HeaderButton {
@ -678,9 +635,9 @@ ApplicationWindow {
Menu { Menu {
id: appsMenu id: appsMenu
x: -2 x: Style.trayWindowMenuOffsetX
y: (trayWindowAppsButton.y + trayWindowAppsButton.height + 2) y: (trayWindowAppsButton.y + trayWindowAppsButton.height + Style.trayWindowMenuOffsetY)
width: Style.trayWindowWidth * 0.35 width: Style.trayWindowWidth * Style.trayWindowMenuWidthFactor
height: implicitHeight + y > Style.trayWindowHeight ? Style.trayWindowHeight - y : implicitHeight height: implicitHeight + y > Style.trayWindowHeight ? Style.trayWindowHeight - y : implicitHeight
closePolicy: Menu.CloseOnPressOutsideParent | Menu.CloseOnEscape closePolicy: Menu.CloseOnPressOutsideParent | Menu.CloseOnEscape

View file

@ -36,6 +36,19 @@ constexpr qint64 activityDefaultExpirationTimeMsecs = 1000 * 60 * 10;
} }
namespace OCC { 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) User::User(AccountStatePtr &account, const bool &isCurrent, QObject *parent)
: 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::headerTextColorChanged);
connect(_account->account().data(), &Account::capabilitiesChanged, this, &User::accentColorChanged); 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(_activityModel, &ActivityListModel::sendNotificationRequest, this, &User::slotSendNotificationRequest);
connect(this, &User::sendReplyMessage, this, &User::slotSendReplyMessage); 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 void User::connectPushNotifications() const
{ {
connect(_account->account().data(), &Account::pushNotificationsDisabled, this, &User::slotDisconnectPushNotifications, Qt::UniqueConnection); 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) void User::slotItemCompleted(const QString &folder, const SyncFileItemPtr &item)
{ {
auto folderInstance = FolderMan::instance()->folder(folder); 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 void User::login() const
{ {
_account->account()->resetRejectedCertificates(); _account->account()->resetRejectedCertificates();
@ -945,6 +1031,99 @@ void User::forceSyncNow() const
FolderMan::instance()->forceSyncForFolder(getFolder()); 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; UserModel *UserModel::_instance = nullptr;
@ -1105,6 +1284,15 @@ void UserModel::openCurrentAccountServer()
QDesktopServices::openUrl(url); 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) void UserModel::setCurrentUserId(const int id)
{ {
Q_ASSERT(id < _users.size()); Q_ASSERT(id < _users.size());
@ -1296,7 +1484,6 @@ int UserModel::findUserIdForAccount(AccountState *account) const
const auto id = std::distance(std::cbegin(_users), it); const auto id = std::distance(std::cbegin(_users), it);
return id; return id;
} }
/*-------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------*/
ImageProvider::ImageProvider() ImageProvider::ImageProvider()

View file

@ -19,6 +19,28 @@
namespace OCC { namespace OCC {
class UnifiedSearchResultsListModel; 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 class User : public QObject
{ {
Q_OBJECT Q_OBJECT
@ -37,6 +59,7 @@ class User : public QObject
Q_PROPERTY(QString avatar READ avatarUrl NOTIFY avatarChanged) Q_PROPERTY(QString avatar READ avatarUrl NOTIFY avatarChanged)
Q_PROPERTY(bool isConnected READ isConnected NOTIFY accountStateChanged) Q_PROPERTY(bool isConnected READ isConnected NOTIFY accountStateChanged)
Q_PROPERTY(UnifiedSearchResultsListModel* unifiedSearchResultsListModel READ getUnifiedSearchResultsListModel CONSTANT) Q_PROPERTY(UnifiedSearchResultsListModel* unifiedSearchResultsListModel READ getUnifiedSearchResultsListModel CONSTANT)
Q_PROPERTY(QVariantList groupFolders READ groupFolders NOTIFY groupFoldersChanged)
public: public:
User(AccountStatePtr &account, const bool &isCurrent = false, QObject *parent = nullptr); User(AccountStatePtr &account, const bool &isCurrent = false, QObject *parent = nullptr);
@ -51,6 +74,7 @@ public:
ActivityListModel *getActivityModel(); ActivityListModel *getActivityModel();
[[nodiscard]] UnifiedSearchResultsListModel *getUnifiedSearchResultsListModel() const; [[nodiscard]] UnifiedSearchResultsListModel *getUnifiedSearchResultsListModel() const;
void openLocalFolder(); void openLocalFolder();
void openFolderLocallyOrInBrowser(const QString &fullRemotePath);
[[nodiscard]] QString name() const; [[nodiscard]] QString name() const;
[[nodiscard]] QString server(bool shortened = true) const; [[nodiscard]] QString server(bool shortened = true) const;
[[nodiscard]] bool hasLocalFolder() const; [[nodiscard]] bool hasLocalFolder() const;
@ -73,6 +97,7 @@ public:
[[nodiscard]] QUrl statusIcon() const; [[nodiscard]] QUrl statusIcon() const;
[[nodiscard]] QString statusEmoji() const; [[nodiscard]] QString statusEmoji() const;
void processCompletedSyncItem(const Folder *folder, const SyncFileItemPtr &item); void processCompletedSyncItem(const Folder *folder, const SyncFileItemPtr &item);
[[nodiscard]] const QVariantList &groupFolders() const;
signals: signals:
void nameChanged(); void nameChanged();
@ -86,6 +111,7 @@ signals:
void headerTextColorChanged(); void headerTextColorChanged();
void accentColorChanged(); void accentColorChanged();
void sendReplyMessage(const int activityIndex, const QString &conversationToken, const QString &message, const QString &replyTo); void sendReplyMessage(const int activityIndex, const QString &conversationToken, const QString &message, const QString &replyTo);
void groupFoldersChanged();
public slots: public slots:
void slotItemCompleted(const QString &folder, const OCC::SyncFileItemPtr &item); void slotItemCompleted(const QString &folder, const OCC::SyncFileItemPtr &item);
@ -109,6 +135,8 @@ public slots:
void slotRebuildNavigationAppList(); void slotRebuildNavigationAppList();
void slotSendReplyMessage(const int activityIndex, const QString &conversationToken, const QString &message, const QString &replyTo); void slotSendReplyMessage(const int activityIndex, const QString &conversationToken, const QString &message, const QString &replyTo);
void forceSyncNow() const; void forceSyncNow() const;
void slotAccountCapabilitiesChangedRefreshGroupFolders();
void slotFetchGroupFolders();
private slots: private slots:
void slotPushNotificationsReady(); void slotPushNotificationsReady();
@ -116,7 +144,7 @@ private slots:
void slotReceivedPushNotification(OCC::Account *account); void slotReceivedPushNotification(OCC::Account *account);
void slotReceivedPushActivity(OCC::Account *account); void slotReceivedPushActivity(OCC::Account *account);
void slotCheckExpiredActivities(); void slotCheckExpiredActivities();
void slotGroupFoldersFetched(QNetworkReply *reply);
void checkNotifiedNotifications(); void checkNotifiedNotifications();
void showDesktopNotification(const QString &title, const QString &message, const long notificationId); void showDesktopNotification(const QString &title, const QString &message, const long notificationId);
void showDesktopNotification(const OCC::Activity &activity); void showDesktopNotification(const OCC::Activity &activity);
@ -124,6 +152,8 @@ private slots:
void showDesktopTalkNotification(const OCC::Activity &activity); void showDesktopTalkNotification(const OCC::Activity &activity);
private: private:
void prePendGroupFoldersWithLocalFolder();
void parseNewGroupFolderPath(const QString &path);
void connectPushNotifications() const; void connectPushNotifications() const;
[[nodiscard]] bool checkPushNotificationsAreReady() const; [[nodiscard]] bool checkPushNotificationsAreReady() const;
@ -138,6 +168,8 @@ private:
ActivityListModel *_activityModel; ActivityListModel *_activityModel;
UnifiedSearchResultsListModel *_unifiedSearchResultsModel; UnifiedSearchResultsListModel *_unifiedSearchResultsModel;
ActivityList _blacklistedNotifications; ActivityList _blacklistedNotifications;
QVariantList _trayFolderInfos;
QTimer _expiredActivitiesCheckTimer; QTimer _expiredActivitiesCheckTimer;
QTimer _notificationCheckTimer; QTimer _notificationCheckTimer;
@ -158,6 +190,7 @@ class UserModel : public QAbstractListModel
Q_PROPERTY(User* currentUser READ currentUser NOTIFY currentUserChanged) Q_PROPERTY(User* currentUser READ currentUser NOTIFY currentUserChanged)
Q_PROPERTY(int currentUserId READ currentUserId WRITE setCurrentUserId NOTIFY currentUserChanged) Q_PROPERTY(int currentUserId READ currentUserId WRITE setCurrentUserId NOTIFY currentUserChanged)
public: public:
static UserModel *instance(); static UserModel *instance();
~UserModel() override = default; ~UserModel() override = default;
@ -208,6 +241,7 @@ public slots:
void openCurrentAccountLocalFolder(); void openCurrentAccountLocalFolder();
void openCurrentAccountTalk(); void openCurrentAccountTalk();
void openCurrentAccountServer(); void openCurrentAccountServer();
void openCurrentAccountFolderFromTrayInfo(const QString &fullRemotePath);
void setCurrentUserId(const int id); void setCurrentUserId(const int id);
void login(const int id); void login(const int id);
void logout(const int id); void logout(const int id);

View file

@ -350,6 +350,11 @@ bool Capabilities::uploadConflictFiles() const
return _capabilities[QStringLiteral("uploadConflictFiles")].toBool(); return _capabilities[QStringLiteral("uploadConflictFiles")].toBool();
} }
bool Capabilities::groupFoldersAvailable() const
{
return _capabilities[QStringLiteral("groupfolders")].toMap().value(QStringLiteral("hasGroupFolders"), false).toBool();
}
QStringList Capabilities::blacklistedFiles() const QStringList Capabilities::blacklistedFiles() const
{ {
return _capabilities["files"].toMap()["blacklisted_files"].toStringList(); return _capabilities["files"].toMap()["blacklisted_files"].toStringList();

View file

@ -167,6 +167,8 @@ public:
*/ */
[[nodiscard]] bool uploadConflictFiles() const; [[nodiscard]] bool uploadConflictFiles() const;
[[nodiscard]] bool groupFoldersAvailable() const;
// Direct Editing // Direct Editing
void addDirectEditor(DirectEditor* directEditor); void addDirectEditor(DirectEditor* directEditor);
DirectEditor* getDirectEditorForMimetype(const QMimeType &mimeType); DirectEditor* getDirectEditorForMimetype(const QMimeType &mimeType);

View file

@ -368,6 +368,7 @@ void DiscoverySingleDirectoryJob::start()
<< "http://owncloud.org/ns:dDC" << "http://owncloud.org/ns:dDC"
<< "http://owncloud.org/ns:permissions" << "http://owncloud.org/ns:permissions"
<< "http://owncloud.org/ns:checksums"; << "http://owncloud.org/ns:checksums";
if (_isRootPath) if (_isRootPath)
props << "http://owncloud.org/ns:data-fingerprint"; props << "http://owncloud.org/ns:data-fingerprint";
if (_account->serverVersionInt() >= Account::makeServerVersion(10, 0, 0)) { if (_account->serverVersionInt() >= Account::makeServerVersion(10, 0, 0)) {

View file

@ -86,6 +86,7 @@
<file>theme/colored/state-warning-256.png</file> <file>theme/colored/state-warning-256.png</file>
<file>theme/black/folder.png</file> <file>theme/black/folder.png</file>
<file>theme/black/folder.svg</file> <file>theme/black/folder.svg</file>
<file>theme/black/folder-group.svg</file>
<file>theme/black/folder@2x.png</file> <file>theme/black/folder@2x.png</file>
<file>theme/white/folder.png</file> <file>theme/white/folder.png</file>
<file>theme/white/folder@2x.png</file> <file>theme/white/folder@2x.png</file>

View file

@ -51,6 +51,7 @@ QtObject {
property int standardSpacing: 10 property int standardSpacing: 10
property int smallSpacing: 5 property int smallSpacing: 5
property int extraSmallSpacing: 2
property int iconButtonWidth: 36 property int iconButtonWidth: 36
property int standardPrimaryButtonHeight: 40 property int standardPrimaryButtonHeight: 40
@ -131,6 +132,20 @@ QtObject {
readonly property int activityContentSpace: 4 readonly property int activityContentSpace: 4
readonly property double smallIconScaleFactor: 0.6
readonly property double trayFolderListButtonWidthScaleFactor: 1.75
readonly property int trayFolderStatusIndicatorSizeOffset: 2
readonly property double trayFolderStatusIndicatorRadiusFactor: 0.5
readonly property double trayFolderStatusIndicatorMouseHoverOpacityFactor: 0.2
readonly property double trayWindowMenuWidthFactor: 0.35
readonly property int trayWindowMenuOffsetX: -2
readonly property int trayWindowMenuOffsetY: 2
readonly property int trayWindowMenuEntriesMargin: 6
function variableSize(size) { function variableSize(size) {
return size * (1 + Math.min(pixelSize / 100, 1)); return size * (1 + Math.min(pixelSize / 100, 1));
} }

View file

@ -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>

After

Width:  |  Height:  |  Size: 3.3 KiB