Add a file details window/page, QMLify file sharing

Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
This commit is contained in:
Claudio Cambra 2022-07-25 18:57:18 +02:00
parent 1dbdd8853f
commit 7971789112
No known key found for this signature in database
GPG key ID: C839200C384636B0
52 changed files with 4040 additions and 4012 deletions

View file

@ -7,17 +7,24 @@
<file>src/gui/PredefinedStatusButton.qml</file>
<file>src/gui/BasicComboBox.qml</file>
<file>src/gui/ErrorBox.qml</file>
<file>src/gui/filedetails/FileActivityView.qml</file>
<file>src/gui/filedetails/FileDetailsPage.qml</file>
<file>src/gui/filedetails/FileDetailsWindow.qml</file>
<file>src/gui/filedetails/NCInputTextEdit.qml</file>
<file>src/gui/filedetails/NCInputTextField.qml</file>
<file>src/gui/filedetails/NCTabButton.qml</file>
<file>src/gui/filedetails/ShareeDelegate.qml</file>
<file>src/gui/filedetails/ShareDelegate.qml</file>
<file>src/gui/filedetails/ShareeSearchField.qml</file>
<file>src/gui/filedetails/ShareView.qml</file>
<file>src/gui/tray/Window.qml</file>
<file>src/gui/tray/UserLine.qml</file>
<file>src/gui/tray/HeaderButton.qml</file>
<file>src/gui/tray/SyncStatus.qml</file>
<file>theme/Style/Style.qml</file>
<file>theme/Style/qmldir</file>
<file>src/gui/tray/ActivityActionButton.qml</file>
<file>src/gui/tray/ActivityItem.qml</file>
<file>src/gui/tray/AutoSizingMenu.qml</file>
<file>src/gui/tray/ActivityList.qml</file>
<file>src/gui/tray/FileActivityDialog.qml</file>
<file>src/gui/tray/UnifiedSearchInputContainer.qml</file>
<file>src/gui/tray/UnifiedSearchResultFetchMoreTrigger.qml</file>
<file>src/gui/tray/UnifiedSearchResultItem.qml</file>
@ -39,5 +46,7 @@
<file>src/gui/tray/EditFileLocallyLoadingDialog.qml</file>
<file>src/gui/tray/NCBusyIndicator.qml</file>
<file>src/gui/tray/NCToolTip.qml</file>
<file>theme/Style/Style.qml</file>
<file>theme/Style/qmldir</file>
</qresource>
</RCC>

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,47 @@
/*
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import QtQuick 2.15
import QtQuick.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
}
}

View file

@ -0,0 +1,189 @@
/*
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import QtQuick 2.15
import QtQuick.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
}
}
}

View file

@ -0,0 +1,40 @@
/*
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import QtQuick 2.15
import QtQuick.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
}
}

View file

@ -0,0 +1,70 @@
/*
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import 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()
}
}

View file

@ -0,0 +1,65 @@
/*
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import 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()
}
}

View file

@ -0,0 +1,84 @@
/*
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import QtQuick 2.15
import QtQuick.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"
}
}
}

View file

@ -0,0 +1,762 @@
/*
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import QtQuick 2.15
import QtQuick.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()
}
}
}
}
}

View file

@ -0,0 +1,309 @@
/*
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import QtQuick 2.15
import QtQuick.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
}
}
}
}

View file

@ -0,0 +1,27 @@
/*
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import QtQuick 2.15
import QtQuick.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
}

View file

@ -0,0 +1,247 @@
/*
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import QtQuick 2.15
import QtQuick.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()
}
}
}
}
}

View file

@ -0,0 +1,150 @@
/*
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include <QDateTime>
#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<int>(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<int>(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<int>(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<int>(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<int>(elapsedSecs / secsInMonth);
return tr("%1 month(s) ago", "months elapsed since file last modified", elapsedMonths).arg(elapsedMonths);
} else {
const auto elapsedYears = static_cast<int>(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<int>(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

View file

@ -0,0 +1,75 @@
/*
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#pragma once
#include <QFileInfo>
#include <QFileSystemWatcher>
#include <QLocale>
#include <QTimer>
#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

View file

@ -0,0 +1,221 @@
/*
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include "shareemodel.h"
#include <QJsonObject>
#include <QJsonDocument>
#include <QJsonArray>
#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<int, QByteArray> 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<ShareePtr> 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));
}
}

View file

@ -0,0 +1,100 @@
/*
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#pragma once
#include <QAbstractListModel>
#include <QTimer>
#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<ShareePtr>; // FIXME: make it a QSet<Sharee> when Sharee can be compared
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QHash<int, QByteArray> 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<ShareePtr> _sharees;
QVector<ShareePtr> _shareeBlacklist;
};
}

View file

@ -0,0 +1,973 @@
/*
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include "sharemodel.h"
#include <QFileInfo>
#include <QTimeZone>
#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 &current, 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<int, QByteArray> 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<LinkShare>()) {
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<UserGroupShare>()) {
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<QByteArray>()
<< "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<SharePermissions>(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<SharePtr> &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<LinkShare>()) {
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<UserGroupShare>()) {
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<LinkShare>()) {
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<SharePtr>();
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<SharePtr>();
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<SharePtr>();
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>()) {
linkShare->setExpireDate(expireDate);
} else if (const auto userGroupShare = share.objectCast<UserGroupShare>()) {
userGroupShare->setExpireDate(expireDate);
}
}
void ShareModel::toggleShareExpirationDateFromQml(const QVariant &share, const bool enable) const
{
const auto ptr = share.value<SharePtr>();
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>()) {
linkShare->setNote(note);
} else if (const auto userGroupShare = share.objectCast<UserGroupShare>()) {
userGroupShare->setNote(note);
}
}
void ShareModel::toggleShareNoteToRecipientFromQml(const QVariant &share, const bool enable) const
{
const auto ptr = share.value<SharePtr>();
toggleShareNoteToRecipient(ptr, enable);
}
void ShareModel::setLinkShareLabel(const QSharedPointer<LinkShare> &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<SharePtr>().objectCast<LinkShare>();
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>()) {
linkShare->setExpireDate(date);
} else if (const auto userGroupShare = share.objectCast<UserGroupShare>()) {
userGroupShare->setExpireDate(date);
}
}
void ShareModel::setShareExpireDateFromQml(const QVariant &share, const QVariant milliseconds) const
{
const auto ptr = share.value<SharePtr>();
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<SharePtr>();
setSharePassword(ptr, password);
}
void ShareModel::setShareNote(const SharePtr &share, const QString &note) const
{
if (share.isNull()) {
return;
}
if (const auto linkShare = share.objectCast<LinkShare>()) {
linkShare->setNote(note);
} else if (const auto userGroupShare = share.objectCast<UserGroupShare>()) {
userGroupShare->setNote(note);
}
}
void ShareModel::setShareNoteFromQml(const QVariant &share, const QString &note) const
{
const auto ptr = share.value<SharePtr>();
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<ShareePtr>();
createNewUserGroupShare(ptr);
}
void ShareModel::createNewUserGroupShareWithPasswordFromQml(const QVariant &sharee, const QString &password) const
{
const auto ptr = sharee.value<ShareePtr>();
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<SharePtr>();
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

View file

@ -0,0 +1,205 @@
/*
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#pragma once
#include <QAbstractListModel>
#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<int, QByteArray> 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> &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 &note) const;
void setShareNoteFromQml(const QVariant &share, const QString &note) 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<SharePtr> &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<ShareManager> _manager;
QVector<SharePtr> _shares;
QHash<QString, QPersistentModelIndex> _shareIdIndexHash;
QHash<QString, QString> _shareIdRecentlySetPasswords;
};
} // namespace OCC

View file

@ -0,0 +1,114 @@
/*
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#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<ShareModel*>(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<SharePtr>();
const auto rightShare = sourceRight.data(ShareModel::ShareRole).value<SharePtr>();
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<LinkShare>();
const auto rightLinkShare = rightShare.objectCast<LinkShare>();
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

View file

@ -0,0 +1,45 @@
/*
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#pragma once
#include <QSortFilterProxyModel>
#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

View file

@ -24,6 +24,7 @@
#include "filesystem.h"
#include "lockwatcher.h"
#include "common/asserts.h"
#include "gui/systray.h"
#include <pushnotifications.h>
#include <syncengine.h>

View file

@ -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<SortedActivityListModel>("com.nextcloud.desktopclient", 1, 0, "SortedActivityListModel");
qmlRegisterType<WheelHandler>("com.nextcloud.desktopclient", 1, 0, "WheelHandler");
qmlRegisterType<CallStateChecker>("com.nextcloud.desktopclient", 1, 0, "CallStateChecker");
qmlRegisterType<FileDetails>("com.nextcloud.desktopclient", 1, 0, "FileDetails");
qmlRegisterType<ShareModel>("com.nextcloud.desktopclient", 1, 0, "ShareModel");
qmlRegisterType<ShareeModel>("com.nextcloud.desktopclient", 1, 0, "ShareeModel");
qmlRegisterType<SortedShareModel>("com.nextcloud.desktopclient", 1, 0, "SortedShareModel");
qmlRegisterUncreatableType<UnifiedSearchResultsListModel>("com.nextcloud.desktopclient", 1, 0, "UnifiedSearchResultsListModel", "UnifiedSearchResultsListModel");
qmlRegisterUncreatableType<UserStatus>("com.nextcloud.desktopclient", 1, 0, "UserStatus", "Access to Status enum");
@ -134,6 +134,8 @@ ownCloudGui::ownCloudGui(Application *parent)
qRegisterMetaType<ActivityListModel *>("ActivityListModel*");
qRegisterMetaType<UnifiedSearchResultsListModel *>("UnifiedSearchResultsListModel*");
qRegisterMetaType<UserStatus>("UserStatus");
qRegisterMetaType<SharePtr>("SharePtr");
qRegisterMetaType<ShareePtr>("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> &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<QString, QPointer<ShareDialog>> it(_shareDialogs);
while (it.hasNext()) {
it.next();
if (!it.value() || it.value() == sender()) {
it.remove();
}
}
_tray->createFileActivityDialog(localPath);
}
} // end namespace

View file

@ -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<QString, QPointer<ShareDialog>> _shareDialogs;
QAction *_actionNewAccountWizard;
QAction *_actionSettings;
QAction *_actionEstimate;

View file

@ -1,494 +0,0 @@
/*
* Copyright (C) by Roeland Jago Douma <roeland@famdouma.nl>
*
* 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 <QFileInfo>
#include <QFileIconProvider>
#include <QInputDialog>
#include <QPointer>
#include <QPushButton>
#include <QFrame>
#include <QScrollBar>
namespace {
QString createRandomPassword()
{
const auto words = OCC::WordList::getRandomWords(10);
const auto addFirstLetter = [](const QString &current, 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> 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<int>(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<QByteArray>()
<< "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> &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> &linkShare)
{
emit toggleShareLinkAnimation(true);
const auto addedLinkShareWidget = addLinkShareWidget(linkShare);
initLinkShareWidget();
if (linkShare->isPasswordSet()) {
addedLinkShareWidget->focusPasswordLineEdit();
}
emit toggleShareLinkAnimation(false);
}
void ShareDialog::slotSharesFetched(const QList<QSharedPointer<Share>> &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> linkShare = qSharedPointerDynamicCast<LinkShare>(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<SharePermissions>(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<ShareLinkWidget*>(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<ShareLinkWidget*>(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<ShareLinkWidget*>(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

View file

@ -1,114 +0,0 @@
/*
* Copyright (C) by Roeland Jago Douma <roeland@famdouma.nl>
*
* 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 <QSharedPointer>
#include <QPointer>
#include <QString>
#include <QDialog>
#include <QWidget>
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> 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<QSharedPointer<Share>> &shares);
void slotAddLinkShareWidget(const QSharedPointer<LinkShare> &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> &linkShare);
void initLinkShareWidget();
void adjustScrollWidget();
Ui::ShareDialog *_ui;
QPointer<AccountState> _accountState;
QString _sharePath;
QString _localPath;
SharePermissions _maxSharingPermissions;
QByteArray _numericFileId;
SyncJournalFileLockInfo _filelockState;
QString _privateLinkUrl;
ShareDialogStartPage _startPage;
ShareManager *_manager = nullptr;
QList<ShareLinkWidget*> _linkWidgetList;
ShareLinkWidget* _emptyShareLinkWidget = nullptr;
InternalLinkWidget* _internalLinkWidget = nullptr;
ShareUserGroupWidget *_userGroupWidget = nullptr;
QProgressIndicator *_progressIndicator = nullptr;
QWidget *_scrollAreaViewPort = nullptr;
QVBoxLayout *_scrollAreaLayout = nullptr;
};
} // namespace OCC
#endif // SHAREDIALOG_H

View file

@ -1,217 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>OCC::ShareDialog</class>
<widget class="QDialog" name="OCC::ShareDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>385</width>
<height>400</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>320</width>
<height>240</height>
</size>
</property>
<layout class="QVBoxLayout" name="shareDialogVerticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</property>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<item>
<layout class="QGridLayout" name="gridLayout" rowstretch="0,0,0,0" columnstretch="0,0">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="spacing">
<number>2</number>
</property>
<item row="0" column="1">
<widget class="QLabel" name="label_name">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>315</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>share label</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLabel" name="label_lockinfo">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>315</width>
<height>0</height>
</size>
</property>
<property name="text">
<string notr="true">TextLabel</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="label_sharePath">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>315</width>
<height>0</height>
</size>
</property>
<property name="font">
<font>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Nextcloud Path:</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="0" rowspan="4">
<widget class="QLabel" name="label_icon">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>40</width>
<height>40</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Icon</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QScrollArea" name="scrollArea">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarAsNeeded</enum>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::AdjustToContentsOnFirstShow</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>359</width>
<height>320</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -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<QSharedPointer<Sharee>> newSharees;
{
const QStringList shareeTypes {"users", "groups", "emails", "remotes", "circles", "rooms"};
const auto appendSharees = [this, &shareeTypes](const QJsonObject &data, QVector<QSharedPointer<Sharee>>& 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<QSharedPointer<Sharee>> 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<Sharee> 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<Sharee>(new Sharee(shareWith, displayName, type));
}
// Helper function for setNewSharees (could be a lambda when we can use them)
static QSharedPointer<Sharee> shareeFromModelIndex(const QModelIndex &idx)
{
return idx.data(Qt::UserRole).value<QSharedPointer<Sharee>>();
}
struct FindShareeHelper
{
const QSharedPointer<Sharee> &sharee;
bool operator()(const QSharedPointer<Sharee> &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<QSharedPointer<Sharee>> &newSharees)
{
layoutAboutToBeChanged();
const auto persistent = persistentIndexList();
QVector<QSharedPointer<Sharee>> 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> &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<Sharee> ShareeModel::getSharee(int at)
{
if (at < 0 || at > _sharees.size()) {
return QSharedPointer<Sharee>(nullptr);
}
return _sharees.at(at);
}
}

View file

@ -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<QSharedPointer<Sharee>>; // FIXME: make it a QSet<Sharee> 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<Sharee> 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<Sharee> parseSharee(const QJsonObject &data);
void setNewSharees(const QVector<QSharedPointer<Sharee>> &newSharees);
AccountPtr _account;
QString _search;
QString _type;
QVector<QSharedPointer<Sharee>> _sharees;
QVector<QSharedPointer<Sharee>> _shareeBlacklist;
};
using ShareePtr = QSharedPointer<OCC::Sharee>;
}
Q_DECLARE_METATYPE(QSharedPointer<OCC::Sharee>)
Q_DECLARE_METATYPE(OCC::ShareePtr)
#endif //SHAREE_H

View file

@ -1,625 +0,0 @@
/*
* Copyright (C) by Roeland Jago Douma <roeland@famdouma.nl>
* Copyright (C) 2015 by Klaas Freitag <freitag@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#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 <QBuffer>
#include <QClipboard>
#include <QFileInfo>
#include <QDesktopServices>
#include <QMessageBox>
#include <QMenu>
#include <QTextEdit>
#include <QToolButton>
#include <QPropertyAnimation>
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 = linkShare;
}
QSharedPointer<LinkShare> 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("<p>Do you really want to delete the public link share <i>%1</i>?</p>"
"<p>Note: This action cannot be undone.</p>")
.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()));
}
}
}

View file

@ -1,157 +0,0 @@
/*
* Copyright (C) by Roeland Jago Douma <roeland@famdouma.nl>
* Copyright (C) 2015 by Klaas Freitag <freitag@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#ifndef SHARELINKWIDGET_H
#define SHARELINKWIDGET_H
#include "accountfwd.h"
#include "sharepermissions.h"
#include "QProgressIndicator.h"
#include <QDialog>
#include <QSharedPointer>
#include <QList>
#include <QToolButton>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QWidgetAction>
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> linkShare);
QSharedPointer<LinkShare> 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> _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<QAction> _unshareLinkAction;
QScopedPointer<QAction> _addAnotherLinkAction;
QAction *_noteLinkAction;
QHBoxLayout *_shareLinkLayout{};
QLabel *_shareLinkLabel{};
ElidedLabel *_shareLinkElidedLabel{};
QLineEdit *_shareLinkEdit{};
QToolButton *_shareLinkButton{};
QProgressIndicator *_shareLinkProgressIndicator{};
QWidget *_shareLinkDefaultWidget{};
QWidgetAction *_shareLinkWidgetAction{};
};
}
#endif // SHARELINKWIDGET_H

View file

@ -1,439 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>OCC::ShareLinkWidget</class>
<widget class="QWidget" name="OCC::ShareLinkWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>238</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>12</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>20</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>6</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="shareLinkIconLabel">
<property name="text">
<string notr="true"/>
</property>
<property name="pixmap">
<pixmap resource="../../theme.qrc">:/client/theme/public.svg</pixmap>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="shareLinkLabel">
<property name="text">
<string>Share link</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>25</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QProgressIndicator" name="sharelinkProgressIndicator" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>28</width>
<height>27</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>25</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="enableShareLink">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../theme.qrc">
<normaloff>:/client/theme/add.svg</normaloff>:/client/theme/add.svg</iconset>
</property>
<property name="checkable">
<bool>false</bool>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="shareLinkToolButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="icon">
<iconset resource="../../theme.qrc">
<normaloff>:/client/theme/more.svg</normaloff>:/client/theme/more.svg</iconset>
</property>
<property name="popupMode">
<enum>QToolButton::InstantPopup</enum>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QGridLayout" name="gridLayout">
<property name="leftMargin">
<number>22</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="noteLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>78</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Note</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
<property name="indent">
<number>0</number>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QTextEdit" name="textEdit_note">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>60</height>
</size>
</property>
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::AdjustToContents</enum>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QToolButton" name="confirmNote">
<property name="minimumSize">
<size>
<width>28</width>
<height>27</height>
</size>
</property>
<property name="icon">
<iconset resource="../../theme.qrc">
<normaloff>:/client/theme/confirm.svg</normaloff>:/client/theme/confirm.svg</iconset>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QProgressIndicator" name="noteProgressIndicator" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>28</width>
<height>27</height>
</size>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="passwordLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>78</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Set password</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
<property name="indent">
<number>0</number>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="lineEdit_password">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QToolButton" name="confirmPassword">
<property name="minimumSize">
<size>
<width>28</width>
<height>27</height>
</size>
</property>
<property name="icon">
<iconset resource="../../theme.qrc">
<normaloff>:/client/theme/confirm.svg</normaloff>:/client/theme/confirm.svg</iconset>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QProgressIndicator" name="passwordProgressIndicator" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>28</width>
<height>27</height>
</size>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="expirationLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>78</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Expires</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
<property name="indent">
<number>0</number>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QDateEdit" name="calendar">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QToolButton" name="confirmExpirationDate">
<property name="minimumSize">
<size>
<width>28</width>
<height>27</height>
</size>
</property>
<property name="icon">
<iconset resource="../../theme.qrc">
<normaloff>:/client/theme/confirm.svg</normaloff>:/client/theme/confirm.svg</iconset>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QProgressIndicator" name="expirationDateProgressIndicator" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>28</width>
<height>27</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="errorLabel">
<property name="palette">
<palette>
<active>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
</active>
<inactive>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
</inactive>
<disabled>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>123</red>
<green>121</green>
<blue>134</blue>
</color>
</brush>
</colorrole>
</disabled>
</palette>
</property>
<property name="text">
<string notr="true">TextLabel</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<layoutdefault spacing="6" margin="11"/>
<customwidgets>
<customwidget>
<class>QProgressIndicator</class>
<extends>QWidget</extends>
<header>QProgressIndicator.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources>
<include location="../../theme.qrc"/>
</resources>
<connections/>
</ui>

View file

@ -58,7 +58,7 @@ Share::Share(AccountPtr account,
const ShareType shareType,
bool isPasswordSet,
const Permissions permissions,
const QSharedPointer<Sharee> shareWith)
const ShareePtr shareWith)
: _account(account)
, _id(id)
, _uidowner(uidowner)
@ -101,7 +101,7 @@ Share::ShareType Share::getShareType() const
return _shareType;
}
QSharedPointer<Sharee> 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<Sharee> shareWith,
const ShareePtr shareWith,
const QDate &expireDate,
const QString &note)
: 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> 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<QSharedPointer<Share>> shares;
QList<SharePtr> shares;
foreach (const auto &share, tmpShares) {
auto data = share.toObject();
auto shareType = data.value("share_type").toInt();
QSharedPointer<Share> 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<Share>(newShare));
shares.append(SharePtr(newShare));
}
qCDebug(lcSharing) << "Sending " << shares.count() << "shares";
@ -508,7 +508,7 @@ void ShareManager::slotSharesFetched(const QJsonDocument &reply)
QSharedPointer<UserGroupShare> ShareManager::parseUserGroupShare(const QJsonObject &data)
{
QSharedPointer<Sharee> 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<Sharee::Type>(data.value("share_type").toInt())));
@ -577,13 +577,13 @@ QSharedPointer<LinkShare> ShareManager::parseLinkShare(const QJsonObject &data)
data.value("label").toString()));
}
QSharedPointer<Share> ShareManager::parseShare(const QJsonObject &data)
SharePtr ShareManager::parseShare(const QJsonObject &data) const
{
QSharedPointer<Sharee> 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<Share>(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(),

View file

@ -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<Sharee> shareWith = QSharedPointer<Sharee>(nullptr));
const ShareePtr shareWith = ShareePtr(nullptr));
/**
* The account the share is defined on.
@ -97,13 +108,36 @@ public:
/*
* Get the shareWith
*/
[[nodiscard]] QSharedPointer<Sharee> 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<Sharee> _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<Share>;
/**
* 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 <typename LinkShareSlot>
OcsShareJob *createShareJob(const LinkShareSlot slotFunction);
public slots:
/*
* Set the name of the link share.
*
@ -233,16 +274,6 @@ public:
*/
void setNote(const QString &note);
/*
* 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 <typename LinkShareSlot>
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<Sharee> shareWith,
const ShareePtr shareWith,
const QDate &expireDate,
const QString &note);
void setNote(const QString &note);
[[nodiscard]] QString getNote() const;
void slotNoteSet(const QJsonDocument &, const QVariant &note);
void setExpireDate(const QDate &date);
[[nodiscard]] QDate getExpireDate() const;
void slotExpireDateSet(const QJsonDocument &reply, const QVariant &value);
public slots:
void setNote(const QString &note);
void setExpireDate(const QDate &date);
signals:
void noteSet();
void noteSetError();
void expireDateSet();
private slots:
void slotNoteSet(const QJsonDocument &json, const QVariant &note);
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> &share);
void shareCreated(const SharePtr &share);
void linkShareCreated(const QSharedPointer<LinkShare> &share);
void sharesFetched(const QList<QSharedPointer<Share>> &shares);
void sharesFetched(const QList<SharePtr> &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<LinkShare> parseLinkShare(const QJsonObject &data);
QSharedPointer<UserGroupShare> parseUserGroupShare(const QJsonObject &data);
QSharedPointer<Share> parseShare(const QJsonObject &data);
SharePtr parseShare(const QJsonObject &data) const;
AccountPtr _account;
};
}
Q_DECLARE_METATYPE(OCC::SharePtr);
#endif // SHAREMANAGER_H

File diff suppressed because it is too large Load diff

View file

@ -1,236 +0,0 @@
/*
* Copyright (C) by Roeland Jago Douma <roeland@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#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 <QDialog>
#include <QWidget>
#include <QSharedPointer>
#include <QList>
#include <QVector>
#include <QTimer>
#include <qpushbutton.h>
#include <qscrollarea.h>
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> &share);
void slotStyleChanged();
private slots:
void slotSharesFetched(const QList<QSharedPointer<Share>> &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<QAction> _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<UserGroupShare> Share,
SharePermissions maxSharingPermissions,
bool isFile,
QWidget *parent = nullptr);
~ShareUserLine() override;
[[nodiscard]] QSharedPointer<Share> 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 &note);
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<UserGroupShare> _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

View file

@ -1,154 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>OCC::ShareUserGroupWidget</class>
<widget class="QWidget" name="OCC::ShareUserGroupWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>350</width>
<height>106</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>6</number>
</property>
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item>
<widget class="QLabel" name="mainOwnerLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="shareeHorizontalLayout" stretch="0,0">
<property name="spacing">
<number>6</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLineEdit" name="shareeLineEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="placeholderText">
<string>Share with users or groups …</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="confirmShare">
<property name="icon">
<iconset resource="../../theme.qrc">
<normaloff>:/client/theme/confirm.svg</normaloff>:/client/theme/confirm.svg</iconset>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="errorLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Ignored" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="palette">
<palette>
<active>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
</active>
<inactive>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
</inactive>
<disabled>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>123</red>
<green>121</green>
<blue>134</blue>
</color>
</brush>
</colorrole>
</disabled>
</palette>
</property>
<property name="text">
<string notr="true">Placeholder for Error text</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<layoutdefault spacing="6" margin="11"/>
<resources>
<include location="../../theme.qrc"/>
</resources>
<connections/>
</ui>

View file

@ -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<QSharedPointer<Share>> &shares)
void sharesFetched(const QList<SharePtr> &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();
}

View file

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

View file

@ -285,6 +285,91 @@ void Systray::destroyEditFileLocallyLoadingDialog()
_editFileLocallyLoadingDialog = nullptr;
}
bool Systray::raiseDialogs()
{
if(_dialogs.empty()) {
return false;
}
QVector<QSharedPointer<QQuickWindow>> 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<QQuickWindow> dialog(qobject_cast<QQuickWindow*>(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) {

View file

@ -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<qlonglong> _callsAlreadyNotified;
QPointer<QObject> _editFileLocallyLoadingDialog;
QVector<QSharedPointer<QQuickWindow>> _dialogs;
};
} // namespace OCC

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -93,6 +93,7 @@ QHash<int, QByteArray> ActivityListModel::roleNames() const
void ActivityListModel::setAccountState(AccountState *state)
{
_accountState = state;
Q_EMIT accountStateChanged();
}
void ActivityListModel::setCurrentItem(const int currentItem)

View file

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

View file

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

View file

@ -16,6 +16,7 @@
#include <QImage>
#include <QQuickImageProvider>
#include <QFileIconProvider>
class AsyncImageResponse : public QQuickImageResponse
{
@ -34,5 +35,6 @@ private slots:
QStringList _imagePaths;
QSize _requestedImageSize;
QColor _svgRecolor;
QFileIconProvider _fileIconProvider;
int _index = 0;
};