diff --git a/resources.qrc b/resources.qrc index e49b6ec48..3132330e2 100644 --- a/resources.qrc +++ b/resources.qrc @@ -7,17 +7,24 @@ src/gui/PredefinedStatusButton.qml src/gui/BasicComboBox.qml src/gui/ErrorBox.qml + src/gui/filedetails/FileActivityView.qml + src/gui/filedetails/FileDetailsPage.qml + src/gui/filedetails/FileDetailsWindow.qml + src/gui/filedetails/NCInputTextEdit.qml + src/gui/filedetails/NCInputTextField.qml + src/gui/filedetails/NCTabButton.qml + src/gui/filedetails/ShareeDelegate.qml + src/gui/filedetails/ShareDelegate.qml + src/gui/filedetails/ShareeSearchField.qml + src/gui/filedetails/ShareView.qml src/gui/tray/Window.qml src/gui/tray/UserLine.qml src/gui/tray/HeaderButton.qml src/gui/tray/SyncStatus.qml - theme/Style/Style.qml - theme/Style/qmldir src/gui/tray/ActivityActionButton.qml src/gui/tray/ActivityItem.qml src/gui/tray/AutoSizingMenu.qml src/gui/tray/ActivityList.qml - src/gui/tray/FileActivityDialog.qml src/gui/tray/UnifiedSearchInputContainer.qml src/gui/tray/UnifiedSearchResultFetchMoreTrigger.qml src/gui/tray/UnifiedSearchResultItem.qml @@ -39,5 +46,7 @@ src/gui/tray/EditFileLocallyLoadingDialog.qml src/gui/tray/NCBusyIndicator.qml src/gui/tray/NCToolTip.qml + theme/Style/Style.qml + theme/Style/qmldir diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 3a845531e..77d875b85 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -39,9 +39,6 @@ set(client_UI_SRCS ignorelisttablewidget.ui networksettings.ui settingsdialog.ui - sharedialog.ui - sharelinkwidget.ui - shareusergroupwidget.ui shareuserline.ui sslerrordialog.ui addcertificatedialog.ui @@ -139,14 +136,8 @@ set(client_SRCS selectivesyncdialog.cpp settingsdialog.h settingsdialog.cpp - sharedialog.h - sharedialog.cpp - sharelinkwidget.h - sharelinkwidget.cpp sharemanager.h sharemanager.cpp - shareusergroupwidget.h - shareusergroupwidget.cpp profilepagewidget.h profilepagewidget.cpp sharee.h @@ -193,6 +184,14 @@ set(client_SRCS emojimodel.cpp fileactivitylistmodel.h fileactivitylistmodel.cpp + filedetails/filedetails.h + filedetails/filedetails.cpp + filedetails/sharemodel.h + filedetails/sharemodel.cpp + filedetails/shareemodel.h + filedetails/shareemodel.cpp + filedetails/sortedsharemodel.h + filedetails/sortedsharemodel.cpp tray/svgimageprovider.h tray/svgimageprovider.cpp tray/syncstatussummary.h diff --git a/src/gui/application.cpp b/src/gui/application.cpp index fafc33a24..9dd436dc6 100644 --- a/src/gui/application.cpp +++ b/src/gui/application.cpp @@ -32,7 +32,6 @@ #include "sslerrordialog.h" #include "theme.h" #include "clientproxy.h" -#include "sharedialog.h" #include "accountmanager.h" #include "creds/abstractcredentials.h" #include "pushnotifications.h" @@ -379,7 +378,7 @@ Application::Application(int &argc, char **argv) _gui.data(), &ownCloudGui::slotShowShareDialog); connect(FolderMan::instance()->socketApi(), &SocketApi::fileActivityCommandReceived, - Systray::instance(), &Systray::showFileActivityDialog); + _gui.data(), &ownCloudGui::slotShowFileActivityDialog); // startup procedure. connect(&_checkConnectionTimer, &QTimer::timeout, this, &Application::slotCheckConnection); diff --git a/src/gui/fileactivitylistmodel.cpp b/src/gui/fileactivitylistmodel.cpp index 5fadec981..2dde4924f 100644 --- a/src/gui/fileactivitylistmodel.cpp +++ b/src/gui/fileactivitylistmodel.cpp @@ -24,23 +24,46 @@ FileActivityListModel::FileActivityListModel(QObject *parent) : ActivityListModel(nullptr, parent) { setDisplayActions(false); + connect(this, &FileActivityListModel::accountStateChanged, this, &FileActivityListModel::load); } -void FileActivityListModel::load(AccountState *accountState, const int objectId) +QString FileActivityListModel::localPath() const { - Q_ASSERT(accountState); - if (!accountState || currentlyFetching()) { + return _localPath; +} + +void FileActivityListModel::setLocalPath(const QString &localPath) +{ + _localPath = localPath; + Q_EMIT localPathChanged(); + + load(); +} + +void FileActivityListModel::load() +{ + if (!accountState() || _localPath.isEmpty() || currentlyFetching()) { return; } - setAccountState(accountState); - _objectId = objectId; + const auto folder = FolderMan::instance()->folderForPath(_localPath); + + if (!folder) { + qCWarning(lcFileActivityListModel) << "Invalid folder for localPath:" << _localPath << "will not load activity list model."; + return; + } + + const auto folderRelativePath = _localPath.mid(folder->cleanPath().length() + 1); + SyncJournalFileRecord record; + folder->journalDb()->getFileRecord(folderRelativePath, &record); + + _objectId = record.numericFileId().toInt(); slotRefreshActivity(); } void FileActivityListModel::startFetchJob() { - if (!accountState()->isConnected()) { + if (!accountState()->isConnected() || _objectId == -1) { return; } setAndRefreshCurrentlyFetching(true); diff --git a/src/gui/fileactivitylistmodel.h b/src/gui/fileactivitylistmodel.h index 18d8d9830..4134dc0c7 100644 --- a/src/gui/fileactivitylistmodel.h +++ b/src/gui/fileactivitylistmodel.h @@ -22,17 +22,25 @@ namespace OCC { class FileActivityListModel : public ActivityListModel { Q_OBJECT + Q_PROPERTY(QString localPath READ localPath WRITE setLocalPath NOTIFY localPathChanged) public: explicit FileActivityListModel(QObject *parent = nullptr); + QString localPath() const; + +signals: + void localPathChanged(); + public slots: - void load(AccountState *accountState, const int objectId); + void setLocalPath(const QString &localPath); + void load(); protected slots: void startFetchJob() override; private: - int _objectId; + int _objectId = -1; + QString _localPath; }; } diff --git a/src/gui/filedetails/FileActivityView.qml b/src/gui/filedetails/FileActivityView.qml new file mode 100644 index 000000000..47fbe35be --- /dev/null +++ b/src/gui/filedetails/FileActivityView.qml @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2022 by Claudio Cambra + * + * 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.Layouts 1.15 +import QtQuick.Controls 2.15 + +import com.nextcloud.desktopclient 1.0 +import Style 1.0 +import "../tray" + +Item { + id: root + + property string localPath: "" + property var accountState: ({}) + property int horizontalPadding: 0 + property int iconSize: 32 + property alias model: activityModel + + FileActivityListModel { + id: activityModel + localPath: root.localPath + accountState: root.accountState + } + + ActivityList { + anchors.fill: parent + anchors.leftMargin: root.horizontalPadding + anchors.rightMargin: root.horizontalPadding + + iconSize: root.iconSize + isFileActivityList: true + model: root.model + } +} diff --git a/src/gui/filedetails/FileDetailsPage.qml b/src/gui/filedetails/FileDetailsPage.qml new file mode 100644 index 000000000..3e31f102a --- /dev/null +++ b/src/gui/filedetails/FileDetailsPage.qml @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2022 by Claudio Cambra + * + * 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.Layouts 1.15 +import QtQuick.Controls 2.15 + +import com.nextcloud.desktopclient 1.0 +import Style 1.0 + +Page { + id: root + + property var accountState: ({}) + property string localPath: ({}) + + // We want the SwipeView to "spill" over the edges of the window to really + // make it look nice. If we apply page-wide padding, however, the swipe + // contents only go as far as the page contents, clipped by the padding. + // This property reflects the padding we intend to display, but not the real + // padding, which we have to apply selectively to achieve our desired effect. + property int intendedPadding: Style.standardSpacing * 2 + property int iconSize: 32 + + property FileDetails fileDetails: FileDetails { + id: fileDetails + localPath: root.localPath + } + + Connections { + target: Systray + function onShowFileDetailsPage(fileLocalPath, page) { + if(fileLocalPath === root.localPath) { + switch(page) { + case Systray.FileDetailsPage.Activity: + swipeView.currentIndex = fileActivityView.swipeIndex; + break; + case Systray.FileDetailsPage.Sharing: + swipeView.currentIndex = shareView.swipeIndex; + break; + } + } + } + } + + topPadding: intendedPadding + bottomPadding: intendedPadding + + background: Rectangle { + color: Style.backgroundColor + } + + header: ColumnLayout { + spacing: root.intendedPadding + + GridLayout { + id: headerGridLayout + + readonly property bool showFileLockedString: root.fileDetails.lockExpireString !== "" + + Layout.fillWidth: parent + Layout.topMargin: root.topPadding + + columns: 2 + rows: showFileLockedString ? 3 : 2 + + rowSpacing: Style.standardSpacing / 2 + columnSpacing: Style.standardSpacing + + Image { + id: fileIcon + + Layout.rowSpan: headerGridLayout.rows + Layout.preferredWidth: Style.trayListItemIconSize + Layout.leftMargin: root.intendedPadding + Layout.fillHeight: true + + verticalAlignment: Image.AlignVCenter + horizontalAlignment: Image.AlignHCenter + source: root.fileDetails.iconUrl + sourceSize.width: Style.trayListItemIconSize + sourceSize.height: Style.trayListItemIconSize + fillMode: Image.PreserveAspectFit + } + + Label { + id: fileNameLabel + + Layout.fillWidth: true + Layout.rightMargin: root.intendedPadding + + text: root.fileDetails.name + color: Style.ncTextColor + font.bold: true + wrapMode: Text.Wrap + } + + Label { + id: fileDetailsLabel + + Layout.fillWidth: true + Layout.rightMargin: root.intendedPadding + + text: `${root.fileDetails.sizeString} · ${root.fileDetails.lastChangedString}` + color: Style.ncSecondaryTextColor + wrapMode: Text.Wrap + } + + Label { + id: fileLockedLabel + + Layout.fillWidth: true + Layout.rightMargin: root.intendedPadding + + text: root.fileDetails.lockExpireString + color: Style.ncSecondaryTextColor + wrapMode: Text.Wrap + visible: headerGridLayout.showFileLockedString + } + } + + TabBar { + id: viewBar + + Layout.leftMargin: root.intendedPadding + Layout.rightMargin: root.intendedPadding + + padding: 0 + background: Rectangle { + color: Style.backgroundColor + } + + NCTabButton { + svgCustomColorSource: "image://svgimage-custom-color/activity.svg" + text: qsTr("Activity") + checked: swipeView.currentIndex === fileActivityView.swipeIndex + onClicked: swipeView.currentIndex = fileActivityView.swipeIndex + } + + NCTabButton { + svgCustomColorSource: "image://svgimage-custom-color/share.svg" + text: qsTr("Sharing") + checked: swipeView.currentIndex === shareView.swipeIndex + onClicked: swipeView.currentIndex = shareView.swipeIndex + } + } + } + + SwipeView { + id: swipeView + + anchors.fill: parent + clip: true + + FileActivityView { + id: fileActivityView + + property int swipeIndex: SwipeView.index + + accountState: root.accountState + localPath: root.localPath + horizontalPadding: root.intendedPadding + iconSize: root.iconSize + } + + ShareView { + id: shareView + + property int swipeIndex: SwipeView.index + + accountState: root.accountState + localPath: root.localPath + fileDetails: root.fileDetails + horizontalPadding: root.intendedPadding + iconSize: root.iconSize + } + } +} diff --git a/src/gui/filedetails/FileDetailsWindow.qml b/src/gui/filedetails/FileDetailsWindow.qml new file mode 100644 index 000000000..a63607a56 --- /dev/null +++ b/src/gui/filedetails/FileDetailsWindow.qml @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2022 by Claudio Cambra + * + * 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.Window 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 + +import com.nextcloud.desktopclient 1.0 +import Style 1.0 + +ApplicationWindow { + id: root + + property var accountState + property string localPath: "" + + width: 400 + height: 500 + + title: qsTr("File details of %1 · %2").arg(fileDetailsPage.fileDetails.name).arg(Systray.windowTitle) + + FileDetailsPage { + id: fileDetailsPage + anchors.fill: parent + accountState: root.accountState + localPath: root.localPath + } +} diff --git a/src/gui/filedetails/NCInputTextEdit.qml b/src/gui/filedetails/NCInputTextEdit.qml new file mode 100644 index 000000000..85cd39940 --- /dev/null +++ b/src/gui/filedetails/NCInputTextEdit.qml @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2022 by Claudio Cambra + * + * 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 com.nextcloud.desktopclient 1.0 +import Style 1.0 + +TextEdit { + id: root + + property color accentColor: Style.ncBlue + property color secondaryColor: Style.menuBorder + property alias submitButton: submitButton + + clip: true + color: Style.ncTextColor + textMargin: Style.smallSpacing + wrapMode: TextEdit.Wrap + selectByMouse: true + height: Math.max(Style.talkReplyTextFieldPreferredHeight, contentHeight) + + Rectangle { + id: textFieldBorder + anchors.fill: parent + radius: Style.slightlyRoundedButtonRadius + border.width: Style.normalBorderWidth + border.color: root.activeFocus ? root.accentColor : root.secondaryColor + color: Style.backgroundColor + z: -1 + } + + Button { + id: submitButton + + anchors.bottom: root.bottom + anchors.right: root.right + anchors.margins: 1 + + width: height + height: parent.height + + background: Rectangle { + radius: width / 2 + color: textFieldBorder.color + } + + flat: true + icon.source: "image://svgimage-custom-color/confirm.svg" + "/" + root.secondaryColor + icon.color: hovered && enabled ? UserModel.currentUser.accentColor : root.secondaryColor + + enabled: root.text !== "" + + onClicked: root.editingFinished() + } +} + diff --git a/src/gui/filedetails/NCInputTextField.qml b/src/gui/filedetails/NCInputTextField.qml new file mode 100644 index 000000000..36dd42ee7 --- /dev/null +++ b/src/gui/filedetails/NCInputTextField.qml @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2022 by Claudio Cambra + * + * 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 com.nextcloud.desktopclient 1.0 +import Style 1.0 + +TextField { + id: root + + property color accentColor: Style.ncBlue + property color secondaryColor: Style.menuBorder + property alias submitButton: submitButton + + implicitHeight: Style.talkReplyTextFieldPreferredHeight + color: Style.ncTextColor + placeholderTextColor: secondaryColor + + rightPadding: submitButton.width + + selectByMouse: true + + background: Rectangle { + id: textFieldBorder + radius: Style.slightlyRoundedButtonRadius + border.width: Style.normalBorderWidth + border.color: root.activeFocus ? root.accentColor : root.secondaryColor + color: Style.backgroundColor + } + + Button { + id: submitButton + + anchors.top: root.top + anchors.right: root.right + anchors.margins: 1 + + width: height + height: parent.height + + background: null + flat: true + icon.source: "image://svgimage-custom-color/confirm.svg" + "/" + root.secondaryColor + icon.color: hovered && enabled ? UserModel.currentUser.accentColor : root.secondaryColor + + enabled: root.text !== "" + + onClicked: root.accepted() + } +} + diff --git a/src/gui/filedetails/NCTabButton.qml b/src/gui/filedetails/NCTabButton.qml new file mode 100644 index 000000000..6a5a2829a --- /dev/null +++ b/src/gui/filedetails/NCTabButton.qml @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2022 by Claudio Cambra + * + * 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.Window 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 + +import com.nextcloud.desktopclient 1.0 +import Style 1.0 + +TabButton { + id: tabButton + + property string svgCustomColorSource: "" + + padding: 0 + background: Rectangle { + color: tabButton.pressed ? Style.lightHover : Style.backgroundColor + } + + contentItem: ColumnLayout { + id: tabButtonLayout + + property var elementColors: tabButton.checked ? Style.ncTextColor : Style.ncSecondaryTextColor + + // We'd like to just set the height of the Image, but this causes crashing. + // So we use a wrapping Item and use anchors to adjust the size. + Item { + id: iconItem + Layout.fillWidth: true + Layout.fillHeight: true + height: 20 + + Image { + id: iconItemImage + anchors.fill: parent + anchors.margins: tabButton.checked ? 0 : 2 + horizontalAlignment: Image.AlignHCenter + verticalAlignment: Image.AlignVCenter + fillMode: Image.PreserveAspectFit + source: tabButton.svgCustomColorSource + "/" + tabButtonLayout.elementColors + sourceSize.width: 32 + sourceSize.height: 32 + } + } + + Label { + id: tabButtonLabel + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: tabButtonLayout.elementColors + text: tabButton.text + font.bold: tabButton.checked + } + + Rectangle { + FontMetrics { + id: fontMetrics + font.family: tabButtonLabel.font.family + font.pixelSize: tabButtonLabel.font.pixelSize + font.bold: true + } + + property int textWidth: fontMetrics.boundingRect(tabButtonLabel.text).width + + implicitWidth: textWidth + Style.standardSpacing * 2 + implicitHeight: 2 + color: tabButton.checked ? Style.ncBlue : "transparent" + } + } +} diff --git a/src/gui/filedetails/ShareDelegate.qml b/src/gui/filedetails/ShareDelegate.qml new file mode 100644 index 000000000..56cbe9edf --- /dev/null +++ b/src/gui/filedetails/ShareDelegate.qml @@ -0,0 +1,762 @@ +/* + * Copyright (C) 2022 by Claudio Cambra + * + * 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.Window 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 +import QtGraphicalEffects 1.15 + +import com.nextcloud.desktopclient 1.0 +import Style 1.0 +import "../tray" + +GridLayout { + id: root + + signal deleteShare + signal createNewLinkShare + + signal toggleAllowEditing(bool enable) + signal toggleAllowResharing(bool enable) + signal togglePasswordProtect(bool enable) + signal toggleExpirationDate(bool enable) + signal toggleNoteToRecipient(bool enable) + + signal setLinkShareLabel(string label) + signal setExpireDate(var milliseconds) // Since QML ints are only 32 bits, use a variant + signal setPassword(string password) + signal setNote(string note) + + anchors.left: parent.left + anchors.right: parent.right + + columns: 3 + rows: linkDetailLabel.visible ? 1 : 2 + + columnSpacing: Style.standardSpacing / 2 + rowSpacing: Style.standardSpacing / 2 + + property int iconSize: 32 + + property var share: model.share ?? ({}) + + property string iconUrl: model.iconUrl ?? "" + property string avatarUrl: model.avatarUrl ?? "" + property string text: model.display ?? "" + property string detailText: model.detailText ?? "" + property string link: model.link ?? "" + property string note: model.note ?? "" + property string password: model.password ?? "" + property string passwordPlaceholder: "●●●●●●●●●●" + + property var expireDate: model.expireDate // Don't use int as we are limited + property var maximumExpireDate: model.enforcedMaximumExpireDate + + property string linkShareLabel: model.linkShareLabel ?? "" + + property bool editingAllowed: model.editingAllowed + property bool noteEnabled: model.noteEnabled + property bool expireDateEnabled: model.expireDateEnabled + property bool expireDateEnforced: model.expireDateEnforced + property bool passwordProtectEnabled: model.passwordProtectEnabled + property bool passwordEnforced: model.passwordEnforced + + property bool isLinkShare: model.shareType === ShareModel.ShareTypeLink + property bool isPlaceholderLinkShare: model.shareType === ShareModel.ShareTypePlaceholderLink + + property bool canCreateLinkShares: true + + property bool waitingForEditingAllowedChange: false + property bool waitingForNoteEnabledChange: false + property bool waitingForExpireDateEnabledChange: false + property bool waitingForPasswordProtectEnabledChange: false + property bool waitingForExpireDateChange: false + property bool waitingForLinkShareLabelChange: false + property bool waitingForPasswordChange: false + property bool waitingForNoteChange: false + + function resetNoteField() { + noteTextEdit.text = note; + waitingForNoteChange = false; + } + + function resetLinkShareLabelField() { + linkShareLabelTextField.text = linkShareLabel; + waitingForLinkShareLabelChange = false; + } + + function resetPasswordField() { + passwordTextField.text = password !== "" ? password : passwordPlaceholder; + waitingForPasswordChange = false; + } + + function resetExpireDateField() { + // Expire date changing is handled by the expireDateSpinBox + waitingForExpireDateChange = false; + } + + function resetEditingAllowedField() { + editingAllowedMenuItem.checked = editingAllowed; + waitingForEditingAllowedChange = false; + } + + function resetNoteEnabledField() { + noteEnabledMenuItem.checked = noteEnabled; + waitingForNoteEnabledChange = false; + } + + function resetExpireDateEnabledField() { + expireDateEnabledMenuItem.checked = expireDateEnabled; + waitingForExpireDateEnabledChange = false; + } + + function resetPasswordProtectEnabledField() { + passwordProtectEnabledMenuItem.checked = passwordProtectEnabled; + waitingForPasswordProtectEnabledChange = false; + } + + function resetMenu() { + moreMenu.close(); + + resetNoteField(); + resetPasswordField(); + resetLinkShareLabelField(); + resetExpireDateField(); + + resetEditingAllowedField(); + resetNoteEnabledField(); + resetExpireDateEnabledField(); + resetPasswordProtectEnabledField(); + } + + // Renaming a link share can lead to the model being reshuffled. + // This can cause a situation where this delegate is assigned to + // a new row and it doesn't have its properties signalled as + // changed by the model, leading to bugs. We therefore reset all + // the fields here when we detect the share has been changed + onShareChanged: resetMenu() + + // Reset value after property binding broken by user interaction + onNoteChanged: resetNoteField() + onPasswordChanged: resetPasswordField() + onLinkShareLabelChanged: resetLinkShareLabelField() + onExpireDateChanged: resetExpireDateField() + + onEditingAllowedChanged: resetEditingAllowedField() + onNoteEnabledChanged: resetNoteEnabledField() + onExpireDateEnabledChanged: resetExpireDateEnabledField() + onPasswordProtectEnabledChanged: resetPasswordProtectEnabledField() + + Item { + id: imageItem + + property bool isAvatar: root.avatarUrl !== "" + + Layout.row: 0 + Layout.column: 0 + Layout.rowSpan: root.rows + Layout.preferredWidth: root.iconSize + Layout.preferredHeight: root.iconSize + + Rectangle { + id: backgroundOrMask + anchors.fill: parent + radius: width / 2 + color: Style.ncBlue + visible: !imageItem.isAvatar + } + + Image { + id: shareIconOrThumbnail + + anchors.centerIn: parent + + verticalAlignment: Image.AlignVCenter + horizontalAlignment: Image.AlignHCenter + fillMode: Image.PreserveAspectFit + + source: imageItem.isAvatar ? root.avatarUrl : root.iconUrl + "/white" + sourceSize.width: imageItem.isAvatar ? root.iconSize : root.iconSize / 2 + sourceSize.height: imageItem.isAvatar ? root.iconSize : root.iconSize / 2 + + visible: !imageItem.isAvatar + } + + OpacityMask { + anchors.fill: parent + source: shareIconOrThumbnail + maskSource: backgroundOrMask + visible: imageItem.isAvatar + } + } + + Label { + id: shareTypeLabel + + Layout.fillWidth: true + Layout.alignment: linkDetailLabel.visible ? Qt.AlignBottom : Qt.AlignVCenter + Layout.row: 0 + Layout.column: 1 + Layout.rowSpan: root.rows + + text: root.text + color: Style.ncTextColor + elide: Text.ElideRight + } + + Label { + id: linkDetailLabel + + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + Layout.row: 1 + Layout.column: 1 + + text: root.detailText + color: Style.ncSecondaryTextColor + elide: Text.ElideRight + visible: text !== "" + } + + RowLayout { + Layout.row: 0 + Layout.column: 2 + Layout.rowSpan: root.rows + Layout.fillHeight: true + spacing: 0 + + Button { + id: createLinkButton + + Layout.alignment: Qt.AlignCenter + Layout.preferredWidth: icon.width + (Style.standardSpacing * 2) + + flat: true + display: AbstractButton.IconOnly + icon.color: Style.ncTextColor + icon.source: "qrc:///client/theme/add.svg" + icon.width: 16 + icon.height: 16 + + visible: root.isPlaceholderLinkShare && root.canCreateLinkShares + enabled: visible + + onClicked: root.createNewLinkShare() + } + + Button { + id: copyLinkButton + + Layout.alignment: Qt.AlignCenter + Layout.preferredWidth: icon.width + (Style.standardSpacing * 2) + + flat: true + display: AbstractButton.IconOnly + icon.color: Style.ncTextColor + icon.source: "qrc:///client/theme/copy.svg" + icon.width: 16 + icon.height: 16 + + visible: root.isLinkShare + enabled: visible + + onClicked: { + clipboardHelper.text = root.link; + clipboardHelper.selectAll(); + clipboardHelper.copy(); + clipboardHelper.clear(); + } + + TextEdit { id: clipboardHelper; visible: false} + } + + Button { + id: moreButton + + Layout.alignment: Qt.AlignCenter + Layout.preferredWidth: icon.width + (Style.standardSpacing * 2) + + flat: true + display: AbstractButton.IconOnly + icon.color: Style.ncTextColor + icon.source: "qrc:///client/theme/more.svg" + icon.width: 16 + icon.height: 16 + + visible: !root.isPlaceholderLinkShare + enabled: visible + + onClicked: moreMenu.popup() + + Menu { + id: moreMenu + + property int rowIconWidth: 16 + property int indicatorItemWidth: 20 + property int indicatorSpacing: Style.standardSpacing + property int itemPadding: Style.smallSpacing + + padding: Style.smallSpacing + // TODO: Rather than setting all these palette colours manually, + // create a custom style and do it for all components globally + palette { + text: Style.ncTextColor + windowText: Style.ncTextColor + buttonText: Style.ncTextColor + light: Style.lightHover + midlight: Style.lightHover + mid: Style.ncSecondaryTextColor + dark: Style.menuBorder + button: Style.menuBorder + window: Style.backgroundColor + base: Style.backgroundColor + } + + RowLayout { + anchors.left: parent.left + anchors.leftMargin: moreMenu.itemPadding + anchors.right: parent.right + anchors.rightMargin: moreMenu.itemPadding + height: visible ? implicitHeight : 0 + spacing: moreMenu.indicatorSpacing + + visible: root.isLinkShare + + Image { + Layout.preferredWidth: moreMenu.indicatorItemWidth + Layout.fillHeight: true + + verticalAlignment: Image.AlignVCenter + horizontalAlignment: Image.AlignHCenter + fillMode: Image.Pad + + source: "image://svgimage-custom-color/edit.svg/" + Style.menuBorder + sourceSize.width: moreMenu.rowIconWidth + sourceSize.height: moreMenu.rowIconWidth + } + + NCInputTextField { + id: linkShareLabelTextField + + Layout.fillWidth: true + height: visible ? implicitHeight : 0 + + text: root.linkShareLabel + placeholderText: qsTr("Share label") + + enabled: root.isLinkShare && + !root.waitingForLinkShareLabelChange + + onAccepted: if(text !== root.linkShareLabel) { + root.setLinkShareLabel(text); + root.waitingForLinkShareLabelChange = true; + } + + NCBusyIndicator { + anchors.fill: parent + visible: root.waitingForLinkShareLabelChange + running: visible + z: 1 + } + } + } + + // On these checkables, the clicked() signal is called after + // the check state changes. + CheckBox { + id: editingAllowedMenuItem + + spacing: moreMenu.indicatorSpacing + padding: moreMenu.itemPadding + indicator.width: moreMenu.indicatorItemWidth + indicator.height: moreMenu.indicatorItemWidth + + checkable: true + checked: root.editingAllowed + text: qsTr("Allow editing") + enabled: !root.waitingForEditingAllowedChange + + onClicked: { + root.toggleAllowEditing(checked); + root.waitingForEditingAllowedChange = true; + } + + NCBusyIndicator { + anchors.fill: parent + visible: root.waitingForEditingAllowedChange + running: visible + z: 1 + } + } + + CheckBox { + id: passwordProtectEnabledMenuItem + + spacing: moreMenu.indicatorSpacing + padding: moreMenu.itemPadding + indicator.width: moreMenu.indicatorItemWidth + indicator.height: moreMenu.indicatorItemWidth + + checkable: true + checked: root.passwordProtectEnabled + text: qsTr("Password protect") + enabled: !root.waitingForPasswordProtectEnabledChange && !root.passwordEnforced + + onClicked: { + root.togglePasswordProtect(checked); + root.waitingForPasswordProtectEnabledChange = true; + } + + NCBusyIndicator { + anchors.fill: parent + visible: root.waitingForPasswordProtectEnabledChange + running: visible + z: 1 + } + } + + RowLayout { + anchors.left: parent.left + anchors.leftMargin: moreMenu.itemPadding + anchors.right: parent.right + anchors.rightMargin: moreMenu.itemPadding + height: visible ? implicitHeight : 0 + spacing: moreMenu.indicatorSpacing + + visible: root.passwordProtectEnabled + + Image { + Layout.preferredWidth: moreMenu.indicatorItemWidth + Layout.fillHeight: true + + verticalAlignment: Image.AlignVCenter + horizontalAlignment: Image.AlignHCenter + fillMode: Image.Pad + + source: "image://svgimage-custom-color/lock-https.svg/" + Style.menuBorder + sourceSize.width: moreMenu.rowIconWidth + sourceSize.height: moreMenu.rowIconWidth + } + + NCInputTextField { + id: passwordTextField + + Layout.fillWidth: true + height: visible ? implicitHeight : 0 + + text: root.password !== "" ? root.password : root.passwordPlaceholder + enabled: root.passwordProtectEnabled && + !root.waitingForPasswordChange && + !root.waitingForPasswordProtectEnabledChange + + onAccepted: if(text !== root.password && text !== root.passwordPlaceholder) { + root.setPassword(text); + root.waitingForPasswordChange = true; + } + + NCBusyIndicator { + anchors.fill: parent + visible: root.waitingForPasswordChange || + root.waitingForPasswordProtectEnabledChange + running: visible + z: 1 + } + } + } + + CheckBox { + id: expireDateEnabledMenuItem + + spacing: moreMenu.indicatorSpacing + padding: moreMenu.itemPadding + indicator.width: moreMenu.indicatorItemWidth + indicator.height: moreMenu.indicatorItemWidth + + checkable: true + checked: root.expireDateEnabled + text: qsTr("Set expiration date") + enabled: !root.waitingForExpireDateEnabledChange && !root.expireDateEnforced + + onClicked: { + root.toggleExpirationDate(checked); + root.waitingForExpireDateEnabledChange = true; + } + + NCBusyIndicator { + anchors.fill: parent + visible: root.waitingForExpireDateEnabledChange + running: visible + z: 1 + } + } + + RowLayout { + anchors.left: parent.left + anchors.leftMargin: moreMenu.itemPadding + anchors.right: parent.right + anchors.rightMargin: moreMenu.itemPadding + height: visible ? implicitHeight : 0 + spacing: moreMenu.indicatorSpacing + + visible: root.expireDateEnabled + + Image { + Layout.preferredWidth: moreMenu.indicatorItemWidth + Layout.fillHeight: true + + verticalAlignment: Image.AlignVCenter + horizontalAlignment: Image.AlignHCenter + fillMode: Image.Pad + + source: "image://svgimage-custom-color/calendar.svg/" + Style.menuBorder + sourceSize.width: moreMenu.rowIconWidth + sourceSize.height: moreMenu.rowIconWidth + } + + // QML dates are essentially JavaScript dates, which makes them very finicky and unreliable. + // Instead, we exclusively deal with msecs from epoch time to make things less painful when editing. + // We only use the QML Date when showing the nice string to the user. + SpinBox { + id: expireDateSpinBox + + // Work arounds the limitations of QML's 32 bit integer when handling msecs from epoch + // Instead, we handle everything as days since epoch + readonly property int dayInMSecs: 24 * 60 * 60 * 1000 + readonly property int expireDateReduced: Math.floor(root.expireDate / dayInMSecs) + // Reset the model data after binding broken on user interact + onExpireDateReducedChanged: value = expireDateReduced + + // We can't use JS's convenient Infinity or Number.MAX_VALUE as + // JS Number type is 64 bits, whereas QML's int type is only 32 bits + readonly property IntValidator intValidator: IntValidator {} + readonly property int maximumExpireDateReduced: root.expireDateEnforced ? + Math.floor(root.maximumExpireDate / dayInMSecs) : + intValidator.top + readonly property int minimumExpireDateReduced: { + const currentDate = new Date(); + const minDateUTC = new Date(Date.UTC(currentDate.getFullYear(), + currentDate.getMonth(), + currentDate.getDate() + 1)); + return Math.floor(minDateUTC / dayInMSecs) // Start of day at 00:00:0000 UTC + } + + // Taken from Kalendar 22.08 + // https://invent.kde.org/pim/kalendar/-/blob/release/22.08/src/contents/ui/KalendarUtils/dateutils.js + function parseDateString(dateString) { + function defaultParse() { + return Date.fromLocaleDateString(Qt.locale(), dateString, Locale.NarrowFormat); + } + + const dateStringDelimiterMatches = dateString.match(/\D/); + if(dateStringDelimiterMatches.length === 0) { + // Let the date method figure out this weirdness + return defaultParse(); + } + + const dateStringDelimiter = dateStringDelimiterMatches[0]; + + const localisedDateFormatSplit = Qt.locale().dateFormat(Locale.NarrowFormat).split(dateStringDelimiter); + const localisedDateDayPosition = localisedDateFormatSplit.findIndex((x) => /d/gi.test(x)); + const localisedDateMonthPosition = localisedDateFormatSplit.findIndex((x) => /m/gi.test(x)); + const localisedDateYearPosition = localisedDateFormatSplit.findIndex((x) => /y/gi.test(x)); + + let splitDateString = dateString.split(dateStringDelimiter); + let userProvidedYear = splitDateString[localisedDateYearPosition] + + const dateNow = new Date(); + const stringifiedCurrentYear = dateNow.getFullYear().toString(); + + // If we have any input weirdness, or if we have a fully-written year + // (e.g. 2022 instead of 22) then use default parse + if(splitDateString.length === 0 || + splitDateString.length > 3 || + userProvidedYear.length >= stringifiedCurrentYear.length) { + return defaultParse(); + } + + let fullyWrittenYear = userProvidedYear.split(""); + const digitsToAdd = stringifiedCurrentYear.length - fullyWrittenYear.length; + for(let i = 0; i < digitsToAdd; i++) { + fullyWrittenYear.splice(i, 0, stringifiedCurrentYear[i]) + } + fullyWrittenYear = fullyWrittenYear.join(""); + + const fixedYearNum = Number(fullyWrittenYear); + const monthIndexNum = Number(splitDateString[localisedDateMonthPosition]) - 1; + const dayNum = Number(splitDateString[localisedDateDayPosition]); + + // Modification: return date in UTC + return new Date(Date.UTC(fixedYearNum, monthIndexNum, dayNum)); + } + + Layout.fillWidth: true + height: visible ? implicitHeight : 0 + + // We want all the internal benefits of the spinbox but don't actually want the + // buttons, so set an empty item as a dummy + up.indicator: Item {} + down.indicator: Item {} + + background: Rectangle { + radius: Style.slightlyRoundedButtonRadius + border.width: Style.normalBorderWidth + border.color: expireDateSpinBox.activeFocus ? Style.ncBlue : Style.menuBorder + color: Style.backgroundColor + } + + value: expireDateReduced + from: minimumExpireDateReduced + to: maximumExpireDateReduced + + textFromValue: (value, locale) => { + const dateFromValue = new Date(value * dayInMSecs); + return dateFromValue.toLocaleDateString(Qt.locale(), Locale.NarrowFormat); + } + valueFromText: (text, locale) => { + const dateFromText = parseDateString(text); + return Math.floor(dateFromText.getTime() / dayInMSecs); + } + + editable: true + inputMethodHints: Qt.ImhDate | Qt.ImhFormattedNumbersOnly + + enabled: root.expireDateEnabled && + !root.waitingForExpireDateChange && + !root.waitingForExpireDateEnabledChange + + onValueModified: { + root.setExpireDate(value * dayInMSecs); + root.waitingForExpireDateChange = true; + } + + NCBusyIndicator { + anchors.fill: parent + visible: root.waitingForExpireDateEnabledChange || + root.waitingForExpireDateChange + running: visible + z: 1 + } + } + } + + CheckBox { + id: noteEnabledMenuItem + + spacing: moreMenu.indicatorSpacing + padding: moreMenu.itemPadding + indicator.width: moreMenu.indicatorItemWidth + indicator.height: moreMenu.indicatorItemWidth + + checkable: true + checked: root.noteEnabled + text: qsTr("Note to recipient") + enabled: !root.waitingForNoteEnabledChange + + onClicked: { + root.toggleNoteToRecipient(checked); + root.waitingForNoteEnabledChange = true; + } + + NCBusyIndicator { + anchors.fill: parent + visible: root.waitingForNoteEnabledChange + running: visible + z: 1 + } + } + + RowLayout { + anchors.left: parent.left + anchors.leftMargin: moreMenu.itemPadding + anchors.right: parent.right + anchors.rightMargin: moreMenu.itemPadding + height: visible ? implicitHeight : 0 + spacing: moreMenu.indicatorSpacing + + visible: root.noteEnabled + + Image { + Layout.preferredWidth: moreMenu.indicatorItemWidth + Layout.fillHeight: true + + verticalAlignment: Image.AlignVCenter + horizontalAlignment: Image.AlignHCenter + fillMode: Image.Pad + + source: "image://svgimage-custom-color/edit.svg/" + Style.menuBorder + sourceSize.width: moreMenu.rowIconWidth + sourceSize.height: moreMenu.rowIconWidth + } + + NCInputTextEdit { + id: noteTextEdit + + Layout.fillWidth: true + height: visible ? Math.max(Style.talkReplyTextFieldPreferredHeight, contentHeight) : 0 + submitButton.height: Math.min(Style.talkReplyTextFieldPreferredHeight, height - 2) + + text: root.note + enabled: root.noteEnabled && + !root.waitingForNoteChange && + !root.waitingForNoteEnabledChange + + onEditingFinished: if(text !== root.note) { + root.setNote(text); + root.waitingForNoteChange = true; + } + + NCBusyIndicator { + anchors.fill: parent + visible: root.waitingForNoteChange || + root.waitingForNoteEnabledChange + running: visible + z: 1 + } + } + } + + MenuItem { + spacing: moreMenu.indicatorSpacing + padding: moreMenu.itemPadding + + icon.width: moreMenu.indicatorItemWidth + icon.height: moreMenu.indicatorItemWidth + icon.color: Style.ncTextColor + icon.source: "qrc:///client/theme/close.svg" + text: qsTr("Unshare") + + onTriggered: root.deleteShare() + } + + MenuItem { + height: visible ? implicitHeight : 0 + spacing: moreMenu.indicatorSpacing + padding: moreMenu.itemPadding + + icon.width: moreMenu.indicatorItemWidth + icon.height: moreMenu.indicatorItemWidth + icon.color: Style.ncTextColor + icon.source: "qrc:///client/theme/add.svg" + text: qsTr("Add another link") + + visible: root.isLinkShare && root.canCreateLinkShares + enabled: visible + + onTriggered: root.createNewLinkShare() + } + } + } + } +} diff --git a/src/gui/filedetails/ShareView.qml b/src/gui/filedetails/ShareView.qml new file mode 100644 index 000000000..eb8da6f9e --- /dev/null +++ b/src/gui/filedetails/ShareView.qml @@ -0,0 +1,309 @@ +/* + * Copyright (C) 2022 by Claudio Cambra + * + * 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.Window 2.15 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.15 + +import com.nextcloud.desktopclient 1.0 +import Style 1.0 +import "../tray" +import "../" + +ColumnLayout { + id: root + + property string localPath: "" + property var accountState: ({}) + property FileDetails fileDetails: FileDetails {} + property int horizontalPadding: 0 + property int iconSize: 32 + + readonly property bool sharingPossible: shareModel && shareModel.canShare && shareModel.sharingEnabled + readonly property bool userGroupSharingPossible: sharingPossible && shareModel.userGroupSharingEnabled + readonly property bool publicLinkSharingPossible: sharingPossible && shareModel.publicLinkSharesEnabled + + readonly property bool loading: sharingPossible && (!shareModel || + shareModel.fetchOngoing || + !shareModel.hasInitialShareFetchCompleted || + waitingForSharesToChange) + property bool waitingForSharesToChange: true // Gets changed to false when listview count changes + property bool stopWaitingForSharesToChangeOnPasswordError: false + + readonly property ShareModel shareModel: ShareModel { + accountState: root.accountState + localPath: root.localPath + + onSharesChanged: root.waitingForSharesToChange = false + + onServerError: { + if(errorBox.text === "") { + errorBox.text = message; + } else { + errorBox.text += "\n\n" + message + } + + errorBox.visible = true; + } + + onPasswordSetError: if(root.stopWaitingForSharesToChangeOnPasswordError) { + root.waitingForSharesToChange = false; + root.stopWaitingForSharesToChangeOnPasswordError = false; + } + + onRequestPasswordForLinkShare: shareRequiresPasswordDialog.open() + onRequestPasswordForEmailSharee: { + shareRequiresPasswordDialog.sharee = sharee; + shareRequiresPasswordDialog.open(); + } + } + + Dialog { + id: shareRequiresPasswordDialog + + property var sharee + + function discardDialog() { + sharee = undefined; + root.waitingForSharesToChange = false; + close(); + } + + anchors.centerIn: parent + width: parent.width * 0.8 + + title: qsTr("Password required for new share") + standardButtons: Dialog.Ok | Dialog.Cancel + modal: true + closePolicy: Popup.NoAutoClose + + // TODO: Rather than setting all these palette colours manually, + // create a custom style and do it for all components globally + palette { + text: Style.ncTextColor + windowText: Style.ncTextColor + buttonText: Style.ncTextColor + light: Style.lightHover + midlight: Style.lightHover + mid: Style.ncSecondaryTextColor + dark: Style.menuBorder + button: Style.menuBorder + window: Style.backgroundColor + base: Style.backgroundColor + } + + visible: false + + onAccepted: { + if(sharee) { + root.shareModel.createNewUserGroupShareWithPasswordFromQml(sharee, dialogPasswordField.text); + sharee = undefined; + } else { + root.shareModel.createNewLinkShareWithPassword(dialogPasswordField.text); + } + + root.stopWaitingForSharesToChangeOnPasswordError = true; + dialogPasswordField.text = ""; + } + onDiscarded: discardDialog() + onRejected: discardDialog() + + NCInputTextField { + id: dialogPasswordField + + anchors.left: parent.left + anchors.right: parent.right + + placeholderText: qsTr("Share password") + onAccepted: shareRequiresPasswordDialog.accept() + } + } + + ErrorBox { + id: errorBox + + Layout.fillWidth: true + Layout.leftMargin: root.horizontalPadding + Layout.rightMargin: root.horizontalPadding + + showCloseButton: true + visible: false + + onCloseButtonClicked: { + text = ""; + visible = false; + } + } + + ShareeSearchField { + Layout.fillWidth: true + Layout.leftMargin: root.horizontalPadding + Layout.rightMargin: root.horizontalPadding + + visible: root.userGroupSharingPossible + enabled: visible && !root.loading + + accountState: root.accountState + shareItemIsFolder: root.fileDetails && root.fileDetails.isFolder + + onShareeSelected: { + root.waitingForSharesToChange = true; + root.shareModel.createNewUserGroupShareFromQml(sharee) + } + } + + Loader { + id: sharesViewLoader + + Layout.fillWidth: true + Layout.fillHeight: true + Layout.leftMargin: root.horizontalPadding + Layout.rightMargin: root.horizontalPadding + + active: root.sharingPossible + + sourceComponent: ScrollView { + id: scrollView + anchors.fill: parent + + contentWidth: availableWidth + clip: true + enabled: root.sharingPossible + + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + + ListView { + id: shareLinksListView + + enabled: !root.loading + model: SortedShareModel { + shareModel: root.shareModel + } + + delegate: ShareDelegate { + id: shareDelegate + + Connections { + target: root.shareModel + // Though we try to handle this internally by listening to onPasswordChanged, + // with passwords we will get the same value from the model data when a + // password set has failed, meaning we won't be able to easily tell when we + // have had a response from the server in QML. So we listen to this signal + // directly from the model and do the reset of the password field manually. + function onPasswordSetError(shareId) { + if(shareId !== model.shareId) { + return; + } + shareDelegate.resetPasswordField(); + } + + function onServerError() { + if(shareId !== model.shareId) { + return; + } + + shareDelegate.resetMenu(); + } + } + + iconSize: root.iconSize + canCreateLinkShares: root.publicLinkSharingPossible + + onCreateNewLinkShare: { + root.waitingForSharesToChange = true; + shareModel.createNewLinkShare(); + } + onDeleteShare: { + root.waitingForSharesToChange = true; + shareModel.deleteShareFromQml(model.share); + } + + onToggleAllowEditing: shareModel.toggleShareAllowEditingFromQml(model.share, enable) + onToggleAllowResharing: shareModel.toggleShareAllowResharingFromQml(model.share, enable) + onTogglePasswordProtect: shareModel.toggleSharePasswordProtectFromQml(model.share, enable) + onToggleExpirationDate: shareModel.toggleShareExpirationDateFromQml(model.share, enable) + onToggleNoteToRecipient: shareModel.toggleShareNoteToRecipientFromQml(model.share, enable) + + onSetLinkShareLabel: shareModel.setLinkShareLabelFromQml(model.share, label) + onSetExpireDate: shareModel.setShareExpireDateFromQml(model.share, milliseconds) + onSetPassword: shareModel.setSharePasswordFromQml(model.share, password) + onSetNote: shareModel.setShareNoteFromQml(model.share, note) + } + + Loader { + id: sharesFetchingLoader + anchors.fill: parent + active: root.loading + z: Infinity + + sourceComponent: Rectangle { + color: Style.backgroundColor + opacity: 0.5 + + NCBusyIndicator { + anchors.centerIn: parent + color: Style.ncSecondaryTextColor + } + } + } + } + } + } + + Loader { + id: sharingNotPossibleView + + Layout.fillWidth: true + Layout.fillHeight: true + Layout.leftMargin: root.horizontalPadding + Layout.rightMargin: root.horizontalPadding + + active: !root.sharingPossible + + sourceComponent: Column { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + Label { + id: sharingDisabledLabel + width: parent.width + text: qsTr("Sharing is disabled") + color: Style.ncSecondaryTextColor + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + Label { + width: parent.width + text: qsTr("This item cannot be shared.") + color: Style.ncSecondaryTextColor + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + visible: !root.shareModel.canShare + } + Label { + width: parent.width + text: qsTr("Sharing is disabled.") + color: Style.ncSecondaryTextColor + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + visible: !root.shareModel.sharingEnabled + } + } + } +} diff --git a/src/gui/filedetails/ShareeDelegate.qml b/src/gui/filedetails/ShareeDelegate.qml new file mode 100644 index 000000000..a9128cb4c --- /dev/null +++ b/src/gui/filedetails/ShareeDelegate.qml @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2022 by Claudio Cambra + * + * 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.Window 2.15 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.15 + +import com.nextcloud.desktopclient 1.0 +import Style 1.0 + +ItemDelegate { + id: root + + text: model.display +} diff --git a/src/gui/filedetails/ShareeSearchField.qml b/src/gui/filedetails/ShareeSearchField.qml new file mode 100644 index 000000000..eedf20daa --- /dev/null +++ b/src/gui/filedetails/ShareeSearchField.qml @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2022 by Claudio Cambra + * + * 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.Window 2.15 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.15 + +import com.nextcloud.desktopclient 1.0 +import Style 1.0 +import "../tray" + +TextField { + id: root + + signal shareeSelected(var sharee) + + property var accountState: ({}) + property bool shareItemIsFolder: false + property ShareeModel shareeModel: ShareeModel { + accountState: root.accountState + shareItemIsFolder: root.shareItemIsFolder + searchString: root.text + } + + readonly property int horizontalPaddingOffset: Style.trayHorizontalMargin + readonly property color placeholderColor: Style.menuBorder + readonly property double iconsScaleFactor: 0.6 + + function triggerSuggestionsVisibility() { + shareeListView.count > 0 && text !== "" ? suggestionsPopup.open() : suggestionsPopup.close(); + } + + placeholderText: qsTr("Search for users or groups…") + placeholderTextColor: placeholderColor + color: Style.ncTextColor + enabled: !shareeModel.fetchOngoing + + onActiveFocusChanged: triggerSuggestionsVisibility() + onTextChanged: triggerSuggestionsVisibility() + Keys.onPressed: { + if(suggestionsPopup.visible) { + switch(event.key) { + case Qt.Key_Escape: + suggestionsPopup.close(); + shareeListView.currentIndex = -1; + event.accepted = true; + break; + + case Qt.Key_Up: + shareeListView.decrementCurrentIndex(); + event.accepted = true; + break; + + case Qt.Key_Down: + shareeListView.incrementCurrentIndex(); + event.accepted = true; + break; + + case Qt.Key_Enter: + case Qt.Key_Return: + if(shareeListView.currentIndex > -1) { + shareeListView.itemAtIndex(shareeListView.currentIndex).selectSharee(); + event.accepted = true; + break; + } + } + } else { + switch(event.key) { + case Qt.Key_Down: + triggerSuggestionsVisibility(); + event.accepted = true; + break; + } + } + } + + leftPadding: searchIcon.width + searchIcon.anchors.leftMargin + horizontalPaddingOffset + rightPadding: clearTextButton.width + clearTextButton.anchors.rightMargin + horizontalPaddingOffset + + background: Rectangle { + radius: 5 + border.color: parent.activeFocus ? UserModel.currentUser.accentColor : Style.menuBorder + border.width: 1 + color: Style.backgroundColor + } + + Image { + id: searchIcon + anchors { + top: parent.top + left: parent.left + bottom: parent.bottom + margins: 4 + } + + width: height + + smooth: true + antialiasing: true + mipmap: true + fillMode: Image.PreserveAspectFit + horizontalAlignment: Image.AlignLeft + + source: "image://svgimage-custom-color/search.svg" + "/" + root.placeholderColor + sourceSize: Qt.size(parent.height * root.iconsScaleFactor, parent.height * root.iconsScaleFactor) + + visible: !root.shareeModel.fetchOngoing + } + + NCBusyIndicator { + id: busyIndicator + + anchors { + top: parent.top + left: parent.left + bottom: parent.bottom + } + + width: height + color: root.placeholderColor + visible: root.shareeModel.fetchOngoing + running: visible + } + + Image { + id: clearTextButton + + anchors { + top: parent.top + right: parent.right + bottom: parent.bottom + margins: 4 + } + + width: height + + smooth: true + antialiasing: true + mipmap: true + fillMode: Image.PreserveAspectFit + + source: "image://svgimage-custom-color/clear.svg" + "/" + root.placeholderColor + sourceSize: Qt.size(parent.height * root.iconsScaleFactor, parent.height * root.iconsScaleFactor) + + visible: root.text + + MouseArea { + id: clearTextButtonMouseArea + anchors.fill: parent + onClicked: root.clear() + } + } + + Popup { + id: suggestionsPopup + + width: root.width + height: 100 + y: root.height + + // TODO: Rather than setting all these palette colours manually, + // create a custom style and do it for all components globally + palette { + text: Style.ncTextColor + windowText: Style.ncTextColor + buttonText: Style.ncTextColor + light: Style.lightHover + midlight: Style.lightHover + mid: Style.ncSecondaryTextColor + dark: Style.menuBorder + button: Style.menuBorder + window: Style.backgroundColor + base: Style.backgroundColor + } + + contentItem: ScrollView { + id: suggestionsScrollView + + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + + ListView { + id: shareeListView + + spacing: 0 + currentIndex: -1 + interactive: true + + highlight: Rectangle { + width: shareeListView.currentItem.width + height: shareeListView.currentItem.height + color: Style.lightHover + } + highlightFollowsCurrentItem: true + highlightMoveDuration: 0 + highlightResizeDuration: 0 + highlightRangeMode: ListView.ApplyRange + preferredHighlightBegin: 0 + preferredHighlightEnd: suggestionsScrollView.height + + onCountChanged: root.triggerSuggestionsVisibility() + + model: root.shareeModel + delegate: ShareeDelegate { + anchors.left: parent.left + anchors.right: parent.right + + function selectSharee() { + root.shareeSelected(model.sharee); + suggestionsPopup.close(); + + root.clear(); + } + + onHoveredChanged: if (hovered) { + // When we set the currentIndex the list view will scroll... + // unless we tamper with the preferred highlight points to stop this. + const savedPreferredHighlightBegin = shareeListView.preferredHighlightBegin; + const savedPreferredHighlightEnd = shareeListView.preferredHighlightEnd; + // Set overkill values to make sure no scroll happens when we hover with mouse + shareeListView.preferredHighlightBegin = -suggestionsScrollView.height; + shareeListView.preferredHighlightEnd = suggestionsScrollView.height * 2; + + shareeListView.currentIndex = index + + // Reset original values so keyboard navigation makes list view scroll + shareeListView.preferredHighlightBegin = savedPreferredHighlightBegin; + shareeListView.preferredHighlightEnd = savedPreferredHighlightEnd; + } + onClicked: selectSharee() + } + } + } + } +} diff --git a/src/gui/filedetails/filedetails.cpp b/src/gui/filedetails/filedetails.cpp new file mode 100644 index 000000000..b7fa88a18 --- /dev/null +++ b/src/gui/filedetails/filedetails.cpp @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2022 by Claudio Cambra + * + * 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. + */ + +#include + +#include "filedetails.h" +#include "folderman.h" + +namespace OCC { + +FileDetails::FileDetails(QObject *parent) + : QObject(parent) +{ + _filelockStateUpdateTimer.setInterval(6000); + _filelockStateUpdateTimer.setSingleShot(false); + connect(&_filelockStateUpdateTimer, &QTimer::timeout, this, &FileDetails::updateLockExpireString); +} + +void FileDetails::refreshFileDetails() +{ + _fileInfo.refresh(); + Q_EMIT fileChanged(); +} + +QString FileDetails::localPath() const +{ + return _localPath; +} + +void FileDetails::setLocalPath(const QString &localPath) +{ + if(localPath.isEmpty()) { + return; + } + + if(!_localPath.isEmpty()) { + _fileWatcher.removePath(_localPath); + } + + if(_fileInfo.exists()) { + disconnect(&_fileWatcher, &QFileSystemWatcher::fileChanged, this, &FileDetails::refreshFileDetails); + } + + _localPath = localPath; + _fileInfo = QFileInfo(localPath); + + _fileWatcher.addPath(localPath); + connect(&_fileWatcher, &QFileSystemWatcher::fileChanged, this, &FileDetails::refreshFileDetails); + + const auto folder = FolderMan::instance()->folderForPath(_localPath); + const auto file = _localPath.mid(folder->cleanPath().length() + 1); + + folder->journalDb()->getFileRecord(file, &_fileRecord); + + _filelockState = _fileRecord._lockstate; + updateLockExpireString(); + + Q_EMIT fileChanged(); +} + +QString FileDetails::name() const +{ + return _fileInfo.fileName(); +} + +QString FileDetails::sizeString() const +{ + return _locale.formattedDataSize(_fileInfo.size()); +} + +QString FileDetails::lastChangedString() const +{ + static constexpr auto secsInMinute = 60; + static constexpr auto secsInHour = secsInMinute * 60; + static constexpr auto secsInDay = secsInHour * 24; + static constexpr auto secsInMonth = secsInDay * 30; + static constexpr auto secsInYear = secsInMonth * 12; + + const auto elapsedSecs = _fileInfo.lastModified().secsTo(QDateTime::currentDateTime()); + + if(elapsedSecs < 60) { + const auto elapsedSecsAsInt = static_cast(elapsedSecs); + return tr("%1 second(s) ago", "seconds elapsed since file last modified", elapsedSecsAsInt).arg(elapsedSecsAsInt); + } else if (elapsedSecs < secsInHour) { + const auto elapsedMinutes = static_cast(elapsedSecs / secsInMinute); + return tr("%1 minute(s) ago", "minutes elapsed since file last modified", elapsedMinutes).arg(elapsedMinutes); + } else if (elapsedSecs < secsInDay) { + const auto elapsedHours = static_cast(elapsedSecs / secsInHour); + return tr("%1 hour(s) ago", "hours elapsed since file last modified", elapsedHours).arg(elapsedHours); + } else if (elapsedSecs < secsInMonth) { + const auto elapsedDays = static_cast(elapsedSecs / secsInDay); + return tr("%1 day(s) ago", "days elapsed since file last modified", elapsedDays).arg(elapsedDays); + } else if (elapsedSecs < secsInYear) { + const auto elapsedMonths = static_cast(elapsedSecs / secsInMonth); + return tr("%1 month(s) ago", "months elapsed since file last modified", elapsedMonths).arg(elapsedMonths); + } else { + const auto elapsedYears = static_cast(elapsedSecs / secsInYear); + return tr("%1 year(s) ago", "years elapsed since file last modified", elapsedYears).arg(elapsedYears); + } +} + +QString FileDetails::iconUrl() const +{ + return QStringLiteral("image://tray-image-provider/:/fileicon") + _localPath; +} + +QString FileDetails::lockExpireString() const +{ + return _lockExpireString; +} + +void FileDetails::updateLockExpireString() +{ + if(!_filelockState._locked) { + _filelockStateUpdateTimer.stop(); + _lockExpireString = QString(); + Q_EMIT lockExpireStringChanged(); + return; + } + + if(!_filelockStateUpdateTimer.isActive()) { + _filelockStateUpdateTimer.start(); + } + + static constexpr auto SECONDS_PER_MINUTE = 60; + const auto lockExpirationTime = _filelockState._lockTime + _filelockState._lockTimeout; + const auto remainingTime = QDateTime::currentDateTime().secsTo(QDateTime::fromSecsSinceEpoch(lockExpirationTime)); + const auto remainingTimeInMinutes = static_cast(remainingTime > 0 ? remainingTime / SECONDS_PER_MINUTE : 0); + + _lockExpireString = tr("Locked by %1 - Expires in %2 minute(s)", "remaining time before lock expires", remainingTimeInMinutes).arg(_filelockState._lockOwnerDisplayName).arg(remainingTimeInMinutes); + Q_EMIT lockExpireStringChanged(); +} + +bool FileDetails::isFolder() const +{ + return _fileInfo.isDir(); +} + +} // namespace OCC diff --git a/src/gui/filedetails/filedetails.h b/src/gui/filedetails/filedetails.h new file mode 100644 index 000000000..7b3384709 --- /dev/null +++ b/src/gui/filedetails/filedetails.h @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2022 by Claudio Cambra + * + * 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. + */ + +#pragma once + +#include +#include +#include +#include + +#include "common/syncjournalfilerecord.h" + +namespace OCC { + +class FileDetails : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString localPath READ localPath WRITE setLocalPath NOTIFY localPathChanged) + Q_PROPERTY(QString name READ name NOTIFY fileChanged) + Q_PROPERTY(QString sizeString READ sizeString NOTIFY fileChanged) + Q_PROPERTY(QString lastChangedString READ lastChangedString NOTIFY fileChanged) + Q_PROPERTY(QString iconUrl READ iconUrl NOTIFY fileChanged) + Q_PROPERTY(QString lockExpireString READ lockExpireString NOTIFY lockExpireStringChanged) + Q_PROPERTY(bool isFolder READ isFolder NOTIFY isFolderChanged) + +public: + explicit FileDetails(QObject *parent = nullptr); + + QString localPath() const; + QString name() const; + QString sizeString() const; + QString lastChangedString() const; + QString iconUrl() const; + QString lockExpireString() const; + bool isFolder() const; + +public slots: + void setLocalPath(const QString &localPath); + +signals: + void localPathChanged(); + void fileChanged(); + void lockExpireStringChanged(); + void isFolderChanged(); + +private slots: + void refreshFileDetails(); + void updateLockExpireString(); + +private: + QString _localPath; + + QFileInfo _fileInfo; + QFileSystemWatcher _fileWatcher; + SyncJournalFileRecord _fileRecord; + SyncJournalFileLockInfo _filelockState; + QByteArray _numericFileId; + QString _lockExpireString; + QTimer _filelockStateUpdateTimer; + + QLocale _locale; +}; + +} // namespace OCC diff --git a/src/gui/filedetails/shareemodel.cpp b/src/gui/filedetails/shareemodel.cpp new file mode 100644 index 000000000..ddc9d7ac8 --- /dev/null +++ b/src/gui/filedetails/shareemodel.cpp @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2022 by Claudio Cambra + * + * 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. + */ + +#include "shareemodel.h" + +#include +#include +#include + +#include "ocsshareejob.h" + +namespace OCC { + +Q_LOGGING_CATEGORY(lcShareeModel, "com.nextcloud.shareemodel") + +ShareeModel::ShareeModel(QObject *parent) + : QAbstractListModel(parent) +{ + _userStoppedTypingTimer.setSingleShot(true); + _userStoppedTypingTimer.setInterval(500); + connect(&_userStoppedTypingTimer, &QTimer::timeout, this, &ShareeModel::fetch); +} + +// ---------------------- QAbstractListModel methods ---------------------- // + +int ShareeModel::rowCount(const QModelIndex &parent) const +{ + if(parent.isValid() || !_accountState) { + return 0; + } + + return _sharees.count(); +} + +QHash ShareeModel::roleNames() const +{ + auto roles = QAbstractListModel::roleNames(); + roles[ShareeRole] = "sharee"; + roles[AutoCompleterStringMatchRole] = "autoCompleterStringMatch"; + + return roles; +} + +QVariant ShareeModel::data(const QModelIndex &index, const int role) const +{ + if (index.row() < 0 || index.row() > _sharees.size()) { + return {}; + } + + const auto sharee = _sharees.at(index.row()); + + if(sharee.isNull()) { + return {}; + } + + switch(role) { + case Qt::DisplayRole: + return sharee->format(); + case AutoCompleterStringMatchRole: + // Don't show this to the user + return QString(sharee->displayName() + " (" + sharee->shareWith() + ")"); + case ShareeRole: + return QVariant::fromValue(sharee); + default: + qCWarning(lcShareeModel) << "Got unknown role -- returning null value."; + return {}; + } +} + +// --------------------------- QPROPERTY methods --------------------------- // + +AccountState *ShareeModel::accountState() const +{ + return _accountState.data(); +} + +void ShareeModel::setAccountState(AccountState *accountState) +{ + _accountState = accountState; + Q_EMIT accountStateChanged(); +} + +bool ShareeModel::shareItemIsFolder() const +{ + return _shareItemIsFolder; +} + +void ShareeModel::setShareItemIsFolder(const bool shareItemIsFolder) +{ + _shareItemIsFolder = shareItemIsFolder; + Q_EMIT shareItemIsFolderChanged(); +} + +QString ShareeModel::searchString() const +{ + return _searchString; +} + +void ShareeModel::setSearchString(const QString &searchString) +{ + _searchString = searchString; + Q_EMIT searchStringChanged(); + + _userStoppedTypingTimer.start(); +} + +bool ShareeModel::fetchOngoing() const +{ + return _fetchOngoing; +} + +ShareeModel::LookupMode ShareeModel::lookupMode() const +{ + return _lookupMode; +} + +void ShareeModel::setLookupMode(const ShareeModel::LookupMode lookupMode) +{ + _lookupMode = lookupMode; + Q_EMIT lookupModeChanged(); +} + +// ------------------------- Internal data methods ------------------------- // + +void ShareeModel::fetch() +{ + if(!_accountState || !_accountState->account() || _searchString.isEmpty()) { + qCInfo(lcShareeModel) << "Not fetching sharees for searchString: " << _searchString; + return; + } + + _fetchOngoing = true; + Q_EMIT fetchOngoingChanged(); + + const auto shareItemTypeString = _shareItemIsFolder ? QStringLiteral("folder") : QStringLiteral("file"); + + auto *job = new OcsShareeJob(_accountState->account()); + + connect(job, &OcsShareeJob::shareeJobFinished, this, &ShareeModel::shareesFetched); + connect(job, &OcsJob::ocsError, this, [&](const int statusCode, const QString &message) { + _fetchOngoing = false; + Q_EMIT fetchOngoingChanged(); + Q_EMIT ShareeModel::displayErrorMessage(statusCode, message); + }); + + job->getSharees(_searchString, shareItemTypeString, 1, 50, _lookupMode == LookupMode::GlobalSearch ? true : false); +} + +void ShareeModel::shareesFetched(const QJsonDocument &reply) +{ + _fetchOngoing = false; + Q_EMIT fetchOngoingChanged(); + + qCInfo(lcShareeModel) << "SearchString: " << _searchString << "resulted in reply: " << reply; + + QVector newSharees; + + const QStringList shareeTypes {"users", "groups", "emails", "remotes", "circles", "rooms"}; + + const auto appendSharees = [this, &shareeTypes, &newSharees](const QJsonObject &data) { + for (const auto &shareeType : shareeTypes) { + const auto category = data.value(shareeType).toArray(); + + for (const auto &sharee : category) { + const auto shareeJsonObject = sharee.toObject(); + const auto parsedSharee = parseSharee(shareeJsonObject); + + // Filter sharees that we have already shared with + const auto shareeInBlacklistIt = std::find_if(_shareeBlacklist.cbegin(), + _shareeBlacklist.cend(), + [&parsedSharee](const ShareePtr &blacklistSharee) { + return parsedSharee->type() == blacklistSharee->type() && + parsedSharee->shareWith() == blacklistSharee->shareWith(); + }); + + if (shareeInBlacklistIt != _shareeBlacklist.cend()) { + continue; + } + + newSharees.append(parsedSharee); + } + } + }; + const auto replyDataObject = reply.object().value("ocs").toObject().value("data").toObject(); + const auto replyDataExactMatchObject = replyDataObject.value("exact").toObject(); + + appendSharees(replyDataObject); + appendSharees(replyDataExactMatchObject); + + Q_EMIT beginResetModel(); + _sharees = newSharees; + Q_EMIT endResetModel(); + + Q_EMIT shareesReady(); +} + +ShareePtr ShareeModel::parseSharee(const QJsonObject &data) const +{ + auto displayName = data.value("label").toString(); + const auto shareWith = data.value("value").toObject().value("shareWith").toString(); + const auto type = (Sharee::Type)data.value("value").toObject().value("shareType").toInt(); + const auto additionalInfo = data.value("value").toObject().value("shareWithAdditionalInfo").toString(); + if (!additionalInfo.isEmpty()) { + displayName = tr("%1 (%2)", "sharee (shareWithAdditionalInfo)").arg(displayName, additionalInfo); + } + + return ShareePtr(new Sharee(shareWith, displayName, type)); +} + +} diff --git a/src/gui/filedetails/shareemodel.h b/src/gui/filedetails/shareemodel.h new file mode 100644 index 000000000..954f10ad7 --- /dev/null +++ b/src/gui/filedetails/shareemodel.h @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2022 by Claudio Cambra + * + * 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. + */ + +#pragma once + +#include +#include + +#include "accountstate.h" +#include "sharee.h" + +class QJsonDocument; +class QJsonObject; + +namespace OCC { + +class ShareeModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(AccountState* accountState READ accountState WRITE setAccountState NOTIFY accountStateChanged) + Q_PROPERTY(bool shareItemIsFolder READ shareItemIsFolder WRITE setShareItemIsFolder NOTIFY shareItemIsFolderChanged) + Q_PROPERTY(QString searchString READ searchString WRITE setSearchString NOTIFY searchStringChanged) + Q_PROPERTY(bool fetchOngoing READ fetchOngoing NOTIFY fetchOngoingChanged) + Q_PROPERTY(LookupMode lookupMode READ lookupMode WRITE setLookupMode NOTIFY lookupModeChanged) + +public: + enum class LookupMode { + LocalSearch = 0, + GlobalSearch = 1 + }; + Q_ENUM(LookupMode); + + enum Roles { + ShareeRole = Qt::UserRole + 1, + AutoCompleterStringMatchRole, + }; + Q_ENUM(Roles); + + explicit ShareeModel(QObject *parent = nullptr); + + using ShareeSet = QVector; // FIXME: make it a QSet when Sharee can be compared + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QHash roleNames() const override; + QVariant data(const QModelIndex &index, const int role) const override; + + AccountState *accountState() const; + bool shareItemIsFolder() const; + QString searchString() const; + bool fetchOngoing() const; + LookupMode lookupMode() const; + +signals: + void accountStateChanged(); + void shareItemIsFolderChanged(); + void searchStringChanged(); + void fetchOngoingChanged(); + void lookupModeChanged(); + + void shareesReady(); + void displayErrorMessage(int code, const QString &); + +public slots: + void setAccountState(AccountState *accountState); + void setShareItemIsFolder(const bool shareItemIsFolder); + void setSearchString(const QString &searchString); + void setLookupMode(const LookupMode lookupMode); + + void fetch(); + +private slots: + void shareesFetched(const QJsonDocument &reply); + +private: + ShareePtr parseSharee(const QJsonObject &data) const; + + QTimer _userStoppedTypingTimer; + + AccountStatePtr _accountState; + QString _searchString; + bool _shareItemIsFolder = false; + bool _fetchOngoing = false; + LookupMode _lookupMode = LookupMode::LocalSearch; + + QVector _sharees; + QVector _shareeBlacklist; +}; + +} diff --git a/src/gui/filedetails/sharemodel.cpp b/src/gui/filedetails/sharemodel.cpp new file mode 100644 index 000000000..fba85c700 --- /dev/null +++ b/src/gui/filedetails/sharemodel.cpp @@ -0,0 +1,973 @@ +/* + * Copyright (C) 2022 by Claudio Cambra + * + * 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. + */ + +#include "sharemodel.h" + +#include +#include + +#include "account.h" +#include "folderman.h" +#include "theme.h" +#include "wordlist.h" + +namespace { + +static const QString placeholderLinkShareId = QStringLiteral("__placeholderLinkShareId__"); + +QString createRandomPassword() +{ + const auto words = OCC::WordList::getRandomWords(10); + + const auto addFirstLetter = [](const QString ¤t, const QString &next) -> QString { + return current + next.at(0); + }; + + return std::accumulate(std::cbegin(words), std::cend(words), QString(), addFirstLetter); +} +} + +namespace OCC { + +Q_LOGGING_CATEGORY(lcShareModel, "com.nextcloud.sharemodel") + +ShareModel::ShareModel(QObject *parent) + : QAbstractListModel(parent) +{ +} + +// ---------------------- QAbstractListModel methods ---------------------- // + +int ShareModel::rowCount(const QModelIndex &parent) const +{ + if(parent.isValid() || !_accountState || _localPath.isEmpty()) { + return 0; + } + + return _shares.count(); +} + +QHash ShareModel::roleNames() const +{ + auto roles = QAbstractListModel::roleNames(); + roles[ShareRole] = "share"; + roles[ShareTypeRole] = "shareType"; + roles[ShareIdRole] = "shareId"; + roles[IconUrlRole] = "iconUrl"; + roles[AvatarUrlRole] = "avatarUrl"; + roles[LinkRole] = "link"; + roles[LinkShareNameRole] = "linkShareName"; + roles[LinkShareLabelRole] = "linkShareLabel"; + roles[NoteEnabledRole] = "noteEnabled"; + roles[NoteRole] = "note"; + roles[ExpireDateEnabledRole] = "expireDateEnabled"; + roles[ExpireDateEnforcedRole] = "expireDateEnforced"; + roles[ExpireDateRole] = "expireDate"; + roles[EnforcedMaximumExpireDateRole] = "enforcedMaximumExpireDate"; + roles[PasswordProtectEnabledRole] = "passwordProtectEnabled"; + roles[PasswordRole] = "password"; + roles[PasswordEnforcedRole] = "passwordEnforced"; + roles[EditingAllowedRole] = "editingAllowed"; + + return roles; +} + +QVariant ShareModel::data(const QModelIndex &index, const int role) const +{ + if (!index.isValid()) { + return {}; + } + + const auto share = _shares.at(index.row()); + + if (!share) { + return {}; + } + + // Some roles only provide values for the link and user/group share types + if(const auto linkShare = share.objectCast()) { + switch(role) { + case LinkRole: + return linkShare->getLink(); + case LinkShareNameRole: + return linkShare->getName(); + case LinkShareLabelRole: + return linkShare->getLabel(); + case NoteEnabledRole: + return !linkShare->getNote().isEmpty(); + case NoteRole: + return linkShare->getNote(); + case ExpireDateEnabledRole: + return linkShare->getExpireDate().isValid(); + case ExpireDateRole: + { + const auto startOfExpireDayUTC = linkShare->getExpireDate().startOfDay(QTimeZone::utc()); + return startOfExpireDayUTC.toMSecsSinceEpoch(); + } + default: + break; + } + + } else if (const auto userGroupShare = share.objectCast()) { + switch(role) { + case NoteEnabledRole: + return !userGroupShare->getNote().isEmpty(); + case NoteRole: + return userGroupShare->getNote(); + case ExpireDateEnabledRole: + return userGroupShare->getExpireDate().isValid(); + case ExpireDateRole: + { + const auto startOfExpireDayUTC = userGroupShare->getExpireDate().startOfDay(QTimeZone::utc()); + return startOfExpireDayUTC.toMSecsSinceEpoch(); + } + default: + break; + } + } + + switch(role) { + case Qt::DisplayRole: + return displayStringForShare(share); + case ShareRole: + return QVariant::fromValue(share); + case ShareTypeRole: + return share->getShareType(); + case ShareIdRole: + return share->getId(); + case IconUrlRole: + return iconUrlForShare(share); + case AvatarUrlRole: + return avatarUrlForShare(share); + case ExpireDateEnforcedRole: + return expireDateEnforcedForShare(share); + case EnforcedMaximumExpireDateRole: + return enforcedMaxExpireDateForShare(share); + case PasswordProtectEnabledRole: + return share->isPasswordSet(); + case PasswordRole: + if (!share->isPasswordSet() || !_shareIdRecentlySetPasswords.contains(share->getId())) { + return {}; + } + return _shareIdRecentlySetPasswords.value(share->getId()); + case PasswordEnforcedRole: + return _accountState && _accountState->account() && _accountState->account()->capabilities().isValid() && + ((share->getShareType() == Share::TypeEmail && _accountState->account()->capabilities().shareEmailPasswordEnforced()) || + (share->getShareType() == Share::TypeLink && _accountState->account()->capabilities().sharePublicLinkEnforcePassword())); + case EditingAllowedRole: + return share->getPermissions().testFlag(SharePermissionUpdate); + + // Deal with roles that only return certain values for link or user/group share types + case NoteEnabledRole: + case ExpireDateEnabledRole: + return false; + case LinkRole: + case LinkShareNameRole: + case LinkShareLabelRole: + case NoteRole: + case ExpireDateRole: + return {}; + default: + qCWarning(lcShareModel) << "Got unknown role" << role + << "for share of type" << share->getShareType() + << "so returning null value."; + return {}; + } +} + +// ---------------------- Internal model data methods ---------------------- // + +void ShareModel::resetData() +{ + beginResetModel(); + + _folder = nullptr; + _sharePath.clear(); + _maxSharingPermissions = {}; + _numericFileId.clear(); + _manager.clear(); + _shares.clear(); + _fetchOngoing = false; + _hasInitialShareFetchCompleted = false; + + Q_EMIT fetchOngoingChanged(); + Q_EMIT hasInitialShareFetchCompletedChanged(); + + endResetModel(); +} + +void ShareModel::updateData() +{ + resetData(); + + if (_localPath.isEmpty() || !_accountState || _accountState->account().isNull()) { + qCWarning(lcShareModel) << "Not updating share model data. Local path is:" << _localPath + << "Is account state null:" << !_accountState; + return; + } + + if (!sharingEnabled()) { + qCWarning(lcShareModel) << "Server does not support sharing"; + return; + } + + _folder = FolderMan::instance()->folderForPath(_localPath); + + if (!_folder) { + qCWarning(lcShareModel) << "Could not update share model data for" << _localPath << "no responsible folder found"; + resetData(); + return; + } + + qCDebug(lcShareModel) << "Updating share model data now."; + + const auto relPath = _localPath.mid(_folder->cleanPath().length() + 1); + _sharePath = _folder->remotePathTrailingSlash() + relPath; + + SyncJournalFileRecord fileRecord; + bool resharingAllowed = true; // lets assume the good + + if(_folder->journalDb()->getFileRecord(relPath, &fileRecord) && fileRecord.isValid()) { + if (!fileRecord._remotePerm.isNull() && + !fileRecord._remotePerm.hasPermission(RemotePermissions::CanReshare)) { + + resharingAllowed = false; + } + } + + _maxSharingPermissions = resharingAllowed ? SharePermissions(_accountState->account()->capabilities().shareDefaultPermissions()) : SharePermissions({}); + Q_EMIT sharePermissionsChanged(); + + _numericFileId = fileRecord.numericFileId(); + + _placeholderLinkShare.reset(new Share(_accountState->account(), + placeholderLinkShareId, + _accountState->account()->id(), + _accountState->account()->davDisplayName(), + _sharePath, + Share::TypePlaceholderLink)); + slotAddShare(_placeholderLinkShare); + + auto job = new PropfindJob(_accountState->account(), _sharePath); + job->setProperties( + QList() + << "https://open-collaboration-services.org/ns:share-permissions" + << "https://owncloud.org/ns:fileid" // numeric file id for fallback private link generation + << "https://owncloud.org/ns:privatelink"); + job->setTimeout(10 * 1000); + connect(job, &PropfindJob::result, this, &ShareModel::slotPropfindReceived); + connect(job, &PropfindJob::finishedWithError, this, [&]{ + qCWarning(lcShareModel) << "Propfind for" << _sharePath << "failed"; + _fetchOngoing = false; + Q_EMIT fetchOngoingChanged(); + }); + + _fetchOngoing = true; + Q_EMIT fetchOngoingChanged(); + job->start(); + + initShareManager(); +} + +void ShareModel::initShareManager() +{ + if (!_accountState || _accountState->account().isNull()) { + return; + } + + bool sharingPossible = true; + if (!publicLinkSharesEnabled()) { + qCWarning(lcSharing) << "Link shares have been disabled"; + sharingPossible = false; + } else if (!canShare()) { + qCWarning(lcSharing) << "The file cannot be shared because it does not have sharing permission."; + sharingPossible = false; + } + + if (_manager.isNull() && sharingPossible) { + _manager.reset(new ShareManager(_accountState->account(), this)); + connect(_manager.data(), &ShareManager::sharesFetched, this, &ShareModel::slotSharesFetched); + connect(_manager.data(), &ShareManager::shareCreated, this, [&]{ _manager->fetchShares(_sharePath); }); + connect(_manager.data(), &ShareManager::linkShareCreated, this, &ShareModel::slotAddShare); + connect(_manager.data(), &ShareManager::linkShareRequiresPassword, this, &ShareModel::requestPasswordForLinkShare); + + _manager->fetchShares(_sharePath); + } +} + +void ShareModel::slotPropfindReceived(const QVariantMap &result) +{ + _fetchOngoing = false; + Q_EMIT fetchOngoingChanged(); + + const QVariant receivedPermissions = result["share-permissions"]; + if (!receivedPermissions.toString().isEmpty()) { + _maxSharingPermissions = static_cast(receivedPermissions.toInt()); + Q_EMIT sharePermissionsChanged(); + qCInfo(lcShareModel) << "Received sharing permissions for" << _sharePath << _maxSharingPermissions; + } + + const auto privateLinkUrl = result["privatelink"].toString(); + const auto numericFileId = result["fileid"].toByteArray(); + + if (!privateLinkUrl.isEmpty()) { + qCInfo(lcShareModel) << "Received private link url for" << _sharePath << privateLinkUrl; + _privateLinkUrl = privateLinkUrl; + } else if (!numericFileId.isEmpty()) { + qCInfo(lcShareModel) << "Received numeric file id for" << _sharePath << numericFileId; + _privateLinkUrl = _accountState->account()->deprecatedPrivateLinkUrl(numericFileId).toString(QUrl::FullyEncoded); + } +} + +void ShareModel::slotSharesFetched(const QList &shares) +{ + _hasInitialShareFetchCompleted = true; + Q_EMIT hasInitialShareFetchCompletedChanged(); + + qCInfo(lcSharing) << "Fetched" << shares.count() << "shares"; + + for (const auto &share : shares) { + if (share.isNull() || + share->account().isNull() || + share->getUidOwner() != share->account()->davUser()) { + + continue; + } + + slotAddShare(share); + } +} + +void ShareModel::slotAddShare(const SharePtr &share) +{ + if (share.isNull()) { + return; + } + + const auto shareId = share->getId(); + + // Remove placeholder link share if this is a link share + if(share->getShareType() == Share::TypeLink) { + slotRemoveShareWithId(placeholderLinkShareId); + } + + QModelIndex shareModelIndex; + + if (_shareIdIndexHash.contains(shareId)) { + const auto sharePersistentModelIndex = _shareIdIndexHash.value(shareId); + const auto shareIndex = sharePersistentModelIndex.row(); + + _shares.replace(shareIndex, share); + + shareModelIndex = index(sharePersistentModelIndex.row()); + Q_EMIT dataChanged(shareModelIndex, shareModelIndex); + } else { + const auto shareIndex = _shares.count(); + + beginInsertRows({}, _shares.count(), _shares.count()); + _shares.append(share); + endInsertRows(); + + shareModelIndex = index(shareIndex); + } + + const QPersistentModelIndex sharePersistentIndex(shareModelIndex); + _shareIdIndexHash.insert(shareId, sharePersistentIndex); + + connect(share.data(), &Share::serverError, this, &ShareModel::slotServerError); + connect(share.data(), &Share::passwordSetError, this, [this, shareId](const int code, const QString &message) { + _shareIdRecentlySetPasswords.remove(shareId); + slotServerError(code, message); + slotSharePasswordSet(shareId); + Q_EMIT passwordSetError(shareId); + }); + + // Passing shareId by reference here will cause crashing, so we pass by value + connect(share.data(), &Share::shareDeleted, this, [this, shareId]{ slotRemoveShareWithId(shareId); }); + connect(share.data(), &Share::permissionsSet, this, [this, shareId]{ slotSharePermissionsSet(shareId); }); + connect(share.data(), &Share::passwordSet, this, [this, shareId]{ slotSharePasswordSet(shareId); }); + + if (const auto linkShare = share.objectCast()) { + connect(linkShare.data(), &LinkShare::noteSet, this, [this, shareId]{ slotShareNoteSet(shareId); }); + connect(linkShare.data(), &LinkShare::nameSet, this, [this, shareId]{ slotShareNameSet(shareId); }); + connect(linkShare.data(), &LinkShare::labelSet, this, [this, shareId]{ slotShareLabelSet(shareId); }); + connect(linkShare.data(), &LinkShare::expireDateSet, this, [this, shareId]{ slotShareExpireDateSet(shareId); }); + } else if (const auto userGroupShare = share.objectCast()) { + connect(userGroupShare.data(), &UserGroupShare::noteSet, this, [this, shareId]{ slotShareNoteSet(shareId); }); + connect(userGroupShare.data(), &UserGroupShare::expireDateSet, this, [this, shareId]{ slotShareExpireDateSet(shareId); }); + } + + if (_manager) { + connect(_manager.data(), &ShareManager::serverError, this, &ShareModel::slotServerError); + } + + Q_EMIT sharesChanged(); +} + +void ShareModel::slotRemoveShareWithId(const QString &shareId) +{ + if (_shares.empty() || shareId.isEmpty() || !_shareIdIndexHash.contains(shareId)) { + return; + } + + _shareIdRecentlySetPasswords.remove(shareId); + const auto shareIndex = _shareIdIndexHash.take(shareId); + + if (!shareIndex.isValid()) { + qCWarning(lcShareModel) << "Won't remove share with id:" << shareId + << ", invalid share index: " << shareIndex; + return; + } + + beginRemoveRows({}, shareIndex.row(), shareIndex.row()); + _shares.removeAt(shareIndex.row()); + endRemoveRows(); + + // If no link shares then re-add placeholder link share + if (shareIndex.data(ShareModel::ShareTypeRole).toInt() == Share::TypeLink) { + + // Early return if we find another link share + for(const auto &share : _shares) { + if(share->getShareType() == Share::TypeLink) { + return; + } + } + + slotAddShare(_placeholderLinkShare); + } + + Q_EMIT sharesChanged(); +} + +void ShareModel::slotServerError(const int code, const QString &message) +{ + qCWarning(lcShareModel) << "Error from server" << code << message; + Q_EMIT serverError(code, message); +} + +QString ShareModel::displayStringForShare(const SharePtr &share) const +{ + if (const auto linkShare = share.objectCast()) { + const auto displayString = tr("Link share"); + + if (!linkShare->getLabel().isEmpty()) { + return QStringLiteral("%1 (%2)").arg(displayString, linkShare->getLabel()); + } + + return displayString; + } else if (share->getShareType() == Share::TypePlaceholderLink) { + return tr("Link share"); + } else if (share->getShareWith()) { + return share->getShareWith()->format(); + } + + qCWarning(lcShareModel) << "Unable to provide good display string for share"; + return QStringLiteral("Share"); +} + +QString ShareModel::iconUrlForShare(const SharePtr &share) const +{ + const auto iconsPath = QStringLiteral("image://svgimage-custom-color/"); + + switch(share->getShareType()) { + case Share::TypePlaceholderLink: + case Share::TypeLink: + return QString(iconsPath + QStringLiteral("public.svg")); + case Share::TypeEmail: + return QString(iconsPath + QStringLiteral("email.svg")); + case Share::TypeRoom: + return QString(iconsPath + QStringLiteral("wizard-talk.svg")); + case Share::TypeUser: + return QString(iconsPath + QStringLiteral("user.svg")); + case Share::TypeGroup: + return QString(iconsPath + QStringLiteral("wizard-groupware.svg")); + default: + return {}; + } +} + +QString ShareModel::avatarUrlForShare(const SharePtr &share) const +{ + if (share->getShareWith() && share->getShareWith()->type() == Sharee::User && _accountState && _accountState->account()) { + const QString provider = QStringLiteral("image://tray-image-provider/"); + const QString userId = share->getShareWith()->shareWith(); + const QString avatarUrl = Utility::concatUrlPath(_accountState->account()->url(), + QString("remote.php/dav/avatars/%1/%2.png").arg(userId, QString::number(64))).toString(); + return QString(provider + avatarUrl); + } + + return {}; +} + +long long ShareModel::enforcedMaxExpireDateForShare(const SharePtr &share) const +{ + if (!_accountState || !_accountState->account() || !_accountState->account()->capabilities().isValid()) { + return {}; + } + + auto expireDays = -1; + + // Both public links and emails count as "public" shares + if ((share->getShareType() == Share::TypeLink || share->getShareType() == Share::TypeEmail) + && _accountState->account()->capabilities().sharePublicLinkEnforceExpireDate()) { + expireDays = _accountState->account()->capabilities().sharePublicLinkExpireDateDays(); + + } else if (share->getShareType() == Share::TypeRemote && _accountState->account()->capabilities().shareRemoteEnforceExpireDate()) { + expireDays = _accountState->account()->capabilities().shareRemoteExpireDateDays(); + + } else if ((share->getShareType() == Share::TypeUser || + share->getShareType() == Share::TypeGroup || + share->getShareType() == Share::TypeCircle || + share->getShareType() == Share::TypeRoom) && + _accountState->account()->capabilities().shareInternalEnforceExpireDate()) { + expireDays = _accountState->account()->capabilities().shareInternalExpireDateDays(); + + } else { + return {}; + } + + const auto expireDateTime = QDate::currentDate().addDays(expireDays).startOfDay(QTimeZone::utc()); + return expireDateTime.toMSecsSinceEpoch(); +} + +bool ShareModel::expireDateEnforcedForShare(const SharePtr &share) const +{ + if(!_accountState || !_accountState->account() || !_accountState->account()->capabilities().isValid()) { + return false; + } + + // Both public links and emails count as "public" shares + if (share->getShareType() == Share::TypeLink || + share->getShareType() == Share::TypeEmail) { + return _accountState->account()->capabilities().sharePublicLinkEnforceExpireDate(); + + } else if (share->getShareType() == Share::TypeRemote) { + return _accountState->account()->capabilities().shareRemoteEnforceExpireDate(); + + } else if (share->getShareType() == Share::TypeUser || + share->getShareType() == Share::TypeGroup || + share->getShareType() == Share::TypeCircle || + share->getShareType() == Share::TypeRoom) { + return _accountState->account()->capabilities().shareInternalEnforceExpireDate(); + + } + + return false; +} + +// ----------------- Shares modified signal handling slots ----------------- // + +void ShareModel::slotSharePermissionsSet(const QString &shareId) +{ + if (shareId.isEmpty() || !_shareIdIndexHash.contains(shareId)) { + return; + } + + const auto sharePersistentModelIndex = _shareIdIndexHash.value(shareId); + const auto shareModelIndex = index(sharePersistentModelIndex.row()); + Q_EMIT dataChanged(shareModelIndex, shareModelIndex, { EditingAllowedRole }); +} + +void ShareModel::slotSharePasswordSet(const QString &shareId) +{ + if (shareId.isEmpty() || !_shareIdIndexHash.contains(shareId)) { + return; + } + + const auto sharePersistentModelIndex = _shareIdIndexHash.value(shareId); + const auto shareModelIndex = index(sharePersistentModelIndex.row()); + Q_EMIT dataChanged(shareModelIndex, shareModelIndex, { PasswordProtectEnabledRole, PasswordRole }); +} + +void ShareModel::slotShareNoteSet(const QString &shareId) +{ + if (shareId.isEmpty() || !_shareIdIndexHash.contains(shareId)) { + return; + } + + const auto sharePersistentModelIndex = _shareIdIndexHash.value(shareId); + const auto shareModelIndex = index(sharePersistentModelIndex.row()); + Q_EMIT dataChanged(shareModelIndex, shareModelIndex, { NoteEnabledRole, NoteRole }); +} + +void ShareModel::slotShareNameSet(const QString &shareId) +{ + if (shareId.isEmpty() || !_shareIdIndexHash.contains(shareId)) { + return; + } + + const auto sharePersistentModelIndex = _shareIdIndexHash.value(shareId); + const auto shareModelIndex = index(sharePersistentModelIndex.row()); + Q_EMIT dataChanged(shareModelIndex, shareModelIndex, { LinkShareNameRole }); +} + +void ShareModel::slotShareLabelSet(const QString &shareId) +{ + if (shareId.isEmpty() || !_shareIdIndexHash.contains(shareId)) { + return; + } + + const auto sharePersistentModelIndex = _shareIdIndexHash.value(shareId); + const auto shareModelIndex = index(sharePersistentModelIndex.row()); + Q_EMIT dataChanged(shareModelIndex, shareModelIndex, { Qt::DisplayRole, LinkShareLabelRole }); +} + +void ShareModel::slotShareExpireDateSet(const QString &shareId) +{ + if (shareId.isEmpty() || !_shareIdIndexHash.contains(shareId)) { + return; + } + + const auto sharePersistentModelIndex = _shareIdIndexHash.value(shareId); + const auto shareModelIndex = index(sharePersistentModelIndex.row()); + Q_EMIT dataChanged(shareModelIndex, shareModelIndex, { ExpireDateEnabledRole, ExpireDateRole }); +} + +// ----------------------- Shares modification slots ----------------------- // + +void ShareModel::toggleShareAllowEditing(const SharePtr &share, const bool enable) const +{ + if (share.isNull()) { + return; + } + + auto permissions = share->getPermissions(); + enable ? permissions |= SharePermissionUpdate : permissions &= ~SharePermissionUpdate; + + share->setPermissions(permissions); +} + +void ShareModel::toggleShareAllowEditingFromQml(const QVariant &share, const bool enable) const +{ + const auto ptr = share.value(); + toggleShareAllowEditing(ptr, enable); +} + +void ShareModel::toggleShareAllowResharing(const SharePtr &share, const bool enable) const +{ + if (share.isNull()) { + return; + } + + auto permissions = share->getPermissions(); + enable ? permissions |= SharePermissionShare : permissions &= ~SharePermissionShare; + + share->setPermissions(permissions); +} + +void ShareModel::toggleShareAllowResharingFromQml(const QVariant &share, const bool enable) const +{ + const auto ptr = share.value(); + toggleShareAllowResharing(ptr, enable); +} + +void ShareModel::toggleSharePasswordProtect(const SharePtr &share, const bool enable) +{ + if (share.isNull()) { + return; + } + + if(!enable) { + share->setPassword({}); + return; + } + + const auto randomPassword = createRandomPassword(); + _shareIdRecentlySetPasswords.insert(share->getId(), randomPassword); + share->setPassword(randomPassword); +} + +void ShareModel::toggleSharePasswordProtectFromQml(const QVariant &share, const bool enable) +{ + const auto ptr = share.value(); + toggleSharePasswordProtect(ptr, enable); +} + +void ShareModel::toggleShareExpirationDate(const SharePtr &share, const bool enable) const +{ + if (share.isNull()) { + return; + } + + const auto expireDate = enable ? QDate::currentDate().addDays(1) : QDate(); + + if (const auto linkShare = share.objectCast()) { + linkShare->setExpireDate(expireDate); + } else if (const auto userGroupShare = share.objectCast()) { + userGroupShare->setExpireDate(expireDate); + } +} + +void ShareModel::toggleShareExpirationDateFromQml(const QVariant &share, const bool enable) const +{ + const auto ptr = share.value(); + toggleShareExpirationDate(ptr, enable); +} + +void ShareModel::toggleShareNoteToRecipient(const SharePtr &share, const bool enable) const +{ + if (share.isNull()) { + return; + } + + const QString note = enable ? tr("Enter a note for the recipient") : QString(); + if (const auto linkShare = share.objectCast()) { + linkShare->setNote(note); + } else if (const auto userGroupShare = share.objectCast()) { + userGroupShare->setNote(note); + } +} + +void ShareModel::toggleShareNoteToRecipientFromQml(const QVariant &share, const bool enable) const +{ + const auto ptr = share.value(); + toggleShareNoteToRecipient(ptr, enable); +} + +void ShareModel::setLinkShareLabel(const QSharedPointer &linkShare, const QString &label) const +{ + if (linkShare.isNull()) { + return; + } + + linkShare->setLabel(label); +} + +void ShareModel::setLinkShareLabelFromQml(const QVariant &linkShare, const QString &label) const +{ + // All of our internal share pointers are SharePtr, so cast to LinkShare for this method + const auto ptr = linkShare.value().objectCast(); + setLinkShareLabel(ptr, label); +} + +void ShareModel::setShareExpireDate(const SharePtr &share, const qint64 milliseconds) const +{ + if (share.isNull()) { + return; + } + + const auto date = QDateTime::fromMSecsSinceEpoch(milliseconds, QTimeZone::utc()).date(); + + if (const auto linkShare = share.objectCast()) { + linkShare->setExpireDate(date); + } else if (const auto userGroupShare = share.objectCast()) { + userGroupShare->setExpireDate(date); + } +} + +void ShareModel::setShareExpireDateFromQml(const QVariant &share, const QVariant milliseconds) const +{ + const auto ptr = share.value(); + const auto millisecondsLL = milliseconds.toLongLong(); + setShareExpireDate(ptr, millisecondsLL); +} + +void ShareModel::setSharePassword(const SharePtr &share, const QString &password) +{ + if (share.isNull()) { + return; + } + + _shareIdRecentlySetPasswords.insert(share->getId(), password); + share->setPassword(password); +} + +void ShareModel::setSharePasswordFromQml(const QVariant &share, const QString &password) +{ + const auto ptr = share.value(); + setSharePassword(ptr, password); +} + +void ShareModel::setShareNote(const SharePtr &share, const QString ¬e) const +{ + if (share.isNull()) { + return; + } + + if (const auto linkShare = share.objectCast()) { + linkShare->setNote(note); + } else if (const auto userGroupShare = share.objectCast()) { + userGroupShare->setNote(note); + } +} + +void ShareModel::setShareNoteFromQml(const QVariant &share, const QString ¬e) const +{ + const auto ptr = share.value(); + setShareNote(ptr, note); +} + +// ------------------- Share creation and deletion slots ------------------- // + +void ShareModel::createNewLinkShare() const +{ + if (_manager) { + const auto askOptionalPassword = _accountState->account()->capabilities().sharePublicLinkAskOptionalPassword(); + const auto password = askOptionalPassword ? createRandomPassword() : QString(); + _manager->createLinkShare(_sharePath, QString(), password); + } +} + +void ShareModel::createNewLinkShareWithPassword(const QString &password) const +{ + if (_manager) { + _manager->createLinkShare(_sharePath, QString(), password); + } +} + +void ShareModel::createNewUserGroupShare(const ShareePtr &sharee) +{ + if (sharee.isNull()) { + return; + } + + qCInfo(lcShareModel) << "Creating new user/group share for sharee: " << sharee->format(); + + if (sharee->type() == Sharee::Email && + _accountState && + !_accountState->account().isNull() && + _accountState->account()->capabilities().isValid() && + _accountState->account()->capabilities().shareEmailPasswordEnforced()) { + + Q_EMIT requestPasswordForEmailSharee(sharee); + return; + } + + _manager->createShare(_sharePath, + Share::ShareType(sharee->type()), + sharee->shareWith(), + _maxSharingPermissions, + {}); +} + +void ShareModel::createNewUserGroupShareWithPassword(const ShareePtr &sharee, const QString &password) const +{ + if (sharee.isNull()) { + return; + } + + _manager->createShare(_sharePath, + Share::ShareType(sharee->type()), + sharee->shareWith(), + _maxSharingPermissions, + password); +} + +void ShareModel::createNewUserGroupShareFromQml(const QVariant &sharee) +{ + const auto ptr = sharee.value(); + createNewUserGroupShare(ptr); +} + +void ShareModel::createNewUserGroupShareWithPasswordFromQml(const QVariant &sharee, const QString &password) const +{ + const auto ptr = sharee.value(); + createNewUserGroupShareWithPassword(ptr, password); +} + +void ShareModel::deleteShare(const SharePtr &share) const +{ + if(share.isNull()) { + return; + } + + share->deleteShare(); +} + +void ShareModel::deleteShareFromQml(const QVariant &share) const +{ + const auto ptr = share.value(); + deleteShare(ptr); +} + +// --------------------------- QPROPERTY methods --------------------------- // + +QString ShareModel::localPath() const +{ + return _localPath; +} + +void ShareModel::setLocalPath(const QString &localPath) +{ + _localPath = localPath; + Q_EMIT localPathChanged(); + updateData(); +} + +AccountState *ShareModel::accountState() const +{ + return _accountState; +} + +void ShareModel::setAccountState(AccountState *accountState) +{ + _accountState = accountState; + + // Change the server and account-related properties + connect(_accountState, &AccountState::stateChanged, this, &ShareModel::accountConnectedChanged); + connect(_accountState, &AccountState::stateChanged, this, &ShareModel::sharingEnabledChanged); + connect(_accountState, &AccountState::stateChanged, this, &ShareModel::publicLinkSharesEnabledChanged); + connect(_accountState, &AccountState::stateChanged, this, &ShareModel::userGroupSharingEnabledChanged); + + Q_EMIT accountStateChanged(); + Q_EMIT accountConnectedChanged(); + Q_EMIT sharingEnabledChanged(); + Q_EMIT publicLinkSharesEnabledChanged(); + Q_EMIT userGroupSharingEnabledChanged(); + updateData(); +} + +bool ShareModel::accountConnected() const +{ + return _accountState && _accountState->isConnected(); +} + +bool ShareModel::sharingEnabled() const +{ + return _accountState && + _accountState->account() && + _accountState->account()->capabilities().isValid() && + _accountState->account()->capabilities().shareAPI(); +} + +bool ShareModel::publicLinkSharesEnabled() const +{ + return Theme::instance()->linkSharing() && + _accountState && + _accountState->account() && + _accountState->account()->capabilities().isValid() && + _accountState->account()->capabilities().sharePublicLink(); +} + +bool ShareModel::userGroupSharingEnabled() const +{ + return Theme::instance()->userGroupSharing(); +} + +bool ShareModel::fetchOngoing() const +{ + return _fetchOngoing; +} + +bool ShareModel::hasInitialShareFetchCompleted() const +{ + return _hasInitialShareFetchCompleted; +} + +bool ShareModel::canShare() const +{ + return _maxSharingPermissions & SharePermissionShare; +} + +} // namespace OCC diff --git a/src/gui/filedetails/sharemodel.h b/src/gui/filedetails/sharemodel.h new file mode 100644 index 000000000..aa4989744 --- /dev/null +++ b/src/gui/filedetails/sharemodel.h @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2022 by Claudio Cambra + * + * 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. + */ + +#pragma once + +#include + +#include "accountstate.h" +#include "folder.h" +#include "sharemanager.h" +#include "sharepermissions.h" + +namespace OCC { + +class ShareModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(AccountState* accountState READ accountState WRITE setAccountState NOTIFY accountStateChanged) + Q_PROPERTY(QString localPath READ localPath WRITE setLocalPath NOTIFY localPathChanged) + Q_PROPERTY(bool accountConnected READ accountConnected NOTIFY accountConnectedChanged) + Q_PROPERTY(bool sharingEnabled READ sharingEnabled NOTIFY sharingEnabledChanged) + Q_PROPERTY(bool publicLinkSharesEnabled READ publicLinkSharesEnabled NOTIFY publicLinkSharesEnabledChanged) + Q_PROPERTY(bool userGroupSharingEnabled READ userGroupSharingEnabled NOTIFY userGroupSharingEnabledChanged) + Q_PROPERTY(bool canShare READ canShare NOTIFY sharePermissionsChanged) + Q_PROPERTY(bool fetchOngoing READ fetchOngoing NOTIFY fetchOngoingChanged) + Q_PROPERTY(bool hasInitialShareFetchCompleted READ hasInitialShareFetchCompleted NOTIFY hasInitialShareFetchCompletedChanged) + +public: + enum Roles { + ShareRole = Qt::UserRole + 1, + ShareTypeRole, + ShareIdRole, + IconUrlRole, + AvatarUrlRole, + LinkRole, + LinkShareNameRole, + LinkShareLabelRole, + NoteEnabledRole, + NoteRole, + ExpireDateEnabledRole, + ExpireDateEnforcedRole, + ExpireDateRole, + EnforcedMaximumExpireDateRole, + PasswordProtectEnabledRole, + PasswordRole, + PasswordEnforcedRole, + EditingAllowedRole, + }; + Q_ENUM(Roles) + + /** + * Possible share types + * Need to be in sync with Share::ShareType. + * We use this in QML. + */ + enum ShareType { + ShareTypeUser = Share::TypeUser, + ShareTypeGroup = Share::TypeGroup, + ShareTypeLink = Share::TypeLink, + ShareTypeEmail = Share::TypeEmail, + ShareTypeRemote = Share::TypeRemote, + ShareTypeCircle = Share::TypeCircle, + ShareTypeRoom = Share::TypeRoom, + ShareTypePlaceholderLink = Share::TypePlaceholderLink, + }; + Q_ENUM(ShareType); + + explicit ShareModel(QObject *parent = nullptr); + + QVariant data(const QModelIndex &index, const int role) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QHash roleNames() const override; + + AccountState *accountState() const; + QString localPath() const; + + bool accountConnected() const; + bool sharingEnabled() const; + bool publicLinkSharesEnabled() const; + bool userGroupSharingEnabled() const; + bool canShare() const; + + bool fetchOngoing() const; + bool hasInitialShareFetchCompleted() const; + +signals: + void localPathChanged(); + void accountStateChanged(); + void accountConnectedChanged(); + void sharingEnabledChanged(); + void publicLinkSharesEnabledChanged(); + void userGroupSharingEnabledChanged(); + void sharePermissionsChanged(); + void lockExpireStringChanged(); + void fetchOngoingChanged(); + void hasInitialShareFetchCompletedChanged(); + + void serverError(const int code, const QString &message); + void passwordSetError(const QString &shareId); + void requestPasswordForLinkShare(); + void requestPasswordForEmailSharee(const ShareePtr &sharee); + + void sharesChanged(); + +public slots: + void setAccountState(AccountState *accountState); + void setLocalPath(const QString &localPath); + + void createNewLinkShare() const; + void createNewLinkShareWithPassword(const QString &password) const; + void createNewUserGroupShare(const ShareePtr &sharee); + void createNewUserGroupShareFromQml(const QVariant &sharee); + void createNewUserGroupShareWithPassword(const ShareePtr &sharee, const QString &password) const; + void createNewUserGroupShareWithPasswordFromQml(const QVariant &sharee, const QString &password) const; + + void deleteShare(const SharePtr &share) const; + void deleteShareFromQml(const QVariant &share) const; + + void toggleShareAllowEditing(const SharePtr &share, const bool enable) const; + void toggleShareAllowEditingFromQml(const QVariant &share, const bool enable) const; + void toggleShareAllowResharing(const SharePtr &share, const bool enable) const; + void toggleShareAllowResharingFromQml(const QVariant &share, const bool enable) const; + void toggleSharePasswordProtect(const SharePtr &share, const bool enable); + void toggleSharePasswordProtectFromQml(const QVariant &share, const bool enable); + void toggleShareExpirationDate(const SharePtr &share, const bool enable) const; + void toggleShareExpirationDateFromQml(const QVariant &share, const bool enable) const; + void toggleShareNoteToRecipient(const SharePtr &share, const bool enable) const; + void toggleShareNoteToRecipientFromQml(const QVariant &share, const bool enable) const; + + void setLinkShareLabel(const QSharedPointer &linkShare, const QString &label) const; + void setLinkShareLabelFromQml(const QVariant &linkShare, const QString &label) const; + void setShareExpireDate(const SharePtr &share, const qint64 milliseconds) const; + // Needed as ints in QML are 32 bits so we need to use a QVariant + void setShareExpireDateFromQml(const QVariant &share, const QVariant milliseconds) const; + void setSharePassword(const SharePtr &share, const QString &password); + void setSharePasswordFromQml(const QVariant &share, const QString &password); + void setShareNote(const SharePtr &share, const QString ¬e) const; + void setShareNoteFromQml(const QVariant &share, const QString ¬e) const; + +private slots: + void resetData(); + void updateData(); + void initShareManager(); + + void slotPropfindReceived(const QVariantMap &result); + void slotServerError(const int code, const QString &message); + void slotAddShare(const SharePtr &share); + void slotRemoveShareWithId(const QString &shareId); + void slotSharesFetched(const QList &shares); + + void slotSharePermissionsSet(const QString &shareId); + void slotSharePasswordSet(const QString &shareId); + void slotShareNoteSet(const QString &shareId); + void slotShareNameSet(const QString &shareId); + void slotShareLabelSet(const QString &shareId); + void slotShareExpireDateSet(const QString &shareId); + +private: + QString displayStringForShare(const SharePtr &share) const; + QString iconUrlForShare(const SharePtr &share) const; + QString avatarUrlForShare(const SharePtr &share) const; + long long enforcedMaxExpireDateForShare(const SharePtr &share) const; + bool expireDateEnforcedForShare(const SharePtr &share) const; + + bool _fetchOngoing = false; + bool _hasInitialShareFetchCompleted = false; + SharePtr _placeholderLinkShare; + + // DO NOT USE QSHAREDPOINTERS HERE. + // QSharedPointers MUST NOT be used with pointers already assigned to other shared pointers. + // This is because they do not share reference counters, and as such are not aware of another + // smart pointer's use of the same object. + // + // We cannot pass objects instantiated in QML using smart pointers through the property interface + // so we have to pass the pointer here. If we kill the dialog using a smart pointer then + // these objects will be deallocated for the entire application. We do not want that!! + AccountState *_accountState; + Folder *_folder; + + QString _localPath; + QString _sharePath; + SharePermissions _maxSharingPermissions; + QByteArray _numericFileId; + SyncJournalFileLockInfo _filelockState; + QString _privateLinkUrl; + + QSharedPointer _manager; + + QVector _shares; + QHash _shareIdIndexHash; + QHash _shareIdRecentlySetPasswords; +}; + +} // namespace OCC diff --git a/src/gui/filedetails/sortedsharemodel.cpp b/src/gui/filedetails/sortedsharemodel.cpp new file mode 100644 index 000000000..9906cfc57 --- /dev/null +++ b/src/gui/filedetails/sortedsharemodel.cpp @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2022 by Claudio Cambra + * + * 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. + */ + +#include "sortedsharemodel.h" + +namespace OCC { + +Q_LOGGING_CATEGORY(lcSortedShareModel, "com.nextcloud.sortedsharemodel") + +SortedShareModel::SortedShareModel(QObject *parent) + : QSortFilterProxyModel(parent) +{ +} + +void SortedShareModel::sortModel() +{ + sort(0); +} + +ShareModel *SortedShareModel::shareModel() const +{ + return qobject_cast(sourceModel()); +} + +void SortedShareModel::setShareModel(ShareModel *shareModel) +{ + const auto currentSetModel = sourceModel(); + + if(currentSetModel) { + disconnect(currentSetModel, &ShareModel::rowsInserted, this, &SortedShareModel::sortModel); + disconnect(currentSetModel, &ShareModel::rowsMoved, this, &SortedShareModel::sortModel); + disconnect(currentSetModel, &ShareModel::rowsRemoved, this, &SortedShareModel::sortModel); + disconnect(currentSetModel, &ShareModel::dataChanged, this, &SortedShareModel::sortModel); + disconnect(currentSetModel, &ShareModel::modelReset, this, &SortedShareModel::sortModel); + } + + // Re-sort model when any changes take place + connect(shareModel, &ShareModel::rowsInserted, this, &SortedShareModel::sortModel); + connect(shareModel, &ShareModel::rowsMoved, this, &SortedShareModel::sortModel); + connect(shareModel, &ShareModel::rowsRemoved, this, &SortedShareModel::sortModel); + connect(shareModel, &ShareModel::dataChanged, this, &SortedShareModel::sortModel); + connect(shareModel, &ShareModel::modelReset, this, &SortedShareModel::sortModel); + + setSourceModel(shareModel); + sortModel(); + Q_EMIT shareModelChanged(); +} + +bool SortedShareModel::lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const +{ + if (!sourceLeft.isValid() || !sourceRight.isValid()) { + return false; + } + + const auto leftShare = sourceLeft.data(ShareModel::ShareRole).value(); + const auto rightShare = sourceRight.data(ShareModel::ShareRole).value(); + + if (leftShare.isNull() || rightShare.isNull()) { + return false; + } + + const auto leftShareType = leftShare->getShareType(); + + // Placeholder link shares always go at top + if(leftShareType == Share::TypePlaceholderLink) { + return true; + } + + const auto rightShareType = rightShare->getShareType(); + + // We want to place link shares at the top + if (leftShareType == Share::TypeLink && rightShareType != Share::TypeLink) { + return true; + } else if (rightShareType == Share::TypeLink && leftShareType != Share::TypeLink) { + return false; + } else if (leftShareType != rightShareType) { + return leftShareType < rightShareType; + } + + if (leftShareType == Share::TypeLink) { + const auto leftLinkShare = leftShare.objectCast(); + const auto rightLinkShare = rightShare.objectCast(); + + if(leftLinkShare.isNull() || rightLinkShare.isNull()) { + qCWarning(lcSortedShareModel) << "One of compared shares is a null pointer after conversion despite having same share type. Left link share is null:" << leftLinkShare.isNull() + << "Right link share is null: " << rightLinkShare.isNull(); + return false; + } + + return leftLinkShare->getLabel() < rightLinkShare->getLabel(); + + } else if (leftShare->getShareWith()) { + if(rightShare->getShareWith().isNull()) { + return true; + } + + return leftShare->getShareWith()->format() < rightShare->getShareWith()->format(); + } + + return false; +} + +} // namespace OCC diff --git a/src/gui/filedetails/sortedsharemodel.h b/src/gui/filedetails/sortedsharemodel.h new file mode 100644 index 000000000..c5cdbb975 --- /dev/null +++ b/src/gui/filedetails/sortedsharemodel.h @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2022 by Claudio Cambra + * + * 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. + */ + +#pragma once + +#include +#include "sharemodel.h" + +namespace OCC { + +class SortedShareModel : public QSortFilterProxyModel +{ + Q_OBJECT + Q_PROPERTY(ShareModel* shareModel READ shareModel WRITE setShareModel NOTIFY shareModelChanged) + +public: + explicit SortedShareModel(QObject *parent = nullptr); + + ShareModel *shareModel() const; + +signals: + void shareModelChanged(); + +public slots: + void setShareModel(ShareModel *shareModel); + +protected: + bool lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const override; + +private slots: + void sortModel(); +}; + +} // namespace OCC diff --git a/src/gui/folderman.cpp b/src/gui/folderman.cpp index 8d57a72b8..b71a9593d 100644 --- a/src/gui/folderman.cpp +++ b/src/gui/folderman.cpp @@ -24,6 +24,7 @@ #include "filesystem.h" #include "lockwatcher.h" #include "common/asserts.h" +#include "gui/systray.h" #include #include diff --git a/src/gui/owncloudgui.cpp b/src/gui/owncloudgui.cpp index ab96fe831..5ef4b7e4c 100644 --- a/src/gui/owncloudgui.cpp +++ b/src/gui/owncloudgui.cpp @@ -29,11 +29,12 @@ #include "owncloudsetupwizard.h" #include "progressdispatcher.h" #include "settingsdialog.h" -#include "sharedialog.h" #include "theme.h" #include "wheelhandler.h" -#include "common/syncjournalfilerecord.h" -#include "creds/abstractcredentials.h" +#include "filedetails/filedetails.h" +#include "filedetails/shareemodel.h" +#include "filedetails/sharemodel.h" +#include "filedetails/sortedsharemodel.h" #include "tray/sortedactivitylistmodel.h" #include "tray/syncstatussummary.h" #include "tray/unifiedsearchresultslistmodel.h" @@ -97,11 +98,6 @@ ownCloudGui::ownCloudGui(Application *parent) connect(_tray.data(), &Systray::shutdown, this, &ownCloudGui::slotShutdown); - connect(_tray.data(), &Systray::openShareDialog, - this, [=](const QString &sharePath, const QString &localPath) { - slotShowShareDialog(sharePath, localPath, ShareDialogStartPage::UsersAndGroups); - }); - ProgressDispatcher *pd = ProgressDispatcher::instance(); connect(pd, &ProgressDispatcher::progressInfo, this, &ownCloudGui::slotUpdateProgress); @@ -125,6 +121,10 @@ ownCloudGui::ownCloudGui(Application *parent) qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "SortedActivityListModel"); qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "WheelHandler"); qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "CallStateChecker"); + qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "FileDetails"); + qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "ShareModel"); + qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "ShareeModel"); + qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "SortedShareModel"); qmlRegisterUncreatableType("com.nextcloud.desktopclient", 1, 0, "UnifiedSearchResultsListModel", "UnifiedSearchResultsListModel"); qmlRegisterUncreatableType("com.nextcloud.desktopclient", 1, 0, "UserStatus", "Access to Status enum"); @@ -134,6 +134,8 @@ ownCloudGui::ownCloudGui(Application *parent) qRegisterMetaType("ActivityListModel*"); qRegisterMetaType("UnifiedSearchResultsListModel*"); qRegisterMetaType("UserStatus"); + qRegisterMetaType("SharePtr"); + qRegisterMetaType("ShareePtr"); qmlRegisterSingletonInstance("com.nextcloud.desktopclient", 1, 0, "UserModel", UserModel::instance()); qmlRegisterSingletonInstance("com.nextcloud.desktopclient", 1, 0, "UserAppsModel", UserAppsModel::instance()); @@ -196,12 +198,8 @@ void ownCloudGui::slotTrayClicked(QSystemTrayIcon::ActivationReason reason) } else if (reason == QSystemTrayIcon::Trigger) { if (OwncloudSetupWizard::bringWizardToFrontIfVisible()) { // brought wizard to front - } else if (_shareDialogs.size() > 0) { - // Share dialog(s) be hidden by other apps, bring them back - Q_FOREACH (const QPointer &shareDialog, _shareDialogs) { - Q_ASSERT(shareDialog.data()); - raiseDialog(shareDialog); - } + } else if (_tray->raiseDialogs()) { + // Brings dialogs hidden by other apps to front, returns true if any raised } else if (_tray->isOpen()) { _tray->hideWindow(); } else { @@ -652,54 +650,14 @@ void ownCloudGui::raiseDialog(QWidget *raiseWidget) } -void ownCloudGui::slotShowShareDialog(const QString &sharePath, const QString &localPath, ShareDialogStartPage startPage) +void ownCloudGui::slotShowShareDialog(const QString &localPath) const { - const auto folder = FolderMan::instance()->folderForPath(localPath); - if (!folder) { - qCWarning(lcApplication) << "Could not open share dialog for" << localPath << "no responsible folder found"; - return; - } - - const auto accountState = folder->accountState(); - - const QString file = localPath.mid(folder->cleanPath().length() + 1); - SyncJournalFileRecord fileRecord; - - bool resharingAllowed = true; // lets assume the good - if (folder->journalDb()->getFileRecord(file, &fileRecord) && fileRecord.isValid()) { - // check the permission: Is resharing allowed? - if (!fileRecord._remotePerm.isNull() && !fileRecord._remotePerm.hasPermission(RemotePermissions::CanReshare)) { - resharingAllowed = false; - } - } - - auto maxSharingPermissions = resharingAllowed? SharePermissions(accountState->account()->capabilities().shareDefaultPermissions()) : SharePermissions({}); - - ShareDialog *w = nullptr; - if (_shareDialogs.contains(localPath) && _shareDialogs[localPath]) { - qCInfo(lcApplication) << "Raising share dialog" << sharePath << localPath; - w = _shareDialogs[localPath]; - } else { - qCInfo(lcApplication) << "Opening share dialog" << sharePath << localPath << maxSharingPermissions; - w = new ShareDialog(accountState, sharePath, localPath, maxSharingPermissions, fileRecord.numericFileId(), fileRecord._lockstate, startPage); - w->setAttribute(Qt::WA_DeleteOnClose, true); - - _shareDialogs[localPath] = w; - connect(w, &QObject::destroyed, this, &ownCloudGui::slotRemoveDestroyedShareDialogs); - } - raiseDialog(w); + _tray->createShareDialog(localPath); } -void ownCloudGui::slotRemoveDestroyedShareDialogs() +void ownCloudGui::slotShowFileActivityDialog(const QString &localPath) const { - QMutableMapIterator> it(_shareDialogs); - while (it.hasNext()) { - it.next(); - if (!it.value() || it.value() == sender()) { - it.remove(); - } - } + _tray->createFileActivityDialog(localPath); } - } // end namespace diff --git a/src/gui/owncloudgui.h b/src/gui/owncloudgui.h index 3ffa57cd1..7b40d520d 100644 --- a/src/gui/owncloudgui.h +++ b/src/gui/owncloudgui.h @@ -100,14 +100,11 @@ public slots: /** * Open a share dialog for a file or folder. * - * sharePath is the full remote path to the item, * localPath is the absolute local path to it (so not relative * to the folder). */ - void slotShowShareDialog(const QString &sharePath, const QString &localPath, ShareDialogStartPage startPage); - - void slotRemoveDestroyedShareDialogs(); - + void slotShowShareDialog(const QString &localPath) const; + void slotShowFileActivityDialog(const QString &localPath) const; void slotNewAccountWizard(); private slots: @@ -123,8 +120,6 @@ private: QDBusConnection _bus; #endif - QMap> _shareDialogs; - QAction *_actionNewAccountWizard; QAction *_actionSettings; QAction *_actionEstimate; diff --git a/src/gui/sharedialog.cpp b/src/gui/sharedialog.cpp deleted file mode 100644 index 99a0b8356..000000000 --- a/src/gui/sharedialog.cpp +++ /dev/null @@ -1,494 +0,0 @@ -/* - * Copyright (C) by Roeland Jago Douma - * - * 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. - */ - -#include "ui_sharedialog.h" -#include "sharedialog.h" -#include "sharee.h" -#include "sharelinkwidget.h" -#include "internallinkwidget.h" -#include "shareusergroupwidget.h" -#include "passwordinputdialog.h" - -#include "sharemanager.h" - -#include "account.h" -#include "accountstate.h" -#include "configfile.h" -#include "theme.h" -#include "thumbnailjob.h" -#include "wordlist.h" - -#include -#include -#include -#include -#include -#include -#include - -namespace { -QString createRandomPassword() -{ - const auto words = OCC::WordList::getRandomWords(10); - - const auto addFirstLetter = [](const QString ¤t, const QString &next) -> QString { - return current + next.at(0); - }; - - return std::accumulate(std::cbegin(words), std::cend(words), QString(), addFirstLetter); -} -} - - -namespace OCC { - -static const int thumbnailSize = 40; - -ShareDialog::ShareDialog(QPointer accountState, - const QString &sharePath, - const QString &localPath, - SharePermissions maxSharingPermissions, - const QByteArray &numericFileId, - SyncJournalFileLockInfo filelockState, - ShareDialogStartPage startPage, - QWidget *parent) - : QDialog(parent) - , _ui(new Ui::ShareDialog) - , _accountState(accountState) - , _sharePath(sharePath) - , _localPath(localPath) - , _maxSharingPermissions(maxSharingPermissions) - , _filelockState(std::move(filelockState)) - , _privateLinkUrl(accountState->account()->deprecatedPrivateLinkUrl(numericFileId).toString(QUrl::FullyEncoded)) - , _startPage(startPage) -{ - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - setAttribute(Qt::WA_DeleteOnClose); - setObjectName("SharingDialog"); // required as group for saveGeometry call - - _ui->setupUi(this); - - // We want to act on account state changes - connect(_accountState.data(), &AccountState::stateChanged, this, &ShareDialog::slotAccountStateChanged); - - // Set icon - QFileInfo f_info(_localPath); - QFileIconProvider icon_provider; - QIcon icon = icon_provider.icon(f_info); - auto pixmap = icon.pixmap(thumbnailSize, thumbnailSize); - if (pixmap.width() > 0) { - _ui->label_icon->setPixmap(pixmap); - } - - // Set filename - QString fileName = QFileInfo(_sharePath).fileName(); - _ui->label_name->setText(tr("%1").arg(fileName)); - QFont f(_ui->label_name->font()); - f.setPointSize(qRound(f.pointSize() * 1.4)); - _ui->label_name->setFont(f); - - if (_filelockState._locked) { - static constexpr auto SECONDS_PER_MINUTE = 60; - const auto lockExpirationTime = _filelockState._lockTime + _filelockState._lockTimeout; - const auto remainingTime = QDateTime::currentDateTime().secsTo(QDateTime::fromSecsSinceEpoch(lockExpirationTime)); - const auto remainingTimeInMinute = static_cast(remainingTime > 0 ? remainingTime / SECONDS_PER_MINUTE : 0); - _ui->label_lockinfo->setText(tr("Locked by %1 - Expires in %2 minutes", "remaining time before lock expires", remainingTimeInMinute).arg(_filelockState._lockOwnerDisplayName).arg(remainingTimeInMinute)); - } else { - _ui->label_lockinfo->setVisible(false); - } - - QString ocDir(_sharePath); - ocDir.truncate(ocDir.length() - fileName.length()); - - ocDir.replace(QRegularExpression("^/*"), ""); - ocDir.replace(QRegularExpression("/*$"), ""); - - // Laying this out is complex because sharePath - // may be in use or not. - _ui->gridLayout->removeWidget(_ui->label_sharePath); - _ui->gridLayout->removeWidget(_ui->label_name); - if (ocDir.isEmpty()) { - _ui->gridLayout->addWidget(_ui->label_name, 0, 1, 2, 1); - _ui->label_sharePath->setText(QString()); - } else { - _ui->gridLayout->addWidget(_ui->label_name, 0, 1, 1, 1); - _ui->gridLayout->addWidget(_ui->label_sharePath, 1, 1, 1, 1); - _ui->label_sharePath->setText(tr("Folder: %2").arg(ocDir)); - } - - this->setWindowTitle(tr("%1 Sharing").arg(Theme::instance()->appNameGUI())); - - if (!accountState->account()->capabilities().shareAPI()) { - return; - } - - if (QFileInfo(_localPath).isFile()) { - auto *job = new ThumbnailJob(_sharePath, _accountState->account(), this); - connect(job, &ThumbnailJob::jobFinished, this, &ShareDialog::slotThumbnailFetched); - job->start(); - } - - auto job = new PropfindJob(accountState->account(), _sharePath); - job->setProperties( - QList() - << "http://open-collaboration-services.org/ns:share-permissions" - << "http://owncloud.org/ns:fileid" // numeric file id for fallback private link generation - << "http://owncloud.org/ns:privatelink"); - job->setTimeout(10 * 1000); - connect(job, &PropfindJob::result, this, &ShareDialog::slotPropfindReceived); - connect(job, &PropfindJob::finishedWithError, this, &ShareDialog::slotPropfindError); - job->start(); - - initShareManager(); - - _scrollAreaViewPort = new QWidget(_ui->scrollArea); - _scrollAreaLayout = new QVBoxLayout(_scrollAreaViewPort); - _scrollAreaLayout->setContentsMargins(0, 0, 0, 0); - _ui->scrollArea->setWidget(_scrollAreaViewPort); - - _internalLinkWidget = new InternalLinkWidget(localPath, this); - _ui->verticalLayout->addWidget(_internalLinkWidget); - _internalLinkWidget->setupUiOptions(); - connect(this, &ShareDialog::styleChanged, _internalLinkWidget, &InternalLinkWidget::slotStyleChanged); - - adjustScrollWidget(); -} - -ShareLinkWidget *ShareDialog::addLinkShareWidget(const QSharedPointer &linkShare) -{ - const auto linkShareWidget = new ShareLinkWidget(_accountState->account(), _sharePath, _localPath, _maxSharingPermissions, _ui->scrollArea); - _linkWidgetList.append(linkShareWidget); - - linkShareWidget->setLinkShare(linkShare); - - connect(linkShare.data(), &Share::serverError, linkShareWidget, &ShareLinkWidget::slotServerError); - connect(linkShare.data(), &Share::shareDeleted, linkShareWidget, &ShareLinkWidget::slotDeleteShareFetched); - - if(_manager) { - connect(_manager, &ShareManager::serverError, linkShareWidget, &ShareLinkWidget::slotServerError); - } - - // Connect all shares signals to gui slots - connect(this, &ShareDialog::toggleShareLinkAnimation, linkShareWidget, &ShareLinkWidget::slotToggleShareLinkAnimation); - connect(linkShareWidget, &ShareLinkWidget::createLinkShare, this, &ShareDialog::slotCreateLinkShare); - connect(linkShareWidget, &ShareLinkWidget::deleteLinkShare, this, &ShareDialog::slotDeleteShare); - connect(linkShareWidget, &ShareLinkWidget::createPassword, this, &ShareDialog::slotCreatePasswordForLinkShare); - - // Connect styleChanged events to our widget, so it can adapt (Dark-/Light-Mode switching) - connect(this, &ShareDialog::styleChanged, linkShareWidget, &ShareLinkWidget::slotStyleChanged); - - _ui->verticalLayout->insertWidget(_linkWidgetList.size() + 1, linkShareWidget); - _scrollAreaLayout->addWidget(linkShareWidget); - - linkShareWidget->setupUiOptions(); - adjustScrollWidget(); - - return linkShareWidget; -} - -void ShareDialog::initLinkShareWidget() -{ - if(_linkWidgetList.size() == 0) { - _emptyShareLinkWidget = new ShareLinkWidget(_accountState->account(), _sharePath, _localPath, _maxSharingPermissions, _ui->scrollArea); - _linkWidgetList.append(_emptyShareLinkWidget); - - _emptyShareLinkWidget->slotStyleChanged(); // Get the initial customizeStyle() to happen - - connect(this, &ShareDialog::toggleShareLinkAnimation, _emptyShareLinkWidget, &ShareLinkWidget::slotToggleShareLinkAnimation); - connect(this, &ShareDialog::styleChanged, _emptyShareLinkWidget, &ShareLinkWidget::slotStyleChanged); - - connect(_emptyShareLinkWidget, &ShareLinkWidget::createLinkShare, this, &ShareDialog::slotCreateLinkShare); - connect(_emptyShareLinkWidget, &ShareLinkWidget::createPassword, this, &ShareDialog::slotCreatePasswordForLinkShare); - - _ui->verticalLayout->insertWidget(_linkWidgetList.size()+1, _emptyShareLinkWidget); - _scrollAreaLayout->addWidget(_emptyShareLinkWidget); - _emptyShareLinkWidget->show(); - } else if (_emptyShareLinkWidget) { - _emptyShareLinkWidget->hide(); - _ui->verticalLayout->removeWidget(_emptyShareLinkWidget); - _linkWidgetList.removeAll(_emptyShareLinkWidget); - _emptyShareLinkWidget = nullptr; - } - - adjustScrollWidget(); -} - -void ShareDialog::slotAddLinkShareWidget(const QSharedPointer &linkShare) -{ - emit toggleShareLinkAnimation(true); - const auto addedLinkShareWidget = addLinkShareWidget(linkShare); - initLinkShareWidget(); - if (linkShare->isPasswordSet()) { - addedLinkShareWidget->focusPasswordLineEdit(); - } - emit toggleShareLinkAnimation(false); -} - -void ShareDialog::slotSharesFetched(const QList> &shares) -{ - emit toggleShareLinkAnimation(true); - - const QString versionString = _accountState->account()->serverVersion(); - qCInfo(lcSharing) << versionString << "Fetched" << shares.count() << "shares"; - - foreach (auto share, shares) { - if (share->getShareType() != Share::TypeLink || share->getUidOwner() != share->account()->davUser()) { - continue; - } - - QSharedPointer linkShare = qSharedPointerDynamicCast(share); - addLinkShareWidget(linkShare); - } - - initLinkShareWidget(); - emit toggleShareLinkAnimation(false); -} - -void ShareDialog::adjustScrollWidget() -{ - _ui->scrollArea->setVisible(_scrollAreaLayout->count() > 0); - - // Sometimes the contentRect returns a height of 0, so we need a backup plan - const auto scrollAreaContentHeight = _scrollAreaLayout->contentsRect().height(); - - auto linkWidgetHeights = 0; - - if(scrollAreaContentHeight == 0 && !_linkWidgetList.empty()) { - for (const auto linkWidget : _linkWidgetList) { - linkWidgetHeights += linkWidget->height() - 10; - } - } - - const auto overAvailableHeight = scrollAreaContentHeight > _ui->scrollArea->height() || - linkWidgetHeights > _ui->scrollArea->height(); - - _ui->scrollArea->setFrameShape(overAvailableHeight ? QFrame::StyledPanel : QFrame::NoFrame); - _ui->verticalLayout->setSpacing(overAvailableHeight ? 10 : 0); -} - -ShareDialog::~ShareDialog() -{ - _linkWidgetList.clear(); - delete _ui; -} - -void ShareDialog::done(int r) -{ - ConfigFile cfg; - cfg.saveGeometry(this); - QDialog::done(r); -} - -void ShareDialog::slotPropfindReceived(const QVariantMap &result) -{ - const QVariant receivedPermissions = result["share-permissions"]; - if (!receivedPermissions.toString().isEmpty()) { - _maxSharingPermissions = static_cast(receivedPermissions.toInt()); - qCInfo(lcSharing) << "Received sharing permissions for" << _sharePath << _maxSharingPermissions; - } - auto privateLinkUrl = result["privatelink"].toString(); - auto numericFileId = result["fileid"].toByteArray(); - if (!privateLinkUrl.isEmpty()) { - qCInfo(lcSharing) << "Received private link url for" << _sharePath << privateLinkUrl; - _privateLinkUrl = privateLinkUrl; - } else if (!numericFileId.isEmpty()) { - qCInfo(lcSharing) << "Received numeric file id for" << _sharePath << numericFileId; - _privateLinkUrl = _accountState->account()->deprecatedPrivateLinkUrl(numericFileId).toString(QUrl::FullyEncoded); - } - - showSharingUi(); -} - -void ShareDialog::slotPropfindError() -{ - // On error show the share ui anyway. The user can still see shares, - // delete them and so on, even though adding new shares or granting - // some of the permissions might fail. - - showSharingUi(); -} - -void ShareDialog::showSharingUi() -{ - auto theme = Theme::instance(); - - // There's no difference between being unable to reshare and - // being unable to reshare with reshare permission. - bool canReshare = _maxSharingPermissions & SharePermissionShare; - - if (!canReshare) { - auto label = new QLabel(this); - label->setText(tr("The file cannot be shared because it does not have sharing permission.")); - label->setWordWrap(true); - _ui->verticalLayout->insertWidget(1, label); - return; - } - - if (theme->userGroupSharing()) { - _userGroupWidget = new ShareUserGroupWidget(_accountState->account(), _sharePath, _localPath, _maxSharingPermissions, _privateLinkUrl, _ui->scrollArea); - _userGroupWidget->getShares(); - - // Connect styleChanged events to our widget, so it can adapt (Dark-/Light-Mode switching) - connect(this, &ShareDialog::styleChanged, _userGroupWidget, &ShareUserGroupWidget::slotStyleChanged); - - _userGroupWidget->slotStyleChanged(); - - _ui->verticalLayout->insertWidget(1, _userGroupWidget); - _scrollAreaLayout->addLayout(_userGroupWidget->shareUserGroupLayout()); - } - - initShareManager(); - - if (theme->linkSharing()) { - if(_manager) { - _manager->fetchShares(_sharePath); - } - } - - adjustScrollWidget(); -} - -void ShareDialog::initShareManager() -{ - bool sharingPossible = true; - if (!_accountState->account()->capabilities().sharePublicLink()) { - qCWarning(lcSharing) << "Link shares have been disabled"; - sharingPossible = false; - } else if (!(_maxSharingPermissions & SharePermissionShare)) { - qCWarning(lcSharing) << "The file cannot be shared because it does not have sharing permission."; - sharingPossible = false; - } - - if (!_manager && sharingPossible) { - _manager = new ShareManager(_accountState->account(), this); - connect(_manager, &ShareManager::sharesFetched, this, &ShareDialog::slotSharesFetched); - connect(_manager, &ShareManager::linkShareCreated, this, &ShareDialog::slotAddLinkShareWidget); - connect(_manager, &ShareManager::linkShareRequiresPassword, this, &ShareDialog::slotLinkShareRequiresPassword); - } -} - -void ShareDialog::slotCreateLinkShare() -{ - if(_manager) { - const auto askOptionalPassword = _accountState->account()->capabilities().sharePublicLinkAskOptionalPassword(); - const auto password = askOptionalPassword ? createRandomPassword() : QString(); - _manager->createLinkShare(_sharePath, QString(), password); - } -} - -void ShareDialog::slotCreatePasswordForLinkShare(const QString &password) -{ - const auto shareLinkWidget = qobject_cast(sender()); - Q_ASSERT(shareLinkWidget); - if (shareLinkWidget) { - connect(_manager, &ShareManager::linkShareRequiresPassword, shareLinkWidget, &ShareLinkWidget::slotCreateShareRequiresPassword); - connect(shareLinkWidget, &ShareLinkWidget::createPasswordProcessed, this, &ShareDialog::slotCreatePasswordForLinkShareProcessed); - shareLinkWidget->getLinkShare()->setPassword(password); - } else { - qCCritical(lcSharing) << "shareLinkWidget is not a sender!"; - } -} - -void ShareDialog::slotCreatePasswordForLinkShareProcessed() -{ - const auto shareLinkWidget = qobject_cast(sender()); - Q_ASSERT(shareLinkWidget); - if (shareLinkWidget) { - disconnect(_manager, &ShareManager::linkShareRequiresPassword, shareLinkWidget, &ShareLinkWidget::slotCreateShareRequiresPassword); - disconnect(shareLinkWidget, &ShareLinkWidget::createPasswordProcessed, this, &ShareDialog::slotCreatePasswordForLinkShareProcessed); - } else { - qCCritical(lcSharing) << "shareLinkWidget is not a sender!"; - } -} - -void ShareDialog::slotLinkShareRequiresPassword(const QString &message) -{ - const auto passwordInputDialog = new PasswordInputDialog(tr("Please enter a password for your link share:"), message, this); - passwordInputDialog->setWindowTitle(tr("Password for share required")); - passwordInputDialog->setAttribute(Qt::WA_DeleteOnClose); - passwordInputDialog->open(); - - connect(passwordInputDialog, &QDialog::finished, this, [this, passwordInputDialog](const int result) { - if (result == QDialog::Accepted && _manager) { - // Try to create the link share again with the newly entered password - _manager->createLinkShare(_sharePath, QString(), passwordInputDialog->password()); - return; - } - emit toggleShareLinkAnimation(false); - }); -} - -void ShareDialog::slotDeleteShare() -{ - auto sharelinkWidget = dynamic_cast(sender()); - sharelinkWidget->hide(); - _ui->verticalLayout->removeWidget(sharelinkWidget); - _scrollAreaLayout->removeWidget(sharelinkWidget); - _linkWidgetList.removeAll(sharelinkWidget); - initLinkShareWidget(); -} - -void ShareDialog::slotThumbnailFetched(const int &statusCode, const QByteArray &reply) -{ - if (statusCode != 200) { - qCWarning(lcSharing) << "Thumbnail status code: " << statusCode; - return; - } - - QPixmap p; - p.loadFromData(reply, "PNG"); - p = p.scaledToHeight(thumbnailSize, Qt::SmoothTransformation); - _ui->label_icon->setPixmap(p); - _ui->label_icon->show(); -} - -void ShareDialog::slotAccountStateChanged(int state) -{ - bool enabled = (state == AccountState::State::Connected); - qCDebug(lcSharing) << "Account connected?" << enabled; - - if (_userGroupWidget) { - _userGroupWidget->setEnabled(enabled); - } - - if(_linkWidgetList.size() > 0){ - foreach(ShareLinkWidget *widget, _linkWidgetList){ - widget->setEnabled(state); - } - } -} - -void ShareDialog::changeEvent(QEvent *e) -{ - switch (e->type()) { - case QEvent::StyleChange: - case QEvent::PaletteChange: - case QEvent::ThemeChange: - // Notify the other widgets (Dark-/Light-Mode switching) - emit styleChanged(); - break; - default: - break; - } - - QDialog::changeEvent(e); -} - -void ShareDialog::resizeEvent(QResizeEvent *event) -{ - adjustScrollWidget(); - QDialog::resizeEvent(event); -} - -} // namespace OCC diff --git a/src/gui/sharedialog.h b/src/gui/sharedialog.h deleted file mode 100644 index 89bb2ef70..000000000 --- a/src/gui/sharedialog.h +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (C) by Roeland Jago Douma - * - * 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. - */ - -#ifndef SHAREDIALOG_H -#define SHAREDIALOG_H - -#include "accountstate.h" -#include "sharepermissions.h" -#include "owncloudgui.h" -#include "common/syncjournalfilerecord.h" - -#include -#include -#include -#include -#include - -class QProgressIndicator; -class QVBoxLayout; - -namespace OCC { - -namespace Ui { - class ShareDialog; -} - -class ShareLinkWidget; -class InternalLinkWidget; -class ShareUserGroupWidget; -class ShareManager; -class LinkShare; -class Share; - -class ShareDialog : public QDialog -{ - Q_OBJECT - -public: - explicit ShareDialog(QPointer accountState, - const QString &sharePath, - const QString &localPath, - SharePermissions maxSharingPermissions, - const QByteArray &numericFileId, - SyncJournalFileLockInfo filelockState, - ShareDialogStartPage startPage, - QWidget *parent = nullptr); - ~ShareDialog() override; - -private slots: - void done(int r) override; - void slotPropfindReceived(const QVariantMap &result); - void slotPropfindError(); - void slotThumbnailFetched(const int &statusCode, const QByteArray &reply); - void slotAccountStateChanged(int state); - - void slotSharesFetched(const QList> &shares); - void slotAddLinkShareWidget(const QSharedPointer &linkShare); - void slotDeleteShare(); - void slotCreateLinkShare(); - void slotCreatePasswordForLinkShare(const QString &password); - void slotCreatePasswordForLinkShareProcessed(); - void slotLinkShareRequiresPassword(const QString &message); - -signals: - void toggleShareLinkAnimation(bool start); - void styleChanged(); - -protected: - void changeEvent(QEvent *) override; - void resizeEvent(QResizeEvent *event) override; - -private: - void showSharingUi(); - void initShareManager(); - ShareLinkWidget *addLinkShareWidget(const QSharedPointer &linkShare); - void initLinkShareWidget(); - void adjustScrollWidget(); - - Ui::ShareDialog *_ui; - - QPointer _accountState; - QString _sharePath; - QString _localPath; - SharePermissions _maxSharingPermissions; - QByteArray _numericFileId; - SyncJournalFileLockInfo _filelockState; - QString _privateLinkUrl; - ShareDialogStartPage _startPage; - ShareManager *_manager = nullptr; - - QList _linkWidgetList; - ShareLinkWidget* _emptyShareLinkWidget = nullptr; - InternalLinkWidget* _internalLinkWidget = nullptr; - ShareUserGroupWidget *_userGroupWidget = nullptr; - QProgressIndicator *_progressIndicator = nullptr; - - QWidget *_scrollAreaViewPort = nullptr; - QVBoxLayout *_scrollAreaLayout = nullptr; -}; - -} // namespace OCC - -#endif // SHAREDIALOG_H diff --git a/src/gui/sharedialog.ui b/src/gui/sharedialog.ui deleted file mode 100644 index 8e4bbbfd8..000000000 --- a/src/gui/sharedialog.ui +++ /dev/null @@ -1,217 +0,0 @@ - - - OCC::ShareDialog - - - - 0 - 0 - 385 - 400 - - - - - 0 - 0 - - - - - 320 - 240 - - - - - 0 - - - QLayout::SetMinimumSize - - - - - 0 - - - QLayout::SetDefaultConstraint - - - - - 0 - - - 0 - - - 0 - - - 2 - - - - - - 0 - 0 - - - - - 315 - 0 - - - - share label - - - Qt::PlainText - - - true - - - - - - - - 0 - 0 - - - - - 315 - 0 - - - - TextLabel - - - Qt::PlainText - - - true - - - - - - - - 0 - 0 - - - - - 315 - 0 - - - - - false - - - - Nextcloud Path: - - - Qt::PlainText - - - true - - - - - - - - 0 - 0 - - - - - 40 - 40 - - - - - 16777215 - 16777215 - - - - Icon - - - - - - - - - - 0 - 0 - - - - - 0 - 0 - - - - QFrame::NoFrame - - - QFrame::Plain - - - Qt::ScrollBarAsNeeded - - - Qt::ScrollBarAlwaysOff - - - QAbstractScrollArea::AdjustToContentsOnFirstShow - - - true - - - - - 0 - 0 - 359 - 320 - - - - - 0 - 0 - - - - - - - - - - - - diff --git a/src/gui/sharee.cpp b/src/gui/sharee.cpp index ed447e2a8..206a9d0e6 100644 --- a/src/gui/sharee.cpp +++ b/src/gui/sharee.cpp @@ -66,160 +66,4 @@ Sharee::Type Sharee::type() const return _type; } -ShareeModel::ShareeModel(const AccountPtr &account, const QString &type, QObject *parent) - : QAbstractListModel(parent) - , _account(account) - , _type(type) -{ -} - -void ShareeModel::fetch(const QString &search, const ShareeSet &blacklist, LookupMode lookupMode) -{ - _search = search; - _shareeBlacklist = blacklist; - auto *job = new OcsShareeJob(_account); - connect(job, &OcsShareeJob::shareeJobFinished, this, &ShareeModel::shareesFetched); - connect(job, &OcsJob::ocsError, this, &ShareeModel::displayErrorMessage); - job->getSharees(_search, _type, 1, 50, lookupMode == GlobalSearch ? true : false); -} - -void ShareeModel::shareesFetched(const QJsonDocument &reply) -{ - QVector> newSharees; - - { - const QStringList shareeTypes {"users", "groups", "emails", "remotes", "circles", "rooms"}; - - const auto appendSharees = [this, &shareeTypes](const QJsonObject &data, QVector>& out) { - for (const auto &shareeType : shareeTypes) { - const auto category = data.value(shareeType).toArray(); - for (const auto &sharee : category) { - out.append(parseSharee(sharee.toObject())); - } - } - }; - - appendSharees(reply.object().value("ocs").toObject().value("data").toObject(), newSharees); - appendSharees(reply.object().value("ocs").toObject().value("data").toObject().value("exact").toObject(), newSharees); - } - - // Filter sharees that we have already shared with - QVector> filteredSharees; - foreach (const auto &sharee, newSharees) { - bool found = false; - foreach (const auto &blacklistSharee, _shareeBlacklist) { - if (sharee->type() == blacklistSharee->type() && sharee->shareWith() == blacklistSharee->shareWith()) { - found = true; - break; - } - } - - if (found == false) { - filteredSharees.append(sharee); - } - } - - setNewSharees(filteredSharees); - shareesReady(); -} - -QSharedPointer ShareeModel::parseSharee(const QJsonObject &data) -{ - QString displayName = data.value("label").toString(); - const QString shareWith = data.value("value").toObject().value("shareWith").toString(); - Sharee::Type type = (Sharee::Type)data.value("value").toObject().value("shareType").toInt(); - const QString additionalInfo = data.value("value").toObject().value("shareWithAdditionalInfo").toString(); - if (!additionalInfo.isEmpty()) { - displayName = tr("%1 (%2)", "sharee (shareWithAdditionalInfo)").arg(displayName, additionalInfo); - } - - return QSharedPointer(new Sharee(shareWith, displayName, type)); -} - - -// Helper function for setNewSharees (could be a lambda when we can use them) -static QSharedPointer shareeFromModelIndex(const QModelIndex &idx) -{ - return idx.data(Qt::UserRole).value>(); -} - -struct FindShareeHelper -{ - const QSharedPointer &sharee; - bool operator()(const QSharedPointer &s2) const - { - return s2->format() == sharee->format() && s2->displayName() == sharee->format(); - } -}; - -/* Set the new sharee - - Do that while preserving the model index so the selection stays -*/ -void ShareeModel::setNewSharees(const QVector> &newSharees) -{ - layoutAboutToBeChanged(); - const auto persistent = persistentIndexList(); - QVector> oldPersistantSharee; - oldPersistantSharee.reserve(persistent.size()); - - std::transform(persistent.begin(), persistent.end(), std::back_inserter(oldPersistantSharee), - shareeFromModelIndex); - - _sharees = newSharees; - - QModelIndexList newPersistant; - newPersistant.reserve(persistent.size()); - foreach (const QSharedPointer &sharee, oldPersistantSharee) { - FindShareeHelper helper = { sharee }; - auto it = std::find_if(_sharees.constBegin(), _sharees.constEnd(), helper); - if (it == _sharees.constEnd()) { - newPersistant << QModelIndex(); - } else { - newPersistant << index(std::distance(_sharees.constBegin(), it)); - } - } - - changePersistentIndexList(persistent, newPersistant); - layoutChanged(); -} - - -int ShareeModel::rowCount(const QModelIndex &) const -{ - return _sharees.size(); -} - -QVariant ShareeModel::data(const QModelIndex &index, int role) const -{ - if (index.row() < 0 || index.row() > _sharees.size()) { - return QVariant(); - } - - const auto &sharee = _sharees.at(index.row()); - if (role == Qt::DisplayRole) { - return sharee->format(); - - } else if (role == Qt::EditRole) { - // This role is used by the completer - it should match - // the full name and the user name and thus we include both - // in the output here. But we need to take care this string - // doesn't leak to the user. - return QString(sharee->displayName() + " (" + sharee->shareWith() + ")"); - - } else if (role == Qt::UserRole) { - return QVariant::fromValue(sharee); - } - - return QVariant(); -} - -QSharedPointer ShareeModel::getSharee(int at) -{ - if (at < 0 || at > _sharees.size()) { - return QSharedPointer(nullptr); - } - - return _sharees.at(at); -} } diff --git a/src/gui/sharee.h b/src/gui/sharee.h index b1aa8f2c2..2139a9117 100644 --- a/src/gui/sharee.h +++ b/src/gui/sharee.h @@ -61,47 +61,9 @@ private: Type _type; }; - -class ShareeModel : public QAbstractListModel -{ - Q_OBJECT -public: - enum LookupMode { - LocalSearch = 0, - GlobalSearch = 1 - }; - - explicit ShareeModel(const AccountPtr &account, const QString &type, QObject *parent = nullptr); - - using ShareeSet = QVector>; // FIXME: make it a QSet when Sharee can be compared - void fetch(const QString &search, const ShareeSet &blacklist, LookupMode lookupMode); - [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; - [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override; - - QSharedPointer getSharee(int at); - - [[nodiscard]] QString currentSearch() const { return _search; } - -signals: - void shareesReady(); - void displayErrorMessage(int code, const QString &); - -private slots: - void shareesFetched(const QJsonDocument &reply); - -private: - QSharedPointer parseSharee(const QJsonObject &data); - void setNewSharees(const QVector> &newSharees); - - AccountPtr _account; - QString _search; - QString _type; - - QVector> _sharees; - QVector> _shareeBlacklist; -}; +using ShareePtr = QSharedPointer; } -Q_DECLARE_METATYPE(QSharedPointer) +Q_DECLARE_METATYPE(OCC::ShareePtr) #endif //SHAREE_H diff --git a/src/gui/sharelinkwidget.cpp b/src/gui/sharelinkwidget.cpp deleted file mode 100644 index 3f6b437a6..000000000 --- a/src/gui/sharelinkwidget.cpp +++ /dev/null @@ -1,625 +0,0 @@ -/* - * Copyright (C) by Roeland Jago Douma - * Copyright (C) 2015 by Klaas Freitag - * - * 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. - */ - -#include "ui_sharelinkwidget.h" -#include "sharelinkwidget.h" -#include "account.h" -#include "capabilities.h" -#include "guiutility.h" -#include "sharemanager.h" -#include "theme.h" -#include "elidedlabel.h" - -#include "QProgressIndicator.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace { - const char *passwordIsSetPlaceholder = "●●●●●●●●"; -} - -namespace OCC { - -Q_LOGGING_CATEGORY(lcShareLink, "nextcloud.gui.sharelink", QtInfoMsg) - -ShareLinkWidget::ShareLinkWidget(AccountPtr account, - const QString &sharePath, - const QString &localPath, - SharePermissions maxSharingPermissions, - QWidget *parent) - : QWidget(parent) - , _ui(new Ui::ShareLinkWidget) - , _account(account) - , _sharePath(sharePath) - , _localPath(localPath) - , _linkShare(nullptr) - , _passwordRequired(false) - , _expiryRequired(false) - , _namesSupported(true) - , _noteRequired(false) - , _linkContextMenu(nullptr) - , _readOnlyLinkAction(nullptr) - , _allowEditingLinkAction(nullptr) - , _allowUploadEditingLinkAction(nullptr) - , _allowUploadLinkAction(nullptr) - , _passwordProtectLinkAction(nullptr) - , _expirationDateLinkAction(nullptr) - , _unshareLinkAction(nullptr) - , _noteLinkAction(nullptr) -{ - _ui->setupUi(this); - - _ui->shareLinkToolButton->hide(); - - //Is this a file or folder? - QFileInfo fi(localPath); - _isFile = fi.isFile(); - - connect(_ui->enableShareLink, &QPushButton::clicked, this, &ShareLinkWidget::slotCreateShareLink); - connect(_ui->lineEdit_password, &QLineEdit::returnPressed, this, &ShareLinkWidget::slotCreatePassword); - connect(_ui->confirmPassword, &QAbstractButton::clicked, this, &ShareLinkWidget::slotCreatePassword); - connect(_ui->confirmNote, &QAbstractButton::clicked, this, &ShareLinkWidget::slotCreateNote); - connect(_ui->confirmExpirationDate, &QAbstractButton::clicked, this, &ShareLinkWidget::slotSetExpireDate); - - _ui->errorLabel->hide(); - - if (!_account->capabilities().sharePublicLink()) { - qCWarning(lcShareLink) << "Link shares have been disabled"; - } else if (!(maxSharingPermissions & SharePermissionShare)) { - qCWarning(lcShareLink) << "The file can not be shared because it was shared without sharing permission."; - } - - _ui->enableShareLink->setChecked(false); - _ui->shareLinkToolButton->setEnabled(false); - _ui->shareLinkToolButton->hide(); - - // Older servers don't support multiple public link shares - if (!_account->capabilities().sharePublicLinkMultiple()) { - _namesSupported = false; - } - - togglePasswordOptions(false); - toggleExpireDateOptions(false); - toggleNoteOptions(false); - - _ui->noteProgressIndicator->setVisible(false); - _ui->passwordProgressIndicator->setVisible(false); - _ui->expirationDateProgressIndicator->setVisible(false); - _ui->sharelinkProgressIndicator->setVisible(false); - - // check if the file is already inside of a synced folder - if (sharePath.isEmpty()) { - qCWarning(lcShareLink) << "Unable to share files not in a sync folder."; - return; - } -} - -ShareLinkWidget::~ShareLinkWidget() -{ - delete _ui; -} - -void ShareLinkWidget::slotToggleShareLinkAnimation(const bool start) -{ - _ui->sharelinkProgressIndicator->setVisible(start); - if (start) { - if (!_ui->sharelinkProgressIndicator->isAnimated()) { - _ui->sharelinkProgressIndicator->startAnimation(); - } - } else { - _ui->sharelinkProgressIndicator->stopAnimation(); - } -} - -void ShareLinkWidget::toggleButtonAnimation(QToolButton *button, QProgressIndicator *progressIndicator, const QAction *checkedAction) const -{ - auto startAnimation = false; - const auto actionIsChecked = checkedAction->isChecked(); - if (!progressIndicator->isAnimated() && actionIsChecked) { - progressIndicator->startAnimation(); - startAnimation = true; - } else { - progressIndicator->stopAnimation(); - } - - button->setVisible(!startAnimation && actionIsChecked); - progressIndicator->setVisible(startAnimation && actionIsChecked); -} - -void ShareLinkWidget::setLinkShare(QSharedPointer linkShare) -{ - _linkShare = linkShare; -} - -QSharedPointer ShareLinkWidget::getLinkShare() -{ - return _linkShare; -} - -void ShareLinkWidget::focusPasswordLineEdit() -{ - _ui->lineEdit_password->setFocus(); -} - -void ShareLinkWidget::setupUiOptions() -{ - connect(_linkShare.data(), &LinkShare::noteSet, this, &ShareLinkWidget::slotNoteSet); - connect(_linkShare.data(), &LinkShare::passwordSet, this, &ShareLinkWidget::slotPasswordSet); - connect(_linkShare.data(), &LinkShare::passwordSetError, this, &ShareLinkWidget::slotPasswordSetError); - connect(_linkShare.data(), &LinkShare::labelSet, this, &ShareLinkWidget::slotLabelSet); - - // Prepare permissions check and create group action - const QDate expireDate = _linkShare.data()->getExpireDate().isValid() ? _linkShare.data()->getExpireDate() : QDate(); - const SharePermissions perm = _linkShare.data()->getPermissions(); - auto checked = false; - auto *permissionsGroup = new QActionGroup(this); - - // Prepare sharing menu - _linkContextMenu = new QMenu(this); - - // radio button style - permissionsGroup->setExclusive(true); - - if (_isFile) { - checked = (perm & SharePermissionRead) && (perm & SharePermissionUpdate); - _allowEditingLinkAction = _linkContextMenu->addAction(tr("Allow editing")); - _allowEditingLinkAction->setCheckable(true); - _allowEditingLinkAction->setChecked(checked); - - } else { - checked = (perm == SharePermissionRead); - _readOnlyLinkAction = permissionsGroup->addAction(tr("View only")); - _readOnlyLinkAction->setCheckable(true); - _readOnlyLinkAction->setChecked(checked); - - checked = (perm & SharePermissionRead) && (perm & SharePermissionCreate) - && (perm & SharePermissionUpdate) && (perm & SharePermissionDelete); - _allowUploadEditingLinkAction = permissionsGroup->addAction(tr("Allow upload and editing")); - _allowUploadEditingLinkAction->setCheckable(true); - _allowUploadEditingLinkAction->setChecked(checked); - - checked = (perm == SharePermissionCreate); - _allowUploadLinkAction = permissionsGroup->addAction(tr("File drop (upload only)")); - _allowUploadLinkAction->setCheckable(true); - _allowUploadLinkAction->setChecked(checked); - } - - _shareLinkElidedLabel = new OCC::ElidedLabel(this); - _shareLinkElidedLabel->setElideMode(Qt::ElideRight); - displayShareLinkLabel(); - _ui->horizontalLayout->insertWidget(2, _shareLinkElidedLabel); - - _shareLinkLayout = new QHBoxLayout(this); - - _shareLinkLabel = new QLabel(this); - _shareLinkLabel->setPixmap(QString(":/client/theme/black/edit.svg")); - _shareLinkLayout->addWidget(_shareLinkLabel); - - _shareLinkEdit = new QLineEdit(this); - connect(_shareLinkEdit, &QLineEdit::returnPressed, this, &ShareLinkWidget::slotCreateLabel); - _shareLinkEdit->setPlaceholderText(tr("Link name")); - _shareLinkEdit->setText(_linkShare.data()->getLabel()); - _shareLinkLayout->addWidget(_shareLinkEdit); - - _shareLinkButton = new QToolButton(this); - connect(_shareLinkButton, &QToolButton::clicked, this, &ShareLinkWidget::slotCreateLabel); - _shareLinkButton->setIcon(QIcon(":/client/theme/confirm.svg")); - _shareLinkButton->setToolButtonStyle(Qt::ToolButtonIconOnly); - _shareLinkLayout->addWidget(_shareLinkButton); - - _shareLinkProgressIndicator = new QProgressIndicator(this); - _shareLinkProgressIndicator->setVisible(false); - _shareLinkLayout->addWidget(_shareLinkProgressIndicator); - - _shareLinkDefaultWidget = new QWidget(this); - _shareLinkDefaultWidget->setLayout(_shareLinkLayout); - - _shareLinkWidgetAction = new QWidgetAction(this); - _shareLinkWidgetAction->setDefaultWidget(_shareLinkDefaultWidget); - _shareLinkWidgetAction->setCheckable(true); - _linkContextMenu->addAction(_shareLinkWidgetAction); - - // Adds permissions actions (radio button style) - if (_isFile) { - _linkContextMenu->addAction(_allowEditingLinkAction); - } else { - _linkContextMenu->addAction(_readOnlyLinkAction); - _linkContextMenu->addAction(_allowUploadEditingLinkAction); - _linkContextMenu->addAction(_allowUploadLinkAction); - } - - // Adds action to display note widget (check box) - _noteLinkAction = _linkContextMenu->addAction(tr("Note to recipient")); - _noteLinkAction->setCheckable(true); - - if (_linkShare->getNote().isSimpleText() && !_linkShare->getNote().isEmpty()) { - _ui->textEdit_note->setText(_linkShare->getNote()); - _noteLinkAction->setChecked(true); - toggleNoteOptions(); - } - - // Adds action to display password widget (check box) - _passwordProtectLinkAction = _linkContextMenu->addAction(tr("Password protect")); - _passwordProtectLinkAction->setCheckable(true); - - if (_linkShare.data()->isPasswordSet()) { - _passwordProtectLinkAction->setChecked(true); - _ui->lineEdit_password->setPlaceholderText(QString::fromUtf8(passwordIsSetPlaceholder)); - togglePasswordOptions(); - } - - // If password is enforced then don't allow users to disable it - if (_account->capabilities().sharePublicLinkEnforcePassword()) { - if (_linkShare.data()->isPasswordSet()) { - _passwordProtectLinkAction->setChecked(true); - _passwordProtectLinkAction->setEnabled(false); - } - _passwordRequired = true; - } - - // Adds action to display expiration date widget (check box) - _expirationDateLinkAction = _linkContextMenu->addAction(tr("Set expiration date")); - _expirationDateLinkAction->setCheckable(true); - if (!expireDate.isNull()) { - _ui->calendar->setDate(expireDate); - _expirationDateLinkAction->setChecked(true); - toggleExpireDateOptions(); - } - connect(_ui->calendar, &QDateTimeEdit::dateChanged, this, &ShareLinkWidget::slotSetExpireDate); - connect(_linkShare.data(), &LinkShare::expireDateSet, this, &ShareLinkWidget::slotExpireDateSet); - - - // If expiredate is enforced do not allow disable and set max days - if (_account->capabilities().sharePublicLinkEnforceExpireDate()) { - _ui->calendar->setMaximumDate(QDate::currentDate().addDays( - _account->capabilities().sharePublicLinkExpireDateDays())); - _expirationDateLinkAction->setChecked(true); - _expirationDateLinkAction->setEnabled(false); - _expiryRequired = true; - } - - // Adds action to unshare widget (check box) - _unshareLinkAction.reset(_linkContextMenu->addAction(QIcon(":/client/theme/delete.svg"), - tr("Delete link"))); - - _linkContextMenu->addSeparator(); - - _addAnotherLinkAction.reset(_linkContextMenu->addAction(QIcon(":/client/theme/add.svg"), - tr("Add another link"))); - - _ui->enableShareLink->setIcon(QIcon(":/client/theme/copy.svg")); - disconnect(_ui->enableShareLink, &QPushButton::clicked, this, &ShareLinkWidget::slotCreateShareLink); - connect(_ui->enableShareLink, &QPushButton::clicked, this, &ShareLinkWidget::slotCopyLinkShare); - - connect(_linkContextMenu, &QMenu::triggered, - this, &ShareLinkWidget::slotLinkContextMenuActionTriggered); - - _ui->shareLinkToolButton->setMenu(_linkContextMenu); - _ui->shareLinkToolButton->setEnabled(true); - _ui->enableShareLink->setEnabled(true); - _ui->enableShareLink->setChecked(true); - - // show sharing options - _ui->shareLinkToolButton->show(); - - customizeStyle(); -} - -void ShareLinkWidget::slotCreateNote() -{ - const auto note = _ui->textEdit_note->toPlainText(); - if (!_linkShare || _linkShare->getNote() == note || note.isEmpty()) { - return; - } - - toggleButtonAnimation(_ui->confirmNote, _ui->noteProgressIndicator, _noteLinkAction); - _ui->errorLabel->hide(); - _linkShare->setNote(note); -} - -void ShareLinkWidget::slotNoteSet() -{ - toggleButtonAnimation(_ui->confirmNote, _ui->noteProgressIndicator, _noteLinkAction); -} - -void ShareLinkWidget::slotCopyLinkShare(const bool clicked) const -{ - Q_UNUSED(clicked); - - QApplication::clipboard()->setText(_linkShare->getLink().toString()); -} - -void ShareLinkWidget::slotExpireDateSet() -{ - toggleButtonAnimation(_ui->confirmExpirationDate, _ui->expirationDateProgressIndicator, _expirationDateLinkAction); -} - -void ShareLinkWidget::slotSetExpireDate() -{ - if (!_linkShare) { - return; - } - - toggleButtonAnimation(_ui->confirmExpirationDate, _ui->expirationDateProgressIndicator, _expirationDateLinkAction); - _ui->errorLabel->hide(); - _linkShare->setExpireDate(_ui->calendar->date()); -} - -void ShareLinkWidget::slotCreatePassword() -{ - if (!_linkShare || _ui->lineEdit_password->text().isEmpty()) { - return; - } - - toggleButtonAnimation(_ui->confirmPassword, _ui->passwordProgressIndicator, _passwordProtectLinkAction); - _ui->errorLabel->hide(); - emit createPassword(_ui->lineEdit_password->text()); -} - -void ShareLinkWidget::slotCreateShareLink(const bool clicked) -{ - Q_UNUSED(clicked); - slotToggleShareLinkAnimation(true); - emit createLinkShare(); -} - -void ShareLinkWidget::slotPasswordSet() -{ - toggleButtonAnimation(_ui->confirmPassword, _ui->passwordProgressIndicator, _passwordProtectLinkAction); - - _ui->lineEdit_password->setText({}); - - if (_linkShare->isPasswordSet()) { - _ui->lineEdit_password->setEnabled(true); - _ui->lineEdit_password->setPlaceholderText(QString::fromUtf8(passwordIsSetPlaceholder)); - } else { - _ui->lineEdit_password->setPlaceholderText({}); - } - - emit createPasswordProcessed(); -} - -void ShareLinkWidget::slotPasswordSetError(const int code, const QString &message) -{ - toggleButtonAnimation(_ui->confirmPassword, _ui->passwordProgressIndicator, _passwordProtectLinkAction); - - slotServerError(code, message); - togglePasswordOptions(); - _ui->lineEdit_password->setFocus(); - emit createPasswordProcessed(); -} - -void ShareLinkWidget::slotDeleteShareFetched() -{ - slotToggleShareLinkAnimation(false); - - _linkShare.clear(); - togglePasswordOptions(false); - toggleNoteOptions(false); - toggleExpireDateOptions(false); - emit deleteLinkShare(); -} - -void ShareLinkWidget::toggleNoteOptions(const bool enable) -{ - _ui->noteLabel->setVisible(enable); - _ui->textEdit_note->setVisible(enable); - _ui->confirmNote->setVisible(enable); - _ui->textEdit_note->setText(enable && _linkShare ? _linkShare->getNote() : QString()); - - if (!enable && _linkShare && !_linkShare->getNote().isEmpty()) { - _linkShare->setNote({}); - } -} - -void ShareLinkWidget::slotCreateLabel() -{ - const auto labelText = _shareLinkEdit->text(); - if (!_linkShare || _linkShare->getLabel() == labelText || labelText.isEmpty()) { - return; - } - _shareLinkWidgetAction->setChecked(true); - toggleButtonAnimation(_shareLinkButton, _shareLinkProgressIndicator, _shareLinkWidgetAction); - _ui->errorLabel->hide(); - _linkShare->setLabel(_shareLinkEdit->text()); -} - -void ShareLinkWidget::slotLabelSet() -{ - toggleButtonAnimation(_shareLinkButton, _shareLinkProgressIndicator, _shareLinkWidgetAction); - displayShareLinkLabel(); -} - -void ShareLinkWidget::slotCreateShareRequiresPassword(const QString &message) -{ - slotToggleShareLinkAnimation(message.isEmpty()); - - if (!message.isEmpty()) { - _ui->errorLabel->setText(message); - _ui->errorLabel->show(); - } - - _passwordRequired = true; - - togglePasswordOptions(); -} - -void ShareLinkWidget::togglePasswordOptions(const bool enable) -{ - _ui->passwordLabel->setVisible(enable); - _ui->lineEdit_password->setVisible(enable); - _ui->confirmPassword->setVisible(enable); - _ui->lineEdit_password->setFocus(); - - if (!enable && _linkShare && _linkShare->isPasswordSet()) { - _linkShare->setPassword({}); - } -} - -void ShareLinkWidget::toggleExpireDateOptions(const bool enable) -{ - _ui->expirationLabel->setVisible(enable); - _ui->calendar->setVisible(enable); - _ui->confirmExpirationDate->setVisible(enable); - - const auto date = enable ? _linkShare->getExpireDate() : QDate::currentDate().addDays(1); - _ui->calendar->setDate(date); - _ui->calendar->setMinimumDate(QDate::currentDate().addDays(1)); - - if(_account->capabilities().sharePublicLinkEnforceExpireDate()) { - _ui->calendar->setMaximumDate(QDate::currentDate().addDays(_account->capabilities().sharePublicLinkExpireDateDays())); - } - - _ui->calendar->setFocus(); - - if (!enable && _linkShare && _linkShare->getExpireDate().isValid()) { - _linkShare->setExpireDate({}); - } -} - -void ShareLinkWidget::confirmAndDeleteShare() -{ - auto messageBox = new QMessageBox( - QMessageBox::Question, - tr("Confirm Link Share Deletion"), - tr("

Do you really want to delete the public link share %1?

" - "

Note: This action cannot be undone.

") - .arg(shareName()), - QMessageBox::NoButton, - this); - QPushButton *yesButton = - messageBox->addButton(tr("Delete"), QMessageBox::YesRole); - messageBox->addButton(tr("Cancel"), QMessageBox::NoRole); - - connect(messageBox, &QMessageBox::finished, this, - [messageBox, yesButton, this]() { - if (messageBox->clickedButton() == yesButton) { - this->slotToggleShareLinkAnimation(true); - this->_linkShare->deleteShare(); - } - }); - messageBox->open(); -} - -QString ShareLinkWidget::shareName() const -{ - QString name = _linkShare->getName(); - if (!name.isEmpty()) - return name; - if (!_namesSupported) - return tr("Public link"); - return _linkShare->getToken(); -} - -void ShareLinkWidget::slotContextMenuButtonClicked() -{ - _linkContextMenu->exec(QCursor::pos()); -} - -void ShareLinkWidget::slotLinkContextMenuActionTriggered(QAction *action) -{ - const auto state = action->isChecked(); - SharePermissions perm = SharePermissionRead; - - if (action == _addAnotherLinkAction.data()) { - emit createLinkShare(); - - } else if (action == _readOnlyLinkAction && state) { - _linkShare->setPermissions(perm); - - } else if (action == _allowEditingLinkAction && state) { - perm |= SharePermissionUpdate; - _linkShare->setPermissions(perm); - - } else if (action == _allowUploadEditingLinkAction && state) { - perm |= SharePermissionCreate | SharePermissionUpdate | SharePermissionDelete; - _linkShare->setPermissions(perm); - - } else if (action == _allowUploadLinkAction && state) { - perm = SharePermissionCreate; - _linkShare->setPermissions(perm); - - } else if (action == _passwordProtectLinkAction) { - togglePasswordOptions(state); - - } else if (action == _expirationDateLinkAction) { - toggleExpireDateOptions(state); - - } else if (action == _noteLinkAction) { - toggleNoteOptions(state); - - } else if (action == _unshareLinkAction.data()) { - confirmAndDeleteShare(); - } -} - -void ShareLinkWidget::slotServerError(const int code, const QString &message) -{ - slotToggleShareLinkAnimation(false); - - qCWarning(lcSharing) << "Error from server" << code << message; - displayError(message); -} - -void ShareLinkWidget::displayError(const QString &errMsg) -{ - _ui->errorLabel->setText(errMsg); - _ui->errorLabel->show(); -} - -void ShareLinkWidget::slotStyleChanged() -{ - customizeStyle(); -} - -void ShareLinkWidget::customizeStyle() -{ - if(_unshareLinkAction) { - _unshareLinkAction->setIcon(Theme::createColorAwareIcon(":/client/theme/delete.svg")); - } - - if(_addAnotherLinkAction) { - _addAnotherLinkAction->setIcon(Theme::createColorAwareIcon(":/client/theme/add.svg")); - } - - _ui->enableShareLink->setIcon(Theme::createColorAwareIcon(":/client/theme/copy.svg")); - - _ui->shareLinkIconLabel->setPixmap(Theme::createColorAwarePixmap(":/client/theme/public.svg")); - - _ui->shareLinkToolButton->setIcon(Theme::createColorAwareIcon(":/client/theme/more.svg")); - - _ui->confirmNote->setIcon(Theme::createColorAwareIcon(":/client/theme/confirm.svg")); - _ui->confirmPassword->setIcon(Theme::createColorAwareIcon(":/client/theme/confirm.svg")); - _ui->confirmExpirationDate->setIcon(Theme::createColorAwareIcon(":/client/theme/confirm.svg")); - - _ui->passwordProgressIndicator->setColor(QGuiApplication::palette().color(QPalette::Text)); -} - -void ShareLinkWidget::displayShareLinkLabel() -{ - _shareLinkElidedLabel->clear(); - if (!_linkShare->getLabel().isEmpty()) { - _shareLinkElidedLabel->setText(QString("(%1)").arg(_linkShare->getLabel())); - } -} - -} diff --git a/src/gui/sharelinkwidget.h b/src/gui/sharelinkwidget.h deleted file mode 100644 index 7ecd9691e..000000000 --- a/src/gui/sharelinkwidget.h +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright (C) by Roeland Jago Douma - * Copyright (C) 2015 by Klaas Freitag - * - * 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. - */ - -#ifndef SHARELINKWIDGET_H -#define SHARELINKWIDGET_H - -#include "accountfwd.h" -#include "sharepermissions.h" -#include "QProgressIndicator.h" -#include -#include -#include -#include -#include -#include -#include -#include - -class QMenu; -class QTableWidgetItem; - -namespace OCC { - -namespace Ui { - class ShareLinkWidget; -} - -class AbstractCredentials; -class SyncResult; -class LinkShare; -class Share; -class ElidedLabel; - -/** - * @brief The ShareDialog class - * @ingroup gui - */ -class ShareLinkWidget : public QWidget -{ - Q_OBJECT - -public: - explicit ShareLinkWidget(AccountPtr account, - const QString &sharePath, - const QString &localPath, - SharePermissions maxSharingPermissions, - QWidget *parent = nullptr); - ~ShareLinkWidget() override; - - void toggleButton(bool show); - void setupUiOptions(); - - void setLinkShare(QSharedPointer linkShare); - QSharedPointer getLinkShare(); - - void focusPasswordLineEdit(); - -public slots: - void slotDeleteShareFetched(); - void slotToggleShareLinkAnimation(const bool start); - void slotServerError(const int code, const QString &message); - void slotCreateShareRequiresPassword(const QString &message); - void slotStyleChanged(); - -private slots: - void slotCreateShareLink(const bool clicked); - void slotCopyLinkShare(const bool clicked) const; - - void slotCreatePassword(); - void slotPasswordSet(); - void slotPasswordSetError(const int code, const QString &message); - - void slotCreateNote(); - void slotNoteSet(); - - void slotSetExpireDate(); - void slotExpireDateSet(); - - void slotContextMenuButtonClicked(); - void slotLinkContextMenuActionTriggered(QAction *action); - - void slotCreateLabel(); - void slotLabelSet(); - -signals: - void createLinkShare(); - void deleteLinkShare(); - void visualDeletionDone(); - void createPassword(const QString &password); - void createPasswordProcessed(); - -private: - void displayError(const QString &errMsg); - - void togglePasswordOptions(const bool enable = true); - void toggleNoteOptions(const bool enable = true); - void toggleExpireDateOptions(const bool enable = true); - void toggleButtonAnimation(QToolButton *button, QProgressIndicator *progressIndicator, const QAction *checkedAction) const; - - /** Confirm with the user and then delete the share */ - void confirmAndDeleteShare(); - - /** Retrieve a share's name, accounting for _namesSupported */ - [[nodiscard]] QString shareName() const; - - void customizeStyle(); - - void displayShareLinkLabel(); - - Ui::ShareLinkWidget *_ui; - AccountPtr _account; - QString _sharePath; - QString _localPath; - QString _shareUrl; - - QSharedPointer _linkShare; - - bool _isFile; - bool _passwordRequired; - bool _expiryRequired; - bool _namesSupported; - bool _noteRequired; - - QMenu *_linkContextMenu; - QAction *_readOnlyLinkAction; - QAction *_allowEditingLinkAction; - QAction *_allowUploadEditingLinkAction; - QAction *_allowUploadLinkAction; - QAction *_passwordProtectLinkAction; - QAction *_expirationDateLinkAction; - QScopedPointer _unshareLinkAction; - QScopedPointer _addAnotherLinkAction; - QAction *_noteLinkAction; - QHBoxLayout *_shareLinkLayout{}; - QLabel *_shareLinkLabel{}; - ElidedLabel *_shareLinkElidedLabel{}; - QLineEdit *_shareLinkEdit{}; - QToolButton *_shareLinkButton{}; - QProgressIndicator *_shareLinkProgressIndicator{}; - QWidget *_shareLinkDefaultWidget{}; - QWidgetAction *_shareLinkWidgetAction{}; -}; -} - -#endif // SHARELINKWIDGET_H diff --git a/src/gui/sharelinkwidget.ui b/src/gui/sharelinkwidget.ui deleted file mode 100644 index a0d5f3df9..000000000 --- a/src/gui/sharelinkwidget.ui +++ /dev/null @@ -1,439 +0,0 @@ - - - OCC::ShareLinkWidget - - - - 0 - 0 - 400 - 238 - - - - - 0 - 0 - - - - - 0 - - - 12 - - - 0 - - - 20 - - - 0 - - - - - 6 - - - 0 - - - - - - - - :/client/theme/public.svg - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Share link - - - - - - - Qt::Horizontal - - - - 40 - 25 - - - - - - - - - 0 - 0 - - - - - 28 - 27 - - - - - - - - Qt::Horizontal - - - - 40 - 25 - - - - - - - - - - - - :/client/theme/add.svg:/client/theme/add.svg - - - false - - - true - - - - - - - false - - - - 0 - 0 - - - - - :/client/theme/more.svg:/client/theme/more.svg - - - QToolButton::InstantPopup - - - true - - - - - - - - - 22 - - - - - - 0 - 0 - - - - - 78 - 0 - - - - Note - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - 0 - - - - - - - - 0 - 0 - - - - - 0 - 60 - - - - QAbstractScrollArea::AdjustToContents - - - - - - - - 28 - 27 - - - - - :/client/theme/confirm.svg:/client/theme/confirm.svg - - - true - - - - - - - - 0 - 0 - - - - - 28 - 27 - - - - - - - - - 0 - 0 - - - - - 78 - 0 - - - - Set password - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - 0 - - - - - - - - 1 - 0 - - - - QLineEdit::Password - - - - - - - - 28 - 27 - - - - - :/client/theme/confirm.svg:/client/theme/confirm.svg - - - true - - - - - - - - 0 - 0 - - - - - 28 - 27 - - - - - - - - - 0 - 0 - - - - - 78 - 0 - - - - Expires - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - 0 - - - - - - - - 1 - 0 - - - - - - - - - 28 - 27 - - - - - :/client/theme/confirm.svg:/client/theme/confirm.svg - - - true - - - - - - - - 0 - 0 - - - - - 28 - 27 - - - - - - - - - - - - - - - - - 255 - 0 - 0 - - - - - - - - - 255 - 0 - 0 - - - - - - - - - 123 - 121 - 134 - - - - - - - - TextLabel - - - Qt::PlainText - - - true - - - - - - - - - - - QProgressIndicator - QWidget -
QProgressIndicator.h
- 1 -
-
- - - - -
diff --git a/src/gui/sharemanager.cpp b/src/gui/sharemanager.cpp index 154bcbec3..7a264557d 100644 --- a/src/gui/sharemanager.cpp +++ b/src/gui/sharemanager.cpp @@ -58,7 +58,7 @@ Share::Share(AccountPtr account, const ShareType shareType, bool isPasswordSet, const Permissions permissions, - const QSharedPointer shareWith) + const ShareePtr shareWith) : _account(account) , _id(id) , _uidowner(uidowner) @@ -101,7 +101,7 @@ Share::ShareType Share::getShareType() const return _shareType; } -QSharedPointer Share::getShareWith() const +ShareePtr Share::getShareWith() const { return _shareWith; } @@ -316,7 +316,7 @@ UserGroupShare::UserGroupShare(AccountPtr account, const ShareType shareType, bool isPasswordSet, const Permissions permissions, - const QSharedPointer shareWith, + const ShareePtr shareWith, const QDate &expireDate, const QString ¬e) : Share(account, id, owner, ownerDisplayName, path, shareType, isPasswordSet, permissions, shareWith) @@ -461,7 +461,7 @@ void ShareManager::slotShareCreated(const QJsonDocument &reply) { //Parse share auto data = reply.object().value("ocs").toObject().value("data").toObject(); - QSharedPointer share(parseShare(data)); + SharePtr share(parseShare(data)); emit shareCreated(share); @@ -482,14 +482,14 @@ void ShareManager::slotSharesFetched(const QJsonDocument &reply) const QString versionString = _account->serverVersion(); qCDebug(lcSharing) << versionString << "Fetched" << tmpShares.count() << "shares"; - QList> shares; + QList shares; foreach (const auto &share, tmpShares) { auto data = share.toObject(); auto shareType = data.value("share_type").toInt(); - QSharedPointer newShare; + SharePtr newShare; if (shareType == Share::TypeLink) { newShare = parseLinkShare(data); @@ -499,7 +499,7 @@ void ShareManager::slotSharesFetched(const QJsonDocument &reply) newShare = parseShare(data); } - shares.append(QSharedPointer(newShare)); + shares.append(SharePtr(newShare)); } qCDebug(lcSharing) << "Sending " << shares.count() << "shares"; @@ -508,7 +508,7 @@ void ShareManager::slotSharesFetched(const QJsonDocument &reply) QSharedPointer ShareManager::parseUserGroupShare(const QJsonObject &data) { - QSharedPointer sharee(new Sharee(data.value("share_with").toString(), + ShareePtr sharee(new Sharee(data.value("share_with").toString(), data.value("share_with_displayname").toString(), static_cast(data.value("share_type").toInt()))); @@ -577,13 +577,13 @@ QSharedPointer ShareManager::parseLinkShare(const QJsonObject &data) data.value("label").toString())); } -QSharedPointer ShareManager::parseShare(const QJsonObject &data) +SharePtr ShareManager::parseShare(const QJsonObject &data) const { - QSharedPointer sharee(new Sharee(data.value("share_with").toString(), + ShareePtr sharee(new Sharee(data.value("share_with").toString(), data.value("share_with_displayname").toString(), (Sharee::Type)data.value("share_type").toInt())); - return QSharedPointer(new Share(_account, + return SharePtr(new Share(_account, data.value("id").toVariant().toString(), // "id" used to be an integer, support both data.value("uid_owner").toVariant().toString(), data.value("displayname_owner").toVariant().toString(), diff --git a/src/gui/sharemanager.h b/src/gui/sharemanager.h index 16d2e47c3..1d9918f04 100644 --- a/src/gui/sharemanager.h +++ b/src/gui/sharemanager.h @@ -36,6 +36,15 @@ class OcsShareJob; class Share : public QObject { Q_OBJECT + Q_PROPERTY(AccountPtr account READ account CONSTANT) + Q_PROPERTY(QString path READ path CONSTANT) + Q_PROPERTY(QString id READ getId CONSTANT) + Q_PROPERTY(QString uidOwner READ getUidOwner CONSTANT) + Q_PROPERTY(QString ownerDisplayName READ getOwnerDisplayName CONSTANT) + Q_PROPERTY(ShareType shareType READ getShareType CONSTANT) + Q_PROPERTY(ShareePtr shareWith READ getShareWith CONSTANT) + Q_PROPERTY(Permissions permissions READ getPermissions WRITE setPermissions NOTIFY permissionsSet) + Q_PROPERTY(bool isPasswordSet READ isPasswordSet NOTIFY passwordSet) public: /** @@ -43,14 +52,16 @@ public: * Need to be in sync with Sharee::Type */ enum ShareType { + TypePlaceholderLink = -1, TypeUser = Sharee::User, TypeGroup = Sharee::Group, TypeLink = 3, TypeEmail = Sharee::Email, TypeRemote = Sharee::Federated, TypeCircle = Sharee::Circle, - TypeRoom = Sharee::Room + TypeRoom = Sharee::Room, }; + Q_ENUM(ShareType); using Permissions = SharePermissions; @@ -65,7 +76,7 @@ public: const ShareType shareType, bool isPasswordSet = false, const Permissions permissions = SharePermissionDefault, - const QSharedPointer shareWith = QSharedPointer(nullptr)); + const ShareePtr shareWith = ShareePtr(nullptr)); /** * The account the share is defined on. @@ -97,13 +108,36 @@ public: /* * Get the shareWith */ - [[nodiscard]] QSharedPointer getShareWith() const; + [[nodiscard]] ShareePtr getShareWith() const; /* * Get permissions */ [[nodiscard]] Permissions getPermissions() const; + [[nodiscard]] bool isPasswordSet() const; + + /* + * Is it a share with a user or group (local or remote) + */ + [[nodiscard]] static bool isShareTypeUserGroupEmailRoomOrRemote(const ShareType type); + +signals: + void permissionsSet(); + void shareDeleted(); + void serverError(int code, const QString &message); + void passwordSet(); + void passwordSetError(int statusCode, const QString &message); + +public slots: + /* + * Deletes a share + * + * On success the shareDeleted signal is emitted + * In case of a server error the serverError signal is emitted. + */ + void deleteShare(); + /* * Set the permissions of a share * @@ -120,28 +154,6 @@ public: */ void setPassword(const QString &password); - [[nodiscard]] bool isPasswordSet() const; - - /* - * Deletes a share - * - * On success the shareDeleted signal is emitted - * In case of a server error the serverError signal is emitted. - */ - void deleteShare(); - - /* - * Is it a share with a user or group (local or remote) - */ - static bool isShareTypeUserGroupEmailRoomOrRemote(const ShareType type); - -signals: - void permissionsSet(); - void shareDeleted(); - void serverError(int code, const QString &message); - void passwordSet(); - void passwordSetError(int statusCode, const QString &message); - protected: AccountPtr _account; QString _id; @@ -151,7 +163,7 @@ protected: ShareType _shareType; bool _isPasswordSet; Permissions _permissions; - QSharedPointer _shareWith; + ShareePtr _shareWith; protected slots: void slotOcsError(int statusCode, const QString &message); @@ -163,6 +175,8 @@ private slots: void slotPermissionsSet(const QJsonDocument &, const QVariant &value); }; +using SharePtr = QSharedPointer; + /** * A Link share is just like a regular share but then slightly different. * There are several methods in the API that either work differently for @@ -171,6 +185,16 @@ private slots: class LinkShare : public Share { Q_OBJECT + Q_PROPERTY(QUrl link READ getLink CONSTANT) + Q_PROPERTY(QUrl directDownloadLink READ getDirectDownloadLink CONSTANT) + Q_PROPERTY(bool publicCanUpload READ getPublicUpload CONSTANT) + Q_PROPERTY(bool publicCanReadDirectory READ getShowFileListing CONSTANT) + Q_PROPERTY(QString name READ getName WRITE setName NOTIFY nameSet) + Q_PROPERTY(QString note READ getNote WRITE setNote NOTIFY noteSet) + Q_PROPERTY(QString label READ getLabel WRITE setLabel NOTIFY labelSet) + Q_PROPERTY(QDate expireDate READ getExpireDate WRITE setExpireDate NOTIFY expireDateSet) + Q_PROPERTY(QString token READ getToken CONSTANT) + public: explicit LinkShare(AccountPtr account, const QString &id, @@ -221,6 +245,23 @@ public: */ [[nodiscard]] QString getLabel() const; + /* + * Returns the token of the link share. + */ + [[nodiscard]] QString getToken() const; + + /* + * Get the expiration date + */ + [[nodiscard]] QDate getExpireDate() const; + + /* + * Create OcsShareJob and connect to signal/slots + */ + template + OcsShareJob *createShareJob(const LinkShareSlot slotFunction); + +public slots: /* * Set the name of the link share. * @@ -233,16 +274,6 @@ public: */ void setNote(const QString ¬e); - /* - * Returns the token of the link share. - */ - [[nodiscard]] QString getToken() const; - - /* - * Get the expiration date - */ - [[nodiscard]] QDate getExpireDate() const; - /* * Set the expiration date * @@ -250,19 +281,12 @@ public: * In case of a server error the serverError signal is emitted. */ void setExpireDate(const QDate &expireDate); - + /* * Set the label of the share link. */ void setLabel(const QString &label); - /* - * Create OcsShareJob and connect to signal/slots - */ - template - OcsShareJob *createShareJob(const LinkShareSlot slotFunction); - - signals: void expireDateSet(); void noteSet(); @@ -287,6 +311,8 @@ private: class UserGroupShare : public Share { Q_OBJECT + Q_PROPERTY(QString note READ getNote WRITE setNote NOTIFY noteSet) + Q_PROPERTY(QDate expireDate READ getExpireDate WRITE setExpireDate NOTIFY expireDateSet) public: UserGroupShare(AccountPtr account, const QString &id, @@ -296,27 +322,26 @@ public: const ShareType shareType, bool isPasswordSet, const Permissions permissions, - const QSharedPointer shareWith, + const ShareePtr shareWith, const QDate &expireDate, const QString ¬e); - void setNote(const QString ¬e); - [[nodiscard]] QString getNote() const; - - void slotNoteSet(const QJsonDocument &, const QVariant ¬e); - - void setExpireDate(const QDate &date); - [[nodiscard]] QDate getExpireDate() const; - void slotExpireDateSet(const QJsonDocument &reply, const QVariant &value); +public slots: + void setNote(const QString ¬e); + void setExpireDate(const QDate &date); signals: void noteSet(); void noteSetError(); void expireDateSet(); +private slots: + void slotNoteSet(const QJsonDocument &json, const QVariant ¬e); + void slotExpireDateSet(const QJsonDocument &reply, const QVariant &value); + private: QString _note; QDate _expireDate; @@ -375,9 +400,9 @@ public: void fetchShares(const QString &path); signals: - void shareCreated(const QSharedPointer &share); + void shareCreated(const SharePtr &share); void linkShareCreated(const QSharedPointer &share); - void sharesFetched(const QList> &shares); + void sharesFetched(const QList &shares); void serverError(int code, const QString &message); /** Emitted when creating a link share with password fails. @@ -396,10 +421,12 @@ private slots: private: QSharedPointer parseLinkShare(const QJsonObject &data); QSharedPointer parseUserGroupShare(const QJsonObject &data); - QSharedPointer parseShare(const QJsonObject &data); + SharePtr parseShare(const QJsonObject &data) const; AccountPtr _account; }; } +Q_DECLARE_METATYPE(OCC::SharePtr); + #endif // SHAREMANAGER_H diff --git a/src/gui/shareusergroupwidget.cpp b/src/gui/shareusergroupwidget.cpp deleted file mode 100644 index 024e1266e..000000000 --- a/src/gui/shareusergroupwidget.cpp +++ /dev/null @@ -1,1129 +0,0 @@ -/* - * Copyright (C) by Roeland Jago Douma - * - * 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. - */ - -#include "ocsprofileconnector.h" -#include "sharee.h" -#include "tray/usermodel.h" -#include "ui_shareusergroupwidget.h" -#include "ui_shareuserline.h" -#include "shareusergroupwidget.h" -#include "account.h" -#include "folderman.h" -#include "folder.h" -#include "accountmanager.h" -#include "theme.h" -#include "configfile.h" -#include "capabilities.h" -#include "guiutility.h" -#include "thumbnailjob.h" -#include "sharemanager.h" -#include "theme.h" -#include "iconutils.h" - -#include "QProgressIndicator.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -namespace { -const char *passwordIsSetPlaceholder = "●●●●●●●●"; - -} - -namespace OCC { - -AvatarEventFilter::AvatarEventFilter(QObject *parent) - : QObject(parent) -{ -} - - -bool AvatarEventFilter::eventFilter(QObject *obj, QEvent *event) -{ - if (event->type() == QEvent::ContextMenu) { - const auto contextMenuEvent = dynamic_cast(event); - if (!contextMenuEvent) { - return false; - } - emit contextMenu(contextMenuEvent->globalPos()); - return true; - } - return QObject::eventFilter(obj, event); -} - -ShareUserGroupWidget::ShareUserGroupWidget(AccountPtr account, - const QString &sharePath, - const QString &localPath, - SharePermissions maxSharingPermissions, - const QString &privateLinkUrl, - QWidget *parent) - : QWidget(parent) - , _ui(new Ui::ShareUserGroupWidget) - , _account(account) - , _sharePath(sharePath) - , _localPath(localPath) - , _maxSharingPermissions(maxSharingPermissions) - , _privateLinkUrl(privateLinkUrl) - , _disableCompleterActivated(false) -{ - setAttribute(Qt::WA_DeleteOnClose); - setObjectName("SharingDialogUG"); // required as group for saveGeometry call - - _ui->setupUi(this); - - //Is this a file or folder? - _isFile = QFileInfo(localPath).isFile(); - - _completer = new QCompleter(this); - _completerModel = new ShareeModel(_account, - _isFile ? QLatin1String("file") : QLatin1String("folder"), - _completer); - connect(_completerModel, &ShareeModel::shareesReady, this, &ShareUserGroupWidget::slotShareesReady); - connect(_completerModel, &ShareeModel::displayErrorMessage, this, &ShareUserGroupWidget::displayError); - - _completer->setModel(_completerModel); - _completer->setCaseSensitivity(Qt::CaseInsensitive); - _completer->setCompletionMode(QCompleter::UnfilteredPopupCompletion); - _ui->shareeLineEdit->setCompleter(_completer); - - _searchGloballyAction.reset(new QAction(_ui->shareeLineEdit)); - _searchGloballyAction->setIcon(Theme::createColorAwareIcon(":/client/theme/magnifying-glass.svg")); - _searchGloballyAction->setToolTip(tr("Search globally")); - - connect(_searchGloballyAction.data(), &QAction::triggered, this, [this]() { - searchForSharees(ShareeModel::GlobalSearch); - }); - - _ui->shareeLineEdit->addAction(_searchGloballyAction.data(), QLineEdit::LeadingPosition); - - _manager = new ShareManager(_account, this); - connect(_manager, &ShareManager::sharesFetched, this, &ShareUserGroupWidget::slotSharesFetched); - connect(_manager, &ShareManager::shareCreated, this, &ShareUserGroupWidget::slotShareCreated); - connect(_manager, &ShareManager::serverError, this, &ShareUserGroupWidget::displayError); - connect(_ui->shareeLineEdit, &QLineEdit::returnPressed, this, &ShareUserGroupWidget::slotLineEditReturn); - connect(_ui->confirmShare, &QAbstractButton::clicked, this, &ShareUserGroupWidget::slotLineEditReturn); - //TODO connect(_ui->privateLinkText, &QLabel::linkActivated, this, &ShareUserGroupWidget::slotPrivateLinkShare); - - // By making the next two QueuedConnections we can override - // the strings the completer sets on the line edit. - connect(_completer, SIGNAL(activated(QModelIndex)), SLOT(slotCompleterActivated(QModelIndex)), - Qt::QueuedConnection); - connect(_completer, SIGNAL(highlighted(QModelIndex)), SLOT(slotCompleterHighlighted(QModelIndex)), - Qt::QueuedConnection); - - // Queued connection so this signal is recieved after textChanged - connect(_ui->shareeLineEdit, &QLineEdit::textEdited, - this, &ShareUserGroupWidget::slotLineEditTextEdited, Qt::QueuedConnection); - _ui->shareeLineEdit->installEventFilter(this); - connect(&_completionTimer, &QTimer::timeout, this, [this]() { - searchForSharees(ShareeModel::LocalSearch); - }); - _completionTimer.setSingleShot(true); - _completionTimer.setInterval(600); - - _ui->errorLabel->hide(); - - _parentScrollArea = parentWidget()->findChild("scrollArea"); - _shareUserGroup = new QVBoxLayout(_parentScrollArea); - _shareUserGroup->setContentsMargins(0, 0, 0, 0); - customizeStyle(); -} - -QVBoxLayout *ShareUserGroupWidget::shareUserGroupLayout() -{ - return _shareUserGroup; -} - -ShareUserGroupWidget::~ShareUserGroupWidget() -{ - delete _ui; -} - -void ShareUserGroupWidget::on_shareeLineEdit_textChanged(const QString &) -{ - _completionTimer.stop(); - emit togglePublicLinkShare(false); -} - -void ShareUserGroupWidget::slotLineEditTextEdited(const QString &text) -{ - _disableCompleterActivated = false; - // First textChanged is called first and we stopped the timer when the text is changed, programatically or not - // Then we restart the timer here if the user touched a key - if (!text.isEmpty()) { - _completionTimer.start(); - emit togglePublicLinkShare(true); - } -} - -void ShareUserGroupWidget::slotLineEditReturn() -{ - _disableCompleterActivated = false; - // did the user type in one of the options? - const auto text = _ui->shareeLineEdit->text(); - for (int i = 0; i < _completerModel->rowCount(); ++i) { - const auto sharee = _completerModel->getSharee(i); - if (sharee->format() == text - || sharee->displayName() == text - || sharee->shareWith() == text) { - slotCompleterActivated(_completerModel->index(i)); - // make sure we do not send the same item twice (because return is called when we press - // return to activate an item inthe completer) - _disableCompleterActivated = true; - return; - } - } - - // nothing found? try to refresh completion - _completionTimer.start(); -} - -void ShareUserGroupWidget::searchForSharees(ShareeModel::LookupMode lookupMode) -{ - if (_ui->shareeLineEdit->text().isEmpty()) { - return; - } - - _ui->shareeLineEdit->setEnabled(false); - _completionTimer.stop(); - _pi_sharee.startAnimation(); - ShareeModel::ShareeSet blacklist; - - // Add the current user to _sharees since we can't share with ourself - QSharedPointer currentUser(new Sharee(_account->credentials()->user(), "", Sharee::Type::User)); - blacklist << currentUser; - - foreach (auto sw, _parentScrollArea->findChildren()) { - blacklist << sw->share()->getShareWith(); - } - _ui->errorLabel->hide(); - _completerModel->fetch(_ui->shareeLineEdit->text(), blacklist, lookupMode); -} - -void ShareUserGroupWidget::getShares() -{ - _manager->fetchShares(_sharePath); -} - -void ShareUserGroupWidget::slotShareCreated(const QSharedPointer &share) -{ - if (share && _account->capabilities().shareEmailPasswordEnabled() && !_account->capabilities().shareEmailPasswordEnforced()) { - // remember this share Id so we can set it's password Line Edit to focus later - _lastCreatedShareId = share->getId(); - } - // fetch all shares including the one we've just created - getShares(); -} - -void ShareUserGroupWidget::slotSharesFetched(const QList> &shares) -{ - int x = 0; - QList linkOwners({}); - - ShareUserLine *justCreatedShareThatNeedsPassword = nullptr; - - while (QLayoutItem *shareUserLine = _shareUserGroup->takeAt(0)) { - delete shareUserLine->widget(); - delete shareUserLine; - } - - foreach (const auto &share, shares) { - // We don't handle link shares, only TypeUser or TypeGroup - if (share->getShareType() == Share::TypeLink) { - if(!share->getUidOwner().isEmpty() && - share->getUidOwner() != share->account()->davUser()){ - linkOwners.append(share->getOwnerDisplayName()); - } - continue; - } - - // the owner of the file that shared it first - // leave out if it's the current user - if(x == 0 && !share->getUidOwner().isEmpty() && !(share->getUidOwner() == _account->credentials()->user())) { - _ui->mainOwnerLabel->setText(QString("Shared with you by ").append(share->getOwnerDisplayName())); - } - - - Q_ASSERT(Share::isShareTypeUserGroupEmailRoomOrRemote(share->getShareType())); - auto userGroupShare = qSharedPointerDynamicCast(share); - auto *s = new ShareUserLine(_account, userGroupShare, _maxSharingPermissions, _isFile, _parentScrollArea); - connect(s, &ShareUserLine::visualDeletionDone, this, &ShareUserGroupWidget::getShares); - s->setBackgroundRole(_shareUserGroup->count() % 2 == 0 ? QPalette::Base : QPalette::AlternateBase); - - // Connect styleChanged events to our widget, so it can adapt (Dark-/Light-Mode switching) - connect(this, &ShareUserGroupWidget::styleChanged, s, &ShareUserLine::slotStyleChanged); - _shareUserGroup->addWidget(s); - - if (!_lastCreatedShareId.isEmpty() && share->getId() == _lastCreatedShareId) { - _lastCreatedShareId = QString(); - if (_account->capabilities().shareEmailPasswordEnabled() && !_account->capabilities().shareEmailPasswordEnforced()) { - justCreatedShareThatNeedsPassword = s; - } - } - - x++; - } - - foreach (const QString &owner, linkOwners) { - auto ownerLabel = new QLabel(QString(owner + " shared via link")); - _shareUserGroup->addWidget(ownerLabel); - ownerLabel->setVisible(true); - } - - _disableCompleterActivated = false; - activateShareeLineEdit(); - - if (justCreatedShareThatNeedsPassword) { - // always set focus to a password Line Edit when the new email share is created on a server with optional passwords enabled for email shares - justCreatedShareThatNeedsPassword->focusPasswordLineEdit(); - } -} - -void ShareUserGroupWidget::slotPrivateLinkShare() -{ - auto menu = new QMenu(this); - menu->setAttribute(Qt::WA_DeleteOnClose); - - // this icon is not handled by slotStyleChanged() -> customizeStyle but we can live with that - menu->addAction(Theme::createColorAwareIcon(":/client/theme/copy.svg"), - tr("Copy link"), - this, SLOT(slotPrivateLinkCopy())); - - menu->exec(QCursor::pos()); -} - -void ShareUserGroupWidget::slotShareesReady() -{ - activateShareeLineEdit(); - - _pi_sharee.stopAnimation(); - if (_completerModel->rowCount() == 0) { - displayError(0, tr("No results for \"%1\"").arg(_completerModel->currentSearch())); - } - - // if no rows are present in the model - complete() will hide the completer - _completer->complete(); -} - -void ShareUserGroupWidget::slotCompleterActivated(const QModelIndex &index) -{ - if (_disableCompleterActivated) - return; - // The index is an index from the QCompletion model which is itelf a proxy - // model proxying the _completerModel - auto sharee = qvariant_cast>(index.data(Qt::UserRole)); - if (sharee.isNull()) { - return; - } - - /* - * Don't send the reshare permissions for federated shares for servers <9.1 - * https://github.com/owncloud/core/issues/22122#issuecomment-185637344 - * https://github.com/owncloud/client/issues/4996 - */ - _lastCreatedShareId = QString(); - - QString password; - if (sharee->type() == Sharee::Email && _account->capabilities().shareEmailPasswordEnforced()) { - _ui->shareeLineEdit->clear(); - // always show a dialog for password-enforced email shares - bool ok = false; - - do { - password = QInputDialog::getText( - this, - tr("Password for share required"), - tr("Please enter a password for your email share:"), - QLineEdit::Password, - QString(), - &ok); - } while (password.isEmpty() && ok); - - if (!ok) { - return; - } - } - - _manager->createShare(_sharePath, Share::ShareType(sharee->type()), - sharee->shareWith(), _maxSharingPermissions, password); - - _ui->shareeLineEdit->setEnabled(false); - _ui->shareeLineEdit->clear(); -} - -void ShareUserGroupWidget::slotCompleterHighlighted(const QModelIndex &index) -{ - // By default the completer would set the text to EditRole, - // override that here. - _ui->shareeLineEdit->setText(index.data(Qt::DisplayRole).toString()); -} - -void ShareUserGroupWidget::displayError(int code, const QString &message) -{ - _pi_sharee.stopAnimation(); - - // Also remove the spinner in the widget list, if any - foreach (auto pi, _parentScrollArea->findChildren()) { - delete pi; - } - - qCWarning(lcSharing) << "Sharing error from server" << code << message; - _ui->errorLabel->setText(message); - _ui->errorLabel->show(); - activateShareeLineEdit(); -} - -void ShareUserGroupWidget::slotPrivateLinkOpenBrowser() -{ - Utility::openBrowser(_privateLinkUrl, this); -} - -void ShareUserGroupWidget::slotPrivateLinkCopy() -{ - QApplication::clipboard()->setText(_privateLinkUrl); -} - -void ShareUserGroupWidget::slotPrivateLinkEmail() -{ - Utility::openEmailComposer( - tr("I shared something with you"), - _privateLinkUrl, - this); -} - -void ShareUserGroupWidget::slotStyleChanged() -{ - customizeStyle(); - - // Notify the other widgets (ShareUserLine in this case, Dark-/Light-Mode switching) - emit styleChanged(); -} - -void ShareUserGroupWidget::customizeStyle() -{ - _searchGloballyAction->setIcon(Theme::createColorAwareIcon(":/client/theme/magnifying-glass.svg")); - - _ui->confirmShare->setIcon(Theme::createColorAwareIcon(":/client/theme/confirm.svg")); - - _pi_sharee.setColor(QGuiApplication::palette().color(QPalette::Text)); - - foreach (auto pi, _parentScrollArea->findChildren()) { - pi->setColor(QGuiApplication::palette().color(QPalette::Text));; - } -} - -void ShareUserGroupWidget::activateShareeLineEdit() -{ - _ui->shareeLineEdit->setEnabled(true); - _ui->shareeLineEdit->setFocus(); -} - -ShareUserLine::ShareUserLine(AccountPtr account, QSharedPointer share, - SharePermissions maxSharingPermissions, bool isFile, QWidget *parent) - : QWidget(parent) - , _ui(new Ui::ShareUserLine) - , _account(account) - , _share(share) - , _isFile(isFile) - , _profilePageMenu(account, share->getShareWith()->shareWith()) -{ - Q_ASSERT(_share); - _ui->setupUi(this); - - _ui->sharedWith->setElideMode(Qt::ElideRight); - _ui->sharedWith->setText(share->getShareWith()->format()); - - // adds permissions - // can edit permission - bool enabled = (maxSharingPermissions & SharePermissionUpdate); - if(!_isFile) enabled = enabled && (maxSharingPermissions & SharePermissionCreate && - maxSharingPermissions & SharePermissionDelete); - _ui->permissionsEdit->setEnabled(enabled); - connect(_ui->permissionsEdit, &QAbstractButton::clicked, this, &ShareUserLine::slotEditPermissionsChanged); - connect(_ui->noteConfirmButton, &QAbstractButton::clicked, this, &ShareUserLine::onNoteConfirmButtonClicked); - connect(_ui->calendar, &QDateTimeEdit::dateChanged, this, &ShareUserLine::setExpireDate); - - connect(_share.data(), &UserGroupShare::noteSet, this, &ShareUserLine::disableProgessIndicatorAnimation); - connect(_share.data(), &UserGroupShare::noteSetError, this, &ShareUserLine::disableProgessIndicatorAnimation); - connect(_share.data(), &UserGroupShare::expireDateSet, this, &ShareUserLine::disableProgessIndicatorAnimation); - - connect(_ui->confirmPassword, &QToolButton::clicked, this, &ShareUserLine::slotConfirmPasswordClicked); - connect(_ui->lineEdit_password, &QLineEdit::returnPressed, this, &ShareUserLine::slotLineEditPasswordReturnPressed); - - // create menu with checkable permissions - auto *menu = new QMenu(this); - _permissionReshare= new QAction(tr("Can reshare"), this); - _permissionReshare->setCheckable(true); - _permissionReshare->setEnabled(maxSharingPermissions & SharePermissionShare); - menu->addAction(_permissionReshare); - connect(_permissionReshare, &QAction::triggered, this, &ShareUserLine::slotPermissionsChanged); - - showNoteOptions(false); - - const bool isNoteSupported = _share->getShareType() != Share::ShareType::TypeEmail && _share->getShareType() != Share::ShareType::TypeRoom; - - if (isNoteSupported) { - _noteLinkAction = new QAction(tr("Note to recipient")); - _noteLinkAction->setCheckable(true); - menu->addAction(_noteLinkAction); - connect(_noteLinkAction, &QAction::triggered, this, &ShareUserLine::toggleNoteOptions); - if (!_share->getNote().isEmpty()) { - _noteLinkAction->setChecked(true); - showNoteOptions(true); - } - } - - showExpireDateOptions(false); - - const bool isExpirationDateSupported = _share->getShareType() != Share::ShareType::TypeEmail; - - if (isExpirationDateSupported) { - // email shares do not support expiration dates - _expirationDateLinkAction = new QAction(tr("Set expiration date")); - _expirationDateLinkAction->setCheckable(true); - menu->addAction(_expirationDateLinkAction); - connect(_expirationDateLinkAction, &QAction::triggered, this, &ShareUserLine::toggleExpireDateOptions); - const auto expireDate = _share->getExpireDate().isValid() ? share.data()->getExpireDate() : QDate(); - if (!expireDate.isNull()) { - _expirationDateLinkAction->setChecked(true); - showExpireDateOptions(true, expireDate); - } - } - - menu->addSeparator(); - - // Adds action to delete share widget - QIcon deleteicon = QIcon::fromTheme(QLatin1String("user-trash"),QIcon(QLatin1String(":/client/theme/delete.svg"))); - _deleteShareButton= new QAction(deleteicon,tr("Unshare"), this); - - menu->addAction(_deleteShareButton); - connect(_deleteShareButton, &QAction::triggered, this, &ShareUserLine::on_deleteShareButton_clicked); - - /* - * Files can't have create or delete permissions - */ - if (!_isFile) { - _permissionCreate = new QAction(tr("Can create"), this); - _permissionCreate->setCheckable(true); - _permissionCreate->setEnabled(maxSharingPermissions & SharePermissionCreate); - menu->addAction(_permissionCreate); - connect(_permissionCreate, &QAction::triggered, this, &ShareUserLine::slotPermissionsChanged); - - _permissionChange = new QAction(tr("Can change"), this); - _permissionChange->setCheckable(true); - _permissionChange->setEnabled(maxSharingPermissions & SharePermissionUpdate); - menu->addAction(_permissionChange); - connect(_permissionChange, &QAction::triggered, this, &ShareUserLine::slotPermissionsChanged); - - _permissionDelete = new QAction(tr("Can delete"), this); - _permissionDelete->setCheckable(true); - _permissionDelete->setEnabled(maxSharingPermissions & SharePermissionDelete); - menu->addAction(_permissionDelete); - connect(_permissionDelete, &QAction::triggered, this, &ShareUserLine::slotPermissionsChanged); - } - - // Adds action to display password widget (check box) - if (_share->getShareType() == Share::TypeEmail && (_share->isPasswordSet() || _account->capabilities().shareEmailPasswordEnabled())) { - _passwordProtectLinkAction = new QAction(tr("Password protect"), this); - _passwordProtectLinkAction->setCheckable(true); - _passwordProtectLinkAction->setChecked(_share->isPasswordSet()); - // checkbox can be checked/unchedkec if the password is not yet set or if it's not enforced - _passwordProtectLinkAction->setEnabled(!_share->isPasswordSet() || !_account->capabilities().shareEmailPasswordEnforced()); - - menu->addAction(_passwordProtectLinkAction); - connect(_passwordProtectLinkAction, &QAction::triggered, this, &ShareUserLine::slotPasswordCheckboxChanged); - - refreshPasswordLineEditPlaceholder(); - - connect(_share.data(), &Share::passwordSet, this, &ShareUserLine::slotPasswordSet); - connect(_share.data(), &Share::passwordSetError, this, &ShareUserLine::slotPasswordSetError); - } - - refreshPasswordOptions(); - - _ui->errorLabel->hide(); - - _ui->permissionToolButton->setMenu(menu); - _ui->permissionToolButton->setPopupMode(QToolButton::InstantPopup); - - _ui->passwordProgressIndicator->setVisible(false); - - // Set the permissions checkboxes - displayPermissions(); - - /* - * We don't show permission share for federated shares with server <9.1 - * https://github.com/owncloud/core/issues/22122#issuecomment-185637344 - * https://github.com/owncloud/client/issues/4996 - */ - if (share->getShareType() == Share::TypeRemote - && share->account()->serverVersionInt() < Account::makeServerVersion(9, 1, 0)) { - _permissionReshare->setVisible(false); - _ui->permissionToolButton->setVisible(false); - } - - connect(share.data(), &Share::permissionsSet, this, &ShareUserLine::slotPermissionsSet); - connect(share.data(), &Share::shareDeleted, this, &ShareUserLine::slotShareDeleted); - - if (!share->account()->capabilities().shareResharing()) { - _permissionReshare->setVisible(false); - } - - const auto avatarEventFilter = new AvatarEventFilter(_ui->avatar); - connect(avatarEventFilter, &AvatarEventFilter::contextMenu, this, &ShareUserLine::onAvatarContextMenu); - _ui->avatar->installEventFilter(avatarEventFilter); - - loadAvatar(); - - customizeStyle(); -} - -void ShareUserLine::onAvatarContextMenu(const QPoint &globalPosition) -{ - if (_share->getShareType() == Share::TypeUser) { - _profilePageMenu.exec(globalPosition); - } -} - -void ShareUserLine::loadAvatar() -{ - const int avatarSize = 36; - - // Set size of the placeholder - _ui->avatar->setMinimumHeight(avatarSize); - _ui->avatar->setMinimumWidth(avatarSize); - _ui->avatar->setMaximumHeight(avatarSize); - _ui->avatar->setMaximumWidth(avatarSize); - _ui->avatar->setAlignment(Qt::AlignCenter); - - setDefaultAvatar(avatarSize); - - /* Start the network job to fetch the avatar data. - * - * Currently only regular users can have avatars. - */ - if (_share->getShareWith()->type() == Sharee::User) { - auto *job = new AvatarJob(_share->account(), _share->getShareWith()->shareWith(), avatarSize, this); - connect(job, &AvatarJob::avatarPixmap, this, &ShareUserLine::slotAvatarLoaded); - job->start(); - } -} - -void ShareUserLine::setDefaultAvatar(int avatarSize) -{ - /* Create the fallback avatar. - * - * This will be shown until the avatar image data arrives. - */ - - // See core/js/placeholder.js for details on colors and styling - const auto backgroundColor = backgroundColorForShareeType(_share->getShareWith()->type()); - const QString style = QString(R"(* { - color: #fff; - background-color: %1; - border-radius: %2px; - text-align: center; - line-height: %2px; - font-size: %2px; - })").arg(backgroundColor.name(), QString::number(avatarSize / 2)); - _ui->avatar->setStyleSheet(style); - - const auto pixmap = pixmapForShareeType(_share->getShareWith()->type(), backgroundColor); - - if (!pixmap.isNull()) { - _ui->avatar->setPixmap(pixmap); - } else { - qCDebug(lcSharing) << "pixmap is null for share type: " << _share->getShareWith()->type(); - - // The avatar label is the first character of the user name. - const auto text = _share->getShareWith()->displayName(); - _ui->avatar->setText(text.at(0).toUpper()); - } -} - -void ShareUserLine::slotAvatarLoaded(QImage avatar) -{ - if (avatar.isNull()) - return; - - avatar = AvatarJob::makeCircularAvatar(avatar); - _ui->avatar->setPixmap(QPixmap::fromImage(avatar)); - - // Remove the stylesheet for the fallback avatar - _ui->avatar->setStyleSheet(""); -} - -void ShareUserLine::on_deleteShareButton_clicked() -{ - setEnabled(false); - _share->deleteShare(); -} - -ShareUserLine::~ShareUserLine() -{ - delete _ui; -} - -void ShareUserLine::slotEditPermissionsChanged() -{ - setEnabled(false); - - // Can never manually be set to "partial". - // This works because the state cycle for clicking is - // unchecked -> partial -> checked -> unchecked. - if (_ui->permissionsEdit->checkState() == Qt::PartiallyChecked) { - _ui->permissionsEdit->setCheckState(Qt::Checked); - } - - Share::Permissions permissions = SharePermissionRead; - - // folders edit = CREATE, READ, UPDATE, DELETE - // files edit = READ + UPDATE - if (_ui->permissionsEdit->checkState() == Qt::Checked) { - - /* - * Files can't have create or delete permisisons - */ - if (!_isFile) { - if (_permissionChange->isEnabled()) - permissions |= SharePermissionUpdate; - if (_permissionCreate->isEnabled()) - permissions |= SharePermissionCreate; - if (_permissionDelete->isEnabled()) - permissions |= SharePermissionDelete; - } else { - permissions |= SharePermissionUpdate; - } - } - - if(_isFile && _permissionReshare->isEnabled() && _permissionReshare->isChecked()) - permissions |= SharePermissionShare; - - _share->setPermissions(permissions); -} - -void ShareUserLine::slotPermissionsChanged() -{ - setEnabled(false); - - Share::Permissions permissions = SharePermissionRead; - - if (_permissionReshare->isChecked()) - permissions |= SharePermissionShare; - - if (!_isFile) { - if (_permissionChange->isChecked()) - permissions |= SharePermissionUpdate; - if (_permissionCreate->isChecked()) - permissions |= SharePermissionCreate; - if (_permissionDelete->isChecked()) - permissions |= SharePermissionDelete; - } else { - if (_ui->permissionsEdit->isChecked()) - permissions |= SharePermissionUpdate; - } - - _share->setPermissions(permissions); -} - -void ShareUserLine::slotPasswordCheckboxChanged() -{ - if (!_passwordProtectLinkAction->isChecked()) { - _ui->errorLabel->hide(); - _ui->errorLabel->clear(); - - if (!_share->isPasswordSet()) { - _ui->lineEdit_password->clear(); - refreshPasswordOptions(); - } else { - // do not call refreshPasswordOptions here, as it will be called after the network request is complete - togglePasswordSetProgressAnimation(true); - _share->setPassword(QString()); - } - } else { - refreshPasswordOptions(); - - if (_ui->lineEdit_password->isVisible() && _ui->lineEdit_password->isEnabled()) { - focusPasswordLineEdit(); - } - } -} - -void ShareUserLine::slotDeleteAnimationFinished() -{ - emit resizeRequested(); - emit visualDeletionDone(); - deleteLater(); - - // There is a painting bug where a small line of this widget isn't - // properly cleared. This explicit repaint() call makes sure any trace of - // the share widget is removed once it's destroyed. #4189 - connect(this, SIGNAL(destroyed(QObject *)), parentWidget(), SLOT(repaint())); -} - -void ShareUserLine::refreshPasswordOptions() -{ - const bool isPasswordEnabled = _share->getShareType() == Share::TypeEmail && _passwordProtectLinkAction->isChecked(); - - _ui->passwordLabel->setVisible(isPasswordEnabled); - _ui->lineEdit_password->setEnabled(isPasswordEnabled); - _ui->lineEdit_password->setVisible(isPasswordEnabled); - _ui->confirmPassword->setVisible(isPasswordEnabled); - - emit resizeRequested(); -} - -void ShareUserLine::refreshPasswordLineEditPlaceholder() -{ - if (_share->isPasswordSet()) { - _ui->lineEdit_password->setPlaceholderText(QString::fromUtf8(passwordIsSetPlaceholder)); - } else { - _ui->lineEdit_password->setPlaceholderText(""); - } -} - -void ShareUserLine::slotPasswordSet() -{ - togglePasswordSetProgressAnimation(false); - _ui->lineEdit_password->setEnabled(true); - _ui->confirmPassword->setEnabled(true); - - _ui->lineEdit_password->setText(""); - - _passwordProtectLinkAction->setEnabled(!_share->isPasswordSet() || !_account->capabilities().shareEmailPasswordEnforced()); - - refreshPasswordLineEditPlaceholder(); - - refreshPasswordOptions(); -} - -void ShareUserLine::slotPasswordSetError(int statusCode, const QString &message) -{ - qCWarning(lcSharing) << "Error from server" << statusCode << message; - - togglePasswordSetProgressAnimation(false); - - _ui->lineEdit_password->setEnabled(true); - _ui->confirmPassword->setEnabled(true); - - refreshPasswordLineEditPlaceholder(); - - refreshPasswordOptions(); - - focusPasswordLineEdit(); - - _ui->errorLabel->show(); - _ui->errorLabel->setText(message); - - emit resizeRequested(); -} - -void ShareUserLine::slotShareDeleted() -{ - auto *animation = new QPropertyAnimation(this, "maximumHeight", this); - - animation->setDuration(500); - animation->setStartValue(height()); - animation->setEndValue(0); - - connect(animation, &QAbstractAnimation::finished, this, &ShareUserLine::slotDeleteAnimationFinished); - connect(animation, &QVariantAnimation::valueChanged, this, &ShareUserLine::resizeRequested); - - animation->start(); -} - -void ShareUserLine::slotPermissionsSet() -{ - displayPermissions(); - setEnabled(true); -} - -QSharedPointer ShareUserLine::share() const -{ - return _share; -} - -void ShareUserLine::displayPermissions() -{ - auto perm = _share->getPermissions(); - -// folders edit = CREATE, READ, UPDATE, DELETE -// files edit = READ + UPDATE - if (perm & SharePermissionUpdate && (_isFile || - (perm & SharePermissionCreate && perm & SharePermissionDelete))) { - _ui->permissionsEdit->setCheckState(Qt::Checked); - } else if (!_isFile && perm & (SharePermissionUpdate | SharePermissionCreate | SharePermissionDelete)) { - _ui->permissionsEdit->setCheckState(Qt::PartiallyChecked); - } else if(perm & SharePermissionRead) { - _ui->permissionsEdit->setCheckState(Qt::Unchecked); - } - -// edit is independent of reshare - if (perm & SharePermissionShare) - _permissionReshare->setChecked(true); - - if(!_isFile){ - _permissionCreate->setChecked(perm & SharePermissionCreate); - _permissionChange->setChecked(perm & SharePermissionUpdate); - _permissionDelete->setChecked(perm & SharePermissionDelete); - } -} - -void ShareUserLine::slotStyleChanged() -{ - customizeStyle(); -} - -void ShareUserLine::focusPasswordLineEdit() -{ - _ui->lineEdit_password->setFocus(); -} - -void ShareUserLine::customizeStyle() -{ - _ui->permissionToolButton->setIcon(Theme::createColorAwareIcon(":/client/theme/more.svg")); - - QIcon deleteicon = QIcon::fromTheme(QLatin1String("user-trash"),Theme::createColorAwareIcon(QLatin1String(":/client/theme/delete.svg"))); - _deleteShareButton->setIcon(deleteicon); - - _ui->noteConfirmButton->setIcon(Theme::createColorAwareIcon(":/client/theme/confirm.svg")); - _ui->progressIndicator->setColor(QGuiApplication::palette().color(QPalette::WindowText)); - - // make sure to force BackgroundRole to QPalette::WindowText for a lable, because it's parent always has a different role set that applies to children unless customized - _ui->errorLabel->setBackgroundRole(QPalette::WindowText); -} - -QPixmap ShareUserLine::pixmapForShareeType(Sharee::Type type, const QColor &backgroundColor) const -{ - switch (type) { - case Sharee::Room: - return Ui::IconUtils::pixmapForBackground(QStringLiteral("talk-app.svg"), backgroundColor); - case Sharee::Email: - return Ui::IconUtils::pixmapForBackground(QStringLiteral("email.svg"), backgroundColor); - case Sharee::Group: - case Sharee::Federated: - case Sharee::Circle: - case Sharee::User: - break; - } - - return {}; -} - -QColor ShareUserLine::backgroundColorForShareeType(Sharee::Type type) const -{ - switch (type) { - case Sharee::Room: - return Theme::instance()->wizardHeaderBackgroundColor(); - case Sharee::Email: - return Theme::instance()->wizardHeaderTitleColor(); - case Sharee::Group: - case Sharee::Federated: - case Sharee::Circle: - case Sharee::User: - break; - } - - const auto calculateBackgroundBasedOnText = [this]() { - const auto hash = QCryptographicHash::hash(_ui->sharedWith->text().toUtf8(), QCryptographicHash::Md5); - Q_ASSERT(hash.size() > 0); - if (hash.size() == 0) { - qCWarning(lcSharing) << "Failed to calculate hash color for share:" << _share->path(); - return QColor{}; - } - const double hue = static_cast(hash[0]) / 255.; - return QColor::fromHslF(hue, 0.7, 0.68); - }; - - return calculateBackgroundBasedOnText(); -} - -void ShareUserLine::showNoteOptions(bool show) -{ - _ui->noteLabel->setVisible(show); - _ui->noteTextEdit->setVisible(show); - _ui->noteConfirmButton->setVisible(show); - - if (show) { - const auto note = _share->getNote(); - _ui->noteTextEdit->setText(note); - _ui->noteTextEdit->setFocus(); - } - - emit resizeRequested(); -} - - -void ShareUserLine::toggleNoteOptions(bool enable) -{ - showNoteOptions(enable); - - if (!enable) { - // Delete note - _share->setNote(QString()); - } -} - -void ShareUserLine::onNoteConfirmButtonClicked() -{ - setNote(_ui->noteTextEdit->toPlainText()); -} - -void ShareUserLine::setNote(const QString ¬e) -{ - enableProgessIndicatorAnimation(true); - _share->setNote(note); -} - -void ShareUserLine::toggleExpireDateOptions(bool enable) -{ - showExpireDateOptions(enable); - - if (!enable) { - _share->setExpireDate(QDate()); - } -} - -void ShareUserLine::showExpireDateOptions(bool show, const QDate &initialDate) -{ - _ui->expirationLabel->setVisible(show); - _ui->calendar->setVisible(show); - - if (show) { - _ui->calendar->setMinimumDate(QDate::currentDate().addDays(1)); - _ui->calendar->setDate(initialDate.isValid() ? initialDate : _ui->calendar->minimumDate()); - _ui->calendar->setFocus(); - - if (enforceExpirationDateForShare(_share->getShareType())) { - _ui->calendar->setMaximumDate(maxExpirationDateForShare(_share->getShareType(), _ui->calendar->maximumDate())); - _expirationDateLinkAction->setChecked(true); - _expirationDateLinkAction->setEnabled(false); - } - } - - emit resizeRequested(); -} - -void ShareUserLine::setExpireDate() -{ - enableProgessIndicatorAnimation(true); - _share->setExpireDate(_ui->calendar->date()); -} - -void ShareUserLine::enableProgessIndicatorAnimation(bool enable) -{ - if (enable) { - if (!_ui->progressIndicator->isAnimated()) { - _ui->progressIndicator->startAnimation(); - } - } else { - _ui->progressIndicator->stopAnimation(); - } -} - -void ShareUserLine::togglePasswordSetProgressAnimation(bool show) -{ - // button and progress indicator are interchanged depending on if the network request is in progress or not - _ui->confirmPassword->setVisible(!show && _passwordProtectLinkAction->isChecked()); - _ui->passwordProgressIndicator->setVisible(show); - if (show) { - if (!_ui->passwordProgressIndicator->isAnimated()) { - _ui->passwordProgressIndicator->startAnimation(); - } - } else { - _ui->passwordProgressIndicator->stopAnimation(); - } -} - -void ShareUserLine::disableProgessIndicatorAnimation() -{ - enableProgessIndicatorAnimation(false); -} - -QDate ShareUserLine::maxExpirationDateForShare(const Share::ShareType type, const QDate &fallbackDate) const -{ - auto daysToExpire = 0; - if (type == Share::ShareType::TypeRemote) { - daysToExpire = _account->capabilities().shareRemoteExpireDateDays(); - } else if (type == Share::ShareType::TypeEmail) { - daysToExpire = _account->capabilities().sharePublicLinkExpireDateDays(); - } else { - daysToExpire = _account->capabilities().shareInternalExpireDateDays(); - } - - if (daysToExpire > 0) { - return QDate::currentDate().addDays(daysToExpire); - } - - return fallbackDate; -} - -bool ShareUserLine::enforceExpirationDateForShare(const Share::ShareType type) const -{ - if (type == Share::ShareType::TypeRemote) { - return _account->capabilities().shareRemoteEnforceExpireDate(); - } else if (type == Share::ShareType::TypeEmail) { - return _account->capabilities().sharePublicLinkEnforceExpireDate(); - } - - return _account->capabilities().shareInternalEnforceExpireDate(); -} - -void ShareUserLine::setPasswordConfirmed() -{ - if (_ui->lineEdit_password->text().isEmpty()) { - return; - } - - _ui->lineEdit_password->setEnabled(false); - _ui->confirmPassword->setEnabled(false); - - _ui->errorLabel->hide(); - _ui->errorLabel->clear(); - - togglePasswordSetProgressAnimation(true); - _share->setPassword(_ui->lineEdit_password->text()); -} - -void ShareUserLine::slotLineEditPasswordReturnPressed() -{ - setPasswordConfirmed(); -} - -void ShareUserLine::slotConfirmPasswordClicked() -{ - setPasswordConfirmed(); -} -} diff --git a/src/gui/shareusergroupwidget.h b/src/gui/shareusergroupwidget.h deleted file mode 100644 index 96132005e..000000000 --- a/src/gui/shareusergroupwidget.h +++ /dev/null @@ -1,236 +0,0 @@ -/* - * Copyright (C) by Roeland Jago Douma - * - * 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. - */ - -#ifndef SHAREUSERGROUPWIDGET_H -#define SHAREUSERGROUPWIDGET_H - -#include "accountfwd.h" -#include "sharemanager.h" -#include "sharepermissions.h" -#include "sharee.h" -#include "profilepagewidget.h" -#include "QProgressIndicator.h" -#include -#include -#include -#include -#include -#include -#include -#include - -class QAction; -class QCompleter; -class QModelIndex; - -namespace OCC { - -namespace Ui { - class ShareUserGroupWidget; - class ShareUserLine; -} - -class AbstractCredentials; -class SyncResult; -class Share; -class ShareManager; - -class AvatarEventFilter : public QObject -{ - Q_OBJECT - -public: - explicit AvatarEventFilter(QObject *parent = nullptr); - -signals: - void clicked(); - void contextMenu(const QPoint &globalPosition); - -protected: - bool eventFilter(QObject *obj, QEvent *event) override; -}; - -/** - * @brief The ShareDialog (user/group) class - * @ingroup gui - */ -class ShareUserGroupWidget : public QWidget -{ - Q_OBJECT - -public: - explicit ShareUserGroupWidget(AccountPtr account, - const QString &sharePath, - const QString &localPath, - SharePermissions maxSharingPermissions, - const QString &privateLinkUrl, - QWidget *parent = nullptr); - ~ShareUserGroupWidget() override; - - QVBoxLayout *shareUserGroupLayout(); - -signals: - void togglePublicLinkShare(bool); - void styleChanged(); - -public slots: - void getShares(); - void slotShareCreated(const QSharedPointer &share); - void slotStyleChanged(); - -private slots: - void slotSharesFetched(const QList> &shares); - - void on_shareeLineEdit_textChanged(const QString &text); - void searchForSharees(ShareeModel::LookupMode lookupMode); - void slotLineEditTextEdited(const QString &text); - - void slotLineEditReturn(); - void slotCompleterActivated(const QModelIndex &index); - void slotCompleterHighlighted(const QModelIndex &index); - void slotShareesReady(); - void slotPrivateLinkShare(); - void displayError(int code, const QString &message); - - void slotPrivateLinkOpenBrowser(); - void slotPrivateLinkCopy(); - void slotPrivateLinkEmail(); - -private: - void customizeStyle(); - - void activateShareeLineEdit(); - - Ui::ShareUserGroupWidget *_ui; - QScopedPointer _searchGloballyAction; - QScrollArea *_parentScrollArea; - QVBoxLayout *_shareUserGroup; - AccountPtr _account; - QString _sharePath; - QString _localPath; - SharePermissions _maxSharingPermissions; - QString _privateLinkUrl; - - QCompleter *_completer; - ShareeModel *_completerModel; - QTimer _completionTimer; - - bool _isFile; - bool _disableCompleterActivated; // in order to avoid that we share the contents twice - ShareManager *_manager; - - QProgressIndicator _pi_sharee; - - QString _lastCreatedShareId; -}; - -/** - * The widget displayed for each user/group share - */ -class ShareUserLine : public QWidget -{ - Q_OBJECT - -public: - explicit ShareUserLine(AccountPtr account, - QSharedPointer Share, - SharePermissions maxSharingPermissions, - bool isFile, - QWidget *parent = nullptr); - ~ShareUserLine() override; - - [[nodiscard]] QSharedPointer share() const; - -signals: - void visualDeletionDone(); - void resizeRequested(); - -public slots: - void slotStyleChanged(); - - void focusPasswordLineEdit(); - -private slots: - void on_deleteShareButton_clicked(); - void slotPermissionsChanged(); - void slotEditPermissionsChanged(); - void slotPasswordCheckboxChanged(); - void slotDeleteAnimationFinished(); - - void refreshPasswordOptions(); - - void refreshPasswordLineEditPlaceholder(); - - void slotPasswordSet(); - void slotPasswordSetError(int statusCode, const QString &message); - - void slotShareDeleted(); - void slotPermissionsSet(); - - void slotAvatarLoaded(QImage avatar); - - void setPasswordConfirmed(); - - void slotLineEditPasswordReturnPressed(); - - void slotConfirmPasswordClicked(); - - void onAvatarContextMenu(const QPoint &globalPosition); - -private: - void displayPermissions(); - void loadAvatar(); - void setDefaultAvatar(int avatarSize); - void customizeStyle(); - - [[nodiscard]] QPixmap pixmapForShareeType(Sharee::Type type, const QColor &backgroundColor = QColor()) const; - [[nodiscard]] QColor backgroundColorForShareeType(Sharee::Type type) const; - - void showNoteOptions(bool show); - void toggleNoteOptions(bool enable); - void onNoteConfirmButtonClicked(); - void setNote(const QString ¬e); - - void toggleExpireDateOptions(bool enable); - void showExpireDateOptions(bool show, const QDate &initialDate = QDate()); - void setExpireDate(); - - void togglePasswordSetProgressAnimation(bool show); - - void enableProgessIndicatorAnimation(bool enable); - void disableProgessIndicatorAnimation(); - - [[nodiscard]] QDate maxExpirationDateForShare(const Share::ShareType type, const QDate &fallbackDate) const; - [[nodiscard]] bool enforceExpirationDateForShare(const Share::ShareType type) const; - - Ui::ShareUserLine *_ui; - AccountPtr _account; - QSharedPointer _share; - bool _isFile; - - ProfilePageMenu _profilePageMenu; - - // _permissionEdit is a checkbox - QAction *_permissionReshare; - QAction *_deleteShareButton; - QAction *_permissionCreate; - QAction *_permissionChange; - QAction *_permissionDelete; - QAction *_noteLinkAction; - QAction *_expirationDateLinkAction; - QAction *_passwordProtectLinkAction; -}; -} - -#endif // SHAREUSERGROUPWIDGET_H diff --git a/src/gui/shareusergroupwidget.ui b/src/gui/shareusergroupwidget.ui deleted file mode 100644 index 38c45a23d..000000000 --- a/src/gui/shareusergroupwidget.ui +++ /dev/null @@ -1,154 +0,0 @@ - - - OCC::ShareUserGroupWidget - - - - 0 - 0 - 350 - 106 - - - - - 0 - 0 - - - - - 6 - - - 6 - - - 6 - - - 6 - - - 6 - - - - - - 0 - 0 - - - - - - - - - - - 6 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - Share with users or groups … - - - - - - - - :/client/theme/confirm.svg:/client/theme/confirm.svg - - - true - - - - - - - - - - 0 - 0 - - - - - - - - - 255 - 0 - 0 - - - - - - - - - 255 - 0 - 0 - - - - - - - - - 123 - 121 - 134 - - - - - - - - Placeholder for Error text - - - Qt::PlainText - - - true - - - - - - - - - - - diff --git a/src/gui/socketapi/socketapi.cpp b/src/gui/socketapi/socketapi.cpp index 4de3317a2..e023dd375 100644 --- a/src/gui/socketapi/socketapi.cpp +++ b/src/gui/socketapi/socketapi.cpp @@ -497,10 +497,10 @@ void SocketApi::broadcastMessage(const QString &msg, bool doWait) void SocketApi::processFileActivityRequest(const QString &localFile) { const auto fileData = FileData::get(localFile); - emit fileActivityCommandReceived(fileData.serverRelativePath, fileData.journalRecord().numericFileId().toInt()); + emit fileActivityCommandReceived(fileData.localPath); } -void SocketApi::processShareRequest(const QString &localFile, SocketListener *listener, ShareDialogStartPage startPage) +void SocketApi::processShareRequest(const QString &localFile, SocketListener *listener) { auto theme = Theme::instance(); @@ -537,7 +537,7 @@ void SocketApi::processShareRequest(const QString &localFile, SocketListener *li const QString message = QLatin1String("SHARE:OK:") + QDir::toNativeSeparators(localFile); listener->sendMessage(message); - emit shareCommandReceived(remotePath, fileData.localPath, startPage); + emit shareCommandReceived(fileData.localPath); } } @@ -581,7 +581,7 @@ void SocketApi::command_RETRIEVE_FILE_STATUS(const QString &argument, SocketList void SocketApi::command_SHARE(const QString &localFile, SocketListener *listener) { - processShareRequest(localFile, listener, ShareDialogStartPage::UsersAndGroups); + processShareRequest(localFile, listener); } void SocketApi::command_ACTIVITY(const QString &localFile, SocketListener *listener) @@ -593,7 +593,7 @@ void SocketApi::command_ACTIVITY(const QString &localFile, SocketListener *liste void SocketApi::command_MANAGE_PUBLIC_LINKS(const QString &localFile, SocketListener *listener) { - processShareRequest(localFile, listener, ShareDialogStartPage::PublicLinks); + processShareRequest(localFile, listener); } void SocketApi::command_VERSION(const QString &, SocketListener *listener) @@ -673,7 +673,7 @@ public: } private slots: - void sharesFetched(const QList> &shares) + void sharesFetched(const QList &shares) { auto shareName = SocketApi::tr("Context menu share"); @@ -783,7 +783,7 @@ void SocketApi::command_COPY_PUBLIC_LINK(const QString &localFile, SocketListene connect(job, &GetOrCreatePublicLinkShare::done, this, [](const QString &url) { copyUrlToClipboard(url); }); connect(job, &GetOrCreatePublicLinkShare::error, this, - [=]() { emit shareCommandReceived(fileData.serverRelativePath, fileData.localPath, ShareDialogStartPage::PublicLinks); }); + [=]() { emit shareCommandReceived(fileData.localPath); }); job->run(); } diff --git a/src/gui/socketapi/socketapi.h b/src/gui/socketapi/socketapi.h index f3529f870..5f16a00fc 100644 --- a/src/gui/socketapi/socketapi.h +++ b/src/gui/socketapi/socketapi.h @@ -17,7 +17,6 @@ #include "syncfileitem.h" #include "common/syncfilestatus.h" -#include "sharedialog.h" // for the ShareDialogStartPage #include "common/syncjournalfilerecord.h" #include "config.h" @@ -63,8 +62,8 @@ public slots: void broadcastStatusPushMessage(const QString &systemPath, SyncFileStatus fileStatus); signals: - void shareCommandReceived(const QString &sharePath, const QString &localPath, ShareDialogStartPage startPage); - void fileActivityCommandReceived(const QString &objectName, const int objectId); + void shareCommandReceived(const QString &localPath); + void fileActivityCommandReceived(const QString &localPath); private slots: void slotNewConnection(); @@ -102,7 +101,7 @@ private: void broadcastMessage(const QString &msg, bool doWait = false); // opens share dialog, sends reply - void processShareRequest(const QString &localFile, SocketListener *listener, ShareDialogStartPage startPage); + void processShareRequest(const QString &localFile, SocketListener *listener); void processFileActivityRequest(const QString &localFile); Q_INVOKABLE void command_RETRIEVE_FOLDER_STATUS(const QString &argument, SocketListener *listener); diff --git a/src/gui/systray.cpp b/src/gui/systray.cpp index e52f62bc8..d3cb9a22c 100644 --- a/src/gui/systray.cpp +++ b/src/gui/systray.cpp @@ -285,6 +285,91 @@ void Systray::destroyEditFileLocallyLoadingDialog() _editFileLocallyLoadingDialog = nullptr; } +bool Systray::raiseDialogs() +{ + if(_dialogs.empty()) { + return false; + } + + QVector> liveDialogs; + + for(const auto &dialog : _dialogs) { + if(dialog.isNull()) { + continue; + } else if(!dialog->isVisible()) { + destroyDialog(dialog.data()); + continue; + } + + liveDialogs.append(dialog); + + dialog->show(); + dialog->raise(); + dialog->requestActivate(); + } + + _dialogs = liveDialogs; + + // If it is empty then we have raised no dialogs, so return false (and viceversa) + return !liveDialogs.empty(); +} + +void Systray::createFileDetailsDialog(const QString &localPath) +{ + qCDebug(lcSystray) << "Opening new file details dialog for " << localPath; + + if(!_trayEngine) { + qCWarning(lcSystray) << "Could not open file details dialog for" << localPath << "as no tray engine was available"; + return; + } + + const auto folder = FolderMan::instance()->folderForPath(localPath); + if (!folder) { + qCWarning(lcSystray) << "Could not open file details dialog for" << localPath << "no responsible folder found"; + return; + } + + const QVariantMap initialProperties{ + {"accountState", QVariant::fromValue(folder->accountState())}, + {"localPath", localPath}, + }; + + const auto fileDetailsDialog = new QQmlComponent(_trayEngine, QStringLiteral("qrc:/qml/src/gui/filedetails/FileDetailsWindow.qml")); + + if (fileDetailsDialog && !fileDetailsDialog->isError()) { + const auto createdDialog = fileDetailsDialog->createWithInitialProperties(initialProperties); + const QSharedPointer dialog(qobject_cast(createdDialog)); + + if(dialog.isNull()) { + qCWarning(lcSystray) << "File details dialog window resulted in creation of object that was not a window!"; + return; + } + + _dialogs.append(dialog); + + dialog->show(); + dialog->raise(); + dialog->requestActivate(); + + } else if (fileDetailsDialog) { + qCWarning(lcSystray) << fileDetailsDialog->errorString(); + } else { + qCWarning(lcSystray) << "Unable to open share dialog for unknown reasons..."; + } +} + +void Systray::createShareDialog(const QString &localPath) +{ + createFileDetailsDialog(localPath); + Q_EMIT showFileDetailsPage(localPath, FileDetailsPage::Sharing); +} + +void Systray::createFileActivityDialog(const QString &localPath) +{ + createFileDetailsDialog(localPath); + Q_EMIT showFileDetailsPage(localPath, FileDetailsPage::Activity); +} + void Systray::slotCurrentUserChanged() { if (_trayEngine) { diff --git a/src/gui/systray.h b/src/gui/systray.h index 7350011fc..d890f9fb4 100644 --- a/src/gui/systray.h +++ b/src/gui/systray.h @@ -82,12 +82,17 @@ public: enum class WindowPosition { Default, Center }; Q_ENUM(WindowPosition); + enum class FileDetailsPage { Activity, Sharing }; + Q_ENUM(FileDetailsPage); + Q_REQUIRED_RESULT QString windowTitle() const; Q_REQUIRED_RESULT bool useNormalWindow() const; Q_REQUIRED_RESULT bool syncIsPaused() const; Q_REQUIRED_RESULT bool isOpen() const; + Q_REQUIRED_RESULT bool raiseDialogs(); + signals: void currentUserChanged(); void openAccountWizard(); @@ -95,8 +100,7 @@ signals: void openHelp(); void shutdown(); - void openShareDialog(const QString &sharePath, const QString &localPath); - void showFileActivityDialog(const QString &objectName, const int objectId); + void showFileDetailsPage(const QString &fileLocalPath, const FileDetailsPage page); void sendChatMessage(const QString &token, const QString &message, const QString &replyTo); void showErrorMessageDialog(const QString &error); @@ -132,6 +136,9 @@ public slots: void setSyncIsPaused(const bool syncIsPaused); void setIsOpen(const bool isOpen); + void createShareDialog(const QString &localPath); + void createFileActivityDialog(const QString &localPath); + private slots: void slotUnpauseAllFolders(); void slotPauseAllFolders(); @@ -143,6 +150,7 @@ private: Systray(); void setupContextMenu(); + void createFileDetailsDialog(const QString &localPath); [[nodiscard]] QScreen *currentScreen() const; [[nodiscard]] QRect currentScreenRect() const; @@ -164,8 +172,8 @@ private: AccessManagerFactory _accessManagerFactory; QSet _callsAlreadyNotified; - QPointer _editFileLocallyLoadingDialog; + QVector> _dialogs; }; } // namespace OCC diff --git a/src/gui/tray/ActivityItem.qml b/src/gui/tray/ActivityItem.qml index 020555ed3..eb06c7213 100644 --- a/src/gui/tray/ActivityItem.qml +++ b/src/gui/tray/ActivityItem.qml @@ -10,6 +10,8 @@ ItemDelegate { property Flickable flickable + property int iconSize: Style.trayListItemIconSize + property bool isFileActivityList: false readonly property bool isChatActivity: model.objectType === "chat" || model.objectType === "room" || model.objectType === "call" @@ -45,9 +47,11 @@ ItemDelegate { showDismissButton: model.links.length > 0 + iconSize: root.iconSize + activityData: model - onShareButtonClicked: Systray.openShareDialog(model.displayPath, model.path) + onShareButtonClicked: Systray.createShareDialog(model.openablePath) onDismissButtonClicked: activityModel.slotTriggerDismiss(model.activityIndex) } diff --git a/src/gui/tray/ActivityItemContent.qml b/src/gui/tray/ActivityItemContent.qml index 51c653312..80bc931fc 100644 --- a/src/gui/tray/ActivityItemContent.qml +++ b/src/gui/tray/ActivityItemContent.qml @@ -17,6 +17,8 @@ RowLayout { property bool childHovered: shareButton.hovered || dismissActionButton.hovered + property int iconSize: Style.trayListItemIconSize + signal dismissButtonClicked() signal shareButtonClicked() @@ -25,8 +27,8 @@ RowLayout { Item { id: thumbnailItem Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter - Layout.preferredWidth: Style.trayListItemIconSize - Layout.preferredHeight: model.thumbnail && model.thumbnail.isMimeTypeIcon ? Style.trayListItemIconSize * 0.9 : Style.trayListItemIconSize + Layout.preferredWidth: root.iconSize + Layout.preferredHeight: model.thumbnail && model.thumbnail.isMimeTypeIcon ? root.iconSize * 0.9 : root.iconSize readonly property int imageWidth: width * (1 - Style.thumbnailImageSizeReduction) readonly property int imageHeight: height * (1 - Style.thumbnailImageSizeReduction) readonly property int thumbnailRadius: model.thumbnail && model.thumbnail.isUserAvatar ? width / 2 : 3 diff --git a/src/gui/tray/ActivityList.qml b/src/gui/tray/ActivityList.qml index b108f3cca..d01c566e1 100644 --- a/src/gui/tray/ActivityList.qml +++ b/src/gui/tray/ActivityList.qml @@ -1,6 +1,7 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 +import Style 1.0 import com.nextcloud.desktopclient 1.0 as NC import Style 1.0 @@ -9,6 +10,7 @@ ScrollView { property alias model: sortedActivityList.activityListModel property bool isFileActivityList: false + property int iconSize: Style.trayListItemIconSize signal openFile(string filePath) signal activityItemClicked(int index) @@ -55,6 +57,7 @@ ScrollView { delegate: ActivityItem { isFileActivityList: controlRoot.isFileActivityList + iconSize: controlRoot.iconSize width: activityList.contentWidth flickable: activityList onHoveredChanged: if (hovered) { diff --git a/src/gui/tray/FileActivityDialog.qml b/src/gui/tray/FileActivityDialog.qml deleted file mode 100644 index 1d75ff222..000000000 --- a/src/gui/tray/FileActivityDialog.qml +++ /dev/null @@ -1,40 +0,0 @@ -import QtQml 2.15 -import QtQuick 2.15 -import QtQuick.Window 2.15 - -import Style 1.0 -import com.nextcloud.desktopclient 1.0 as NC - -Window { - id: dialog - - property alias model: activityModel - - NC.FileActivityListModel { - id: activityModel - } - - width: 500 - height: 500 - - Rectangle { - id: background - anchors.fill: parent - color: Style.backgroundColor - } - - ActivityList { - isFileActivityList: true - anchors.fill: parent - model: dialog.model - } - - Component.onCompleted: { - dialog.show(); - dialog.raise(); - dialog.requestActivate(); - - Systray.forceWindowInit(dialog); - Systray.positionWindowAtScreenCenter(dialog); - } -} diff --git a/src/gui/tray/Window.qml b/src/gui/tray/Window.qml index 94611c647..c462b305f 100644 --- a/src/gui/tray/Window.qml +++ b/src/gui/tray/Window.qml @@ -21,16 +21,8 @@ ApplicationWindow { color: "transparent" flags: Systray.useNormalWindow ? Qt.Window : Qt.Dialog | Qt.FramelessWindowHint - property int fileActivityDialogObjectId: -1 - readonly property int maxMenuHeight: Style.trayWindowHeight - Style.trayWindowHeaderHeight - 2 * Style.trayWindowBorderWidth - function openFileActivityDialog(objectName, objectId) { - fileActivityDialogLoader.objectName = objectName; - fileActivityDialogLoader.objectId = objectId; - fileActivityDialogLoader.refresh(); - } - Component.onCompleted: Systray.forceWindowInit(trayWindow) // Close tray window when focus is lost (e.g. click somewhere else on the screen) @@ -91,10 +83,6 @@ ApplicationWindow { } } - function onShowFileActivityDialog(objectName, objectId) { - openFileActivityDialog(objectName, objectId) - } - function onShowErrorMessageDialog(error) { var newErrorDialog = errorMessageDialog.createObject(trayWindow) newErrorDialog.text = error @@ -819,26 +807,5 @@ ApplicationWindow { model.slotTriggerDefaultAction(index) } } - - Loader { - id: fileActivityDialogLoader - - property string objectName: "" - property int objectId: -1 - - function refresh() { - active = true - item.model.load(activityModel.accountState, objectId) - item.show() - } - - active: false - sourceComponent: FileActivityDialog { - title: qsTr("%1 - File activity").arg(fileActivityDialogLoader.objectName) - onClosing: fileActivityDialogLoader.active = false - } - - onLoaded: refresh() - } } // Item trayWindowMainItem } diff --git a/src/gui/tray/activitylistmodel.cpp b/src/gui/tray/activitylistmodel.cpp index 8ebdcfef5..352f23a55 100644 --- a/src/gui/tray/activitylistmodel.cpp +++ b/src/gui/tray/activitylistmodel.cpp @@ -93,6 +93,7 @@ QHash ActivityListModel::roleNames() const void ActivityListModel::setAccountState(AccountState *state) { _accountState = state; + Q_EMIT accountStateChanged(); } void ActivityListModel::setCurrentItem(const int currentItem) diff --git a/src/gui/tray/activitylistmodel.h b/src/gui/tray/activitylistmodel.h index 1c070487d..e030445ed 100644 --- a/src/gui/tray/activitylistmodel.h +++ b/src/gui/tray/activitylistmodel.h @@ -39,9 +39,8 @@ class InvalidFilenameDialog; class ActivityListModel : public QAbstractListModel { Q_OBJECT - Q_PROPERTY(quint32 maxActionButtons READ maxActionButtons CONSTANT) - Q_PROPERTY(AccountState *accountState READ accountState CONSTANT) + Q_PROPERTY(AccountState *accountState READ accountState WRITE setAccountState NOTIFY accountStateChanged) public: enum DataRole { @@ -123,6 +122,8 @@ public slots: void setCurrentItem(const int currentItem); signals: + void accountStateChanged(); + void activityJobStatusCode(int statusCode); void sendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb, int row); diff --git a/src/gui/tray/asyncimageresponse.cpp b/src/gui/tray/asyncimageresponse.cpp index f393c837a..6bcee6778 100644 --- a/src/gui/tray/asyncimageresponse.cpp +++ b/src/gui/tray/asyncimageresponse.cpp @@ -63,17 +63,30 @@ void AsyncImageResponse::processNextImage() return; } - if (_imagePaths.at(_index).startsWith(QStringLiteral(":/client"))) { - setImageAndEmitFinished(QIcon(_imagePaths.at(_index)).pixmap(_requestedImageSize).toImage()); + const auto imagePath = _imagePaths.at(_index); + if (imagePath.startsWith(QStringLiteral(":/client"))) { + setImageAndEmitFinished(QIcon(imagePath).pixmap(_requestedImageSize).toImage()); + return; + } else if (imagePath.startsWith(QStringLiteral(":/fileicon"))) { + const auto filePath = imagePath.mid(10); + const auto fileInfo = QFileInfo(filePath); + setImageAndEmitFinished(_fileIconProvider.icon(fileInfo).pixmap(_requestedImageSize).toImage()); return; } - const auto currentUser = OCC::UserModel::instance()->currentUser(); - if (currentUser && currentUser->account()) { + OCC::AccountPtr accountInRequestedServer; + + for (const auto &account : OCC::AccountManager::instance()->accounts()) { + if (account && account->account() && imagePath.startsWith(account->account()->url().toString())) { + accountInRequestedServer = account->account(); + } + } + + if (accountInRequestedServer) { const QUrl iconUrl(_imagePaths.at(_index)); if (iconUrl.isValid() && !iconUrl.scheme().isEmpty()) { // fetch the remote resource - const auto reply = currentUser->account()->sendRawRequest(QByteArrayLiteral("GET"), iconUrl); + const auto reply = accountInRequestedServer->sendRawRequest(QByteArrayLiteral("GET"), iconUrl); connect(reply, &QNetworkReply::finished, this, &AsyncImageResponse::slotProcessNetworkReply); ++_index; return; diff --git a/src/gui/tray/asyncimageresponse.h b/src/gui/tray/asyncimageresponse.h index e27c7b99c..9d2d60003 100644 --- a/src/gui/tray/asyncimageresponse.h +++ b/src/gui/tray/asyncimageresponse.h @@ -16,6 +16,7 @@ #include #include +#include class AsyncImageResponse : public QQuickImageResponse { @@ -34,5 +35,6 @@ private slots: QStringList _imagePaths; QSize _requestedImageSize; QColor _svgRecolor; + QFileIconProvider _fileIconProvider; int _index = 0; };