mirror of
https://github.com/nextcloud/desktop.git
synced 2024-11-26 15:06:08 +03:00
Merge pull request #5636 from nextcloud/feature/group-folders-visibility-improved
Feature/group folders visibility improved
This commit is contained in:
commit
127fef7fe0
14 changed files with 612 additions and 80 deletions
|
@ -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>
|
||||
|
|
52
src/gui/tray/ListItemLineAndSubline.qml
Normal file
52
src/gui/tray/ListItemLineAndSubline.qml
Normal 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
|
||||
}
|
||||
}
|
70
src/gui/tray/TrayFolderListItem.qml
Normal file
70
src/gui/tray/TrayFolderListItem.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
216
src/gui/tray/TrayFoldersMenuButton.qml
Normal file
216
src/gui/tray/TrayFoldersMenuButton.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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, " ")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -83,6 +83,7 @@ ApplicationWindow {
|
|||
if(Systray.isOpen) {
|
||||
accountMenu.close();
|
||||
appsMenu.close();
|
||||
openLocalFolderButton.closeMenu()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -466,25 +467,25 @@ ApplicationWindow {
|
|||
id: currentAccountStatusIndicatorBackground
|
||||
visible: UserModel.currentUser.isConnected
|
||||
&& UserModel.currentUser.serverHasUserStatus
|
||||
width: Style.accountAvatarStateIndicatorSize + 2
|
||||
width: Style.accountAvatarStateIndicatorSize + + Style.trayFolderStatusIndicatorSizeOffset
|
||||
height: width
|
||||
anchors.bottom: currentAccountAvatar.bottom
|
||||
anchors.right: currentAccountAvatar.right
|
||||
color: Style.currentUserHeaderColor
|
||||
radius: width*0.5
|
||||
radius: width * Style.trayFolderStatusIndicatorRadiusFactor
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: currentAccountStatusIndicatorMouseHover
|
||||
visible: UserModel.currentUser.isConnected
|
||||
&& UserModel.currentUser.serverHasUserStatus
|
||||
width: Style.accountAvatarStateIndicatorSize + 2
|
||||
width: Style.accountAvatarStateIndicatorSize + + Style.trayFolderStatusIndicatorSizeOffset
|
||||
height: width
|
||||
anchors.bottom: currentAccountAvatar.bottom
|
||||
anchors.right: currentAccountAvatar.right
|
||||
color: currentAccountButton.hovered ? Style.currentUserHeaderTextColor : "transparent"
|
||||
opacity: 0.2
|
||||
radius: width*0.5
|
||||
opacity: Style.trayFolderStatusIndicatorMouseHoverOpacityFactor
|
||||
radius: width * Style.trayFolderStatusIndicatorRadiusFactor
|
||||
}
|
||||
|
||||
Image {
|
||||
|
@ -586,62 +587,18 @@ ApplicationWindow {
|
|||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: openLocalFolderRowLayout
|
||||
spacing: 0
|
||||
Layout.preferredWidth: Style.trayWindowHeaderHeight
|
||||
Layout.preferredHeight: Style.trayWindowHeaderHeight
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
TrayFoldersMenuButton {
|
||||
id: openLocalFolderButton
|
||||
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: qsTr("Open local folder of current account")
|
||||
visible: currentUser.hasLocalFolder
|
||||
currentUser: UserModel.currentUser
|
||||
|
||||
HeaderButton {
|
||||
id: openLocalFolderButton
|
||||
visible: UserModel.currentUser.hasLocalFolder
|
||||
icon.source: "qrc:///client/theme/white/folder.svg"
|
||||
icon.color: Style.currentUserHeaderTextColor
|
||||
onClicked: UserModel.openCurrentAccountLocalFolder()
|
||||
Layout.preferredWidth: Style.iconButtonWidth * Style.trayFolderListButtonWidthScaleFactor
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
Image {
|
||||
id: folderStateIndicator
|
||||
visible: UserModel.currentUser.hasLocalFolder
|
||||
source: UserModel.currentUser.isConnected
|
||||
? Style.stateOnlineImageSource
|
||||
: Style.stateOfflineImageSource
|
||||
cache: false
|
||||
onClicked: openLocalFolderButton.userHasGroupFolders ? openLocalFolderButton.toggleMenuOpen() : UserModel.openCurrentAccountLocalFolder()
|
||||
|
||||
anchors.top: openLocalFolderButton.verticalCenter
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
onFolderEntryTriggered: isGroupFolder ? UserModel.openCurrentAccountFolderFromTrayInfo(fullFolderPath) : UserModel.openCurrentAccountLocalFolder()
|
||||
}
|
||||
|
||||
HeaderButton {
|
||||
|
@ -678,9 +635,9 @@ ApplicationWindow {
|
|||
|
||||
Menu {
|
||||
id: appsMenu
|
||||
x: -2
|
||||
y: (trayWindowAppsButton.y + trayWindowAppsButton.height + 2)
|
||||
width: Style.trayWindowWidth * 0.35
|
||||
x: Style.trayWindowMenuOffsetX
|
||||
y: (trayWindowAppsButton.y + trayWindowAppsButton.height + Style.trayWindowMenuOffsetY)
|
||||
width: Style.trayWindowWidth * Style.trayWindowMenuWidthFactor
|
||||
height: implicitHeight + y > Style.trayWindowHeight ? Style.trayWindowHeight - y : implicitHeight
|
||||
closePolicy: Menu.CloseOnPressOutsideParent | Menu.CloseOnEscape
|
||||
|
||||
|
|
|
@ -37,6 +37,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)
|
||||
, _account(account)
|
||||
|
@ -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()
|
||||
|
|
|
@ -19,6 +19,28 @@
|
|||
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;
|
||||
|
||||
|
@ -139,6 +169,8 @@ private:
|
|||
UnifiedSearchResultsListModel *_unifiedSearchResultsModel;
|
||||
ActivityList _blacklistedNotifications;
|
||||
|
||||
QVariantList _trayFolderInfos;
|
||||
|
||||
QTimer _expiredActivitiesCheckTimer;
|
||||
QTimer _notificationCheckTimer;
|
||||
QHash<AccountState *, QElapsedTimer> _timeSinceLastCheck;
|
||||
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -167,6 +167,8 @@ public:
|
|||
*/
|
||||
[[nodiscard]] bool uploadConflictFiles() const;
|
||||
|
||||
[[nodiscard]] bool groupFoldersAvailable() const;
|
||||
|
||||
// Direct Editing
|
||||
void addDirectEditor(DirectEditor* directEditor);
|
||||
DirectEditor* getDirectEditorForMimetype(const QMimeType &mimeType);
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -51,6 +51,7 @@ QtObject {
|
|||
|
||||
property int standardSpacing: 10
|
||||
property int smallSpacing: 5
|
||||
property int extraSmallSpacing: 2
|
||||
|
||||
property int iconButtonWidth: 36
|
||||
property int standardPrimaryButtonHeight: 40
|
||||
|
@ -131,6 +132,20 @@ QtObject {
|
|||
|
||||
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) {
|
||||
return size * (1 + Math.min(pixelSize / 100, 1));
|
||||
}
|
||||
|
|
1
theme/black/folder-group.svg
Normal file
1
theme/black/folder-group.svg
Normal 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 |
Loading…
Reference in a new issue