Convert share details menu into a page pushed onto stackview
authorClaudio Cambra <claudio.cambra@nextcloud.com>
Fri, 18 Nov 2022 15:12:49 +0000 (16:12 +0100)
committerClaudio Cambra <claudio.cambra@gmail.com>
Fri, 9 Dec 2022 11:54:09 +0000 (12:54 +0100)
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
src/gui/filedetails/FileDetailsPage.qml
src/gui/filedetails/ShareDelegate.qml
src/gui/filedetails/ShareDetailsPage.qml [new file with mode: 0644]
src/gui/filedetails/ShareView.qml

index 304d1cee4982f3ebd69ab7d0dde4fa3b2005d56c..85eb57c3fff3a7a604897f11199564166614d731 100644 (file)
@@ -187,6 +187,7 @@ Page {
             fileDetails: root.fileDetails
             horizontalPadding: root.intendedPadding
             iconSize: root.iconSize
+            rootStackView: root.rootStackView
         }
     }
 }
index 885b78c6c6f42e619bfee611d6ca750aef536db1..d76268405f98eb1cbdb196f4a845c1849fff04f8 100644 (file)
@@ -29,6 +29,10 @@ GridLayout {
     signal deleteShare
     signal createNewLinkShare
 
+    signal resetMenu
+    signal resetPasswordField
+    signal showPasswordSetError(string errorMessage);
+
     signal toggleAllowEditing(bool enable)
     signal toggleAllowResharing(bool enable)
     signal togglePasswordProtect(bool enable)
@@ -40,130 +44,28 @@ GridLayout {
     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 FileDetails fileDetails: FileDetails {}
+    property StackView rootStackView: StackView {}
 
     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 showPasswordSetError(message) {
-        passwordErrorBoxLoader.message = message !== "" ?
-                                         message : qsTr("An error occurred setting the share password.");
-    }
-
-    function resetNoteField() {
-        noteTextEdit.text = note;
-        waitingForNoteChange = false;
-    }
-
-    function resetLinkShareLabelField() {
-        linkShareLabelTextField.text = linkShareLabel;
-        waitingForLinkShareLabelChange = false;
-    }
-
-    function resetPasswordField() {
-        passwordTextField.text = password !== "" ? password : passwordPlaceholder;
-        waitingForPasswordChange = false;
-    }
+    readonly property bool isLinkShare: model.shareType === ShareModel.ShareTypeLink
+    readonly property bool isPlaceholderLinkShare: model.shareType === ShareModel.ShareTypePlaceholderLink
 
-    function resetExpireDateField() {
-        // Expire date changing is handled by the expireDateSpinBox
-        waitingForExpireDateChange = false;
-    }
+    readonly property string text: model.display ?? ""
+    readonly property string detailText: model.detailText ?? ""
+    readonly property string iconUrl: model.iconUrl ?? ""
+    readonly property string avatarUrl: model.avatarUrl ?? ""
 
-    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()
+    anchors.left: parent.left
+    anchors.right: parent.right
 
-    // Reset value after property binding broken by user interaction
-    onNoteChanged: resetNoteField()
-    onPasswordChanged: resetPasswordField()
-    onLinkShareLabelChanged: resetLinkShareLabelField()
-    onExpireDateChanged: resetExpireDateField()
+    columns: 3
+    rows: linkDetailLabel.visible ? 1 : 2
 
-    onEditingAllowedChanged: resetEditingAllowedField()
-    onNoteEnabledChanged: resetNoteEnabledField()
-    onExpireDateEnabledChanged: resetExpireDateEnabledField()
-    onPasswordProtectEnabledChanged: resetPasswordProtectEnabledField()
+    columnSpacing: Style.standardSpacing / 2
+    rowSpacing: Style.standardSpacing / 2
 
     Item {
         id: imageItem
@@ -310,506 +212,50 @@ GridLayout {
             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
+            onClicked: root.rootStackView.push(shareDetailsPageComponent, {}, StackView.PushTransition)
 
-                        verticalAlignment: Image.AlignVCenter
-                        horizontalAlignment: Image.AlignHCenter
-                        fillMode: Image.Pad
+            Component {
+                id: shareDetailsPageComponent
+                ShareDetailsPage {
+                    id: shareDetailsPage
 
-                        source: "image://svgimage-custom-color/edit.svg/" + Style.menuBorder
-                        sourceSize.width: moreMenu.rowIconWidth
-                        sourceSize.height: moreMenu.rowIconWidth
-                    }
+                    width: parent.width
+                    height: parent.height
 
-                    NCInputTextField {
-                        id: linkShareLabelTextField
+                    fileDetails: root.fileDetails
+                    shareModelData: model
 
-                        Layout.fillWidth: true
-                        height: visible ? implicitHeight : 0
+                    canCreateLinkShares: root.canCreateLinkShares
 
-                        text: root.linkShareLabel
-                        placeholderText: qsTr("Share label")
+                    onCloseShareDetails: root.rootStackView.clear(StackView.PopTransition)
 
-                        enabled: root.isLinkShare &&
-                                 !root.waitingForLinkShareLabelChange
+                    onToggleAllowEditing: root.toggleAllowEditing(enable)
+                    onToggleAllowResharing: root.toggleAllowResharing(enable)
+                    onTogglePasswordProtect: root.togglePasswordProtect(enable)
+                    onToggleExpirationDate: root.toggleExpirationDate(enable)
+                    onToggleNoteToRecipient: root.toggleNoteToRecipient(enable)
 
-                        onAccepted: if(text !== root.linkShareLabel) {
-                            root.setLinkShareLabel(text);
-                            root.waitingForLinkShareLabelChange = true;
-                        }
+                    onSetLinkShareLabel: root.setLinkShareLabel(label)
+                    onSetExpireDate: root.setExpireDate(milliseconds) // Since QML ints are only 32 bits, use a variant
+                    onSetPassword: root.setPassword(password)
+                    onSetNote: root.setNote(note)
 
-                        NCBusyIndicator {
-                            anchors.fill: parent
-                            visible: root.waitingForLinkShareLabelChange
-                            running: visible
-                            z: 1
-                        }
+                    onDeleteShare: {
+                        root.deleteShare();
+                        closeShareDetails();
                     }
-                }
-
-                // 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;
+                    onCreateNewLinkShare: {
+                        root.createNewLinkShare();
+                        closeShareDetails();
                     }
 
-                    NCBusyIndicator {
-                        anchors.fill: parent
-                        visible: root.waitingForEditingAllowedChange
-                        running: visible
-                        z: 1
+                    Connections {
+                        target: root
+                        function onResetMenu() { shareDetailsPage.resetMenu() }
+                        function onResetPasswordField() { shareDetailsPage.resetPasswordField() }
+                        function onShowPasswordSetError(errorMessage) { shareDetailsPage.showPasswordSetError(errorMessage) }
                     }
                 }
-
-                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) {
-                            passwordErrorBoxLoader.message = "";
-                            root.setPassword(text);
-                            root.waitingForPasswordChange = true;
-                        }
-
-                        NCBusyIndicator {
-                            anchors.fill: parent
-                            visible: root.waitingForPasswordChange ||
-                                     root.waitingForPasswordProtectEnabledChange
-                            running: visible
-                            z: 1
-                        }
-                    }
-                }
-
-                Loader {
-                    id: passwordErrorBoxLoader
-
-                    property string message: ""
-
-                    anchors.left: parent.left
-                    anchors.right: parent.right
-                    height: message !== "" ? implicitHeight : 0
-
-                    active: message !== ""
-                    visible: active
-
-                    sourceComponent: Item {
-                        anchors.top: parent.top
-                        anchors.left: parent.left
-                        anchors.right: parent.right
-                        // Artificially add vertical padding
-                        implicitHeight: passwordErrorBox.implicitHeight + (Style.smallSpacing * 2)
-
-                        ErrorBox {
-                            id: passwordErrorBox
-                            anchors.left: parent.left
-                            anchors.right: parent.right
-                            anchors.verticalCenter: parent.verticalCenter
-
-                            text: passwordErrorBoxLoader.message
-                        }
-                    }
-                }
-
-                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() {
-                                const defaultParsedDate = Date.fromLocaleDateString(Qt.locale(), dateString, Locale.NarrowFormat);
-                                // JS always generates date in system locale, eliminate timezone difference to UTC
-                                const msecsSinceEpoch = defaultParsedDate.getTime() - (defaultParsedDate.getTimezoneOffset() * 60 * 1000);
-                                return new Date(msecsSinceEpoch);
-                            }
-
-                            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]);
-
-                            console.log(dayNum, monthIndexNum, fixedYearNum);
-
-                            // 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: {
-                            if (!enabled || !activeFocus) {
-                                return;
-                            }
-
-                            root.setExpireDate(value * dayInMSecs);
-                            root.waitingForExpireDateChange = true;
-                        }
-
-                        NCBusyIndicator {
-                            anchors.fill: parent
-                            visible: root.waitingForExpireDateEnabledChange ||
-                                     root.waitingForExpireDateChange
-                            running: visible
-                            z: 1
-                        }
-                    }
-                }
-
-                CheckBox {
-                    id: noteEnabledMenuItem
-
-                    spacing: moreMenu.indicatorSpacing
-                    padding: moreMenu.itemPadding
-                    indicator.width: moreMenu.indicatorItemWidth
-                    indicator.height: moreMenu.indicatorItemWidth
-
-                    checkable: true
-                    checked: root.noteEnabled
-                    text: qsTr("Note to recipient")
-                    enabled: !root.waitingForNoteEnabledChange
-
-                    onClicked: {
-                        root.toggleNoteToRecipient(checked);
-                        root.waitingForNoteEnabledChange = true;
-                    }
-
-                    NCBusyIndicator {
-                        anchors.fill: parent
-                        visible: root.waitingForNoteEnabledChange
-                        running: visible
-                        z: 1
-                    }
-                }
-
-                RowLayout {
-                    anchors.left: parent.left
-                    anchors.leftMargin: moreMenu.itemPadding
-                    anchors.right: parent.right
-                    anchors.rightMargin: moreMenu.itemPadding
-                    height: visible ? implicitHeight : 0
-                    spacing: moreMenu.indicatorSpacing
-
-                    visible: root.noteEnabled
-
-                    Image {
-                        Layout.preferredWidth: moreMenu.indicatorItemWidth
-                        Layout.fillHeight: true
-
-                        verticalAlignment: Image.AlignVCenter
-                        horizontalAlignment: Image.AlignHCenter
-                        fillMode: Image.Pad
-
-                        source: "image://svgimage-custom-color/edit.svg/" + Style.menuBorder
-                        sourceSize.width: moreMenu.rowIconWidth
-                        sourceSize.height: moreMenu.rowIconWidth
-                    }
-
-                    NCInputTextEdit {
-                        id: noteTextEdit
-
-                        Layout.fillWidth: true
-                        height: visible ? Math.max(Style.talkReplyTextFieldPreferredHeight, contentHeight) : 0
-                        submitButton.height: Math.min(Style.talkReplyTextFieldPreferredHeight, height - 2)
-
-                        text: root.note
-                        enabled: root.noteEnabled &&
-                                 !root.waitingForNoteChange &&
-                                 !root.waitingForNoteEnabledChange
-
-                        onEditingFinished: if(text !== root.note) {
-                            root.setNote(text);
-                            root.waitingForNoteChange = true;
-                        }
-
-                        NCBusyIndicator {
-                            anchors.fill: parent
-                            visible: root.waitingForNoteChange ||
-                                     root.waitingForNoteEnabledChange
-                            running: visible
-                            z: 1
-                        }
-                    }
-                }
-
-                MenuItem {
-                    spacing: moreMenu.indicatorSpacing
-                    padding: moreMenu.itemPadding
-
-                    icon.width: moreMenu.indicatorItemWidth
-                    icon.height: moreMenu.indicatorItemWidth
-                    icon.color: Style.ncTextColor
-                    icon.source: "qrc:///client/theme/close.svg"
-                    text: qsTr("Unshare")
-
-                    onTriggered: root.deleteShare()
-                }
-
-                MenuItem {
-                    height: visible ? implicitHeight : 0
-                    spacing: moreMenu.indicatorSpacing
-                    padding: moreMenu.itemPadding
-
-                    icon.width: moreMenu.indicatorItemWidth
-                    icon.height: moreMenu.indicatorItemWidth
-                    icon.color: Style.ncTextColor
-                    icon.source: "qrc:///client/theme/add.svg"
-                    text: qsTr("Add another link")
-
-                    visible: root.isLinkShare && root.canCreateLinkShares
-                    enabled: visible
-
-                    onTriggered: root.createNewLinkShare()
-                }
             }
         }
     }
diff --git a/src/gui/filedetails/ShareDetailsPage.qml b/src/gui/filedetails/ShareDetailsPage.qml
new file mode 100644 (file)
index 0000000..75ef88d
--- /dev/null
@@ -0,0 +1,737 @@
+/*
+ * 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"
+import "../"
+
+Page {
+    id: root
+
+    signal closeShareDetails
+    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)
+
+    property FileDetails fileDetails: FileDetails {}
+    property var shareModelData: ({})
+
+    property bool canCreateLinkShares: true
+
+    readonly property var share: shareModelData.share ?? ({})
+
+    readonly property string iconUrl: shareModelData.iconUrl ?? ""
+    readonly property string avatarUrl: shareModelData.avatarUrl ?? ""
+    readonly property string text: shareModelData.display ?? ""
+    readonly property string detailText: shareModelData.detailText ?? ""
+    readonly property string link: shareModelData.link ?? ""
+    readonly property string note: shareModelData.note ?? ""
+    readonly property string password: shareModelData.password ?? ""
+    readonly property string passwordPlaceholder: "●●●●●●●●●●"
+
+    readonly property var expireDate: shareModelData.expireDate // Don't use int as we are limited
+    readonly property var maximumExpireDate: shareModelData.enforcedMaximumExpireDate
+
+    readonly property string linkShareLabel: shareModelData.linkShareLabel ?? ""
+
+    readonly property bool editingAllowed: shareModelData.editingAllowed
+    readonly property bool noteEnabled: shareModelData.noteEnabled
+    readonly property bool expireDateEnabled: shareModelData.expireDateEnabled
+    readonly property bool expireDateEnforced: shareModelData.expireDateEnforced
+    readonly property bool passwordProtectEnabled: shareModelData.passwordProtectEnabled
+    readonly property bool passwordEnforced: shareModelData.passwordEnforced
+
+    readonly property bool isLinkShare: shareModelData.shareType === ShareModel.ShareTypeLink
+    readonly property bool isPlaceholderLinkShare: shareModelData.shareType === ShareModel.ShareTypePlaceholderLink
+
+    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 showPasswordSetError(message) {
+        passwordErrorBoxLoader.message = message !== "" ?
+                                         message : qsTr("An error occurred setting the share password.");
+    }
+
+    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()
+
+    padding: Style.standardSpacing * 2
+
+    // 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
+    }
+
+    background: Rectangle {
+        color: Style.backgroundColor
+    }
+
+    header: ColumnLayout {
+        spacing: root.intendedPadding
+
+        GridLayout {
+            id: headerGridLayout
+
+            Layout.fillWidth: parent
+            Layout.topMargin: root.topPadding
+
+            columns: 3
+            rows: 2
+
+            rowSpacing: Style.standardSpacing / 2
+            columnSpacing: Style.standardSpacing
+
+            Image {
+                id: fileIcon
+
+                Layout.rowSpan: headerGridLayout.rows
+                Layout.preferredWidth: Style.trayListItemIconSize
+                Layout.leftMargin: root.padding
+                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: headLabel
+
+                Layout.fillWidth: true
+
+                text: qsTr("Edit share")
+                color: Style.ncTextColor
+                font.bold: true
+                elide: Text.ElideRight
+            }
+
+            CustomButton {
+                id: closeButton
+
+                Layout.rowSpan: headerGridLayout.rows
+                Layout.preferredWidth: Style.iconButtonWidth
+                Layout.preferredHeight: width
+                Layout.rightMargin: root.padding
+
+                imageSource: "image://svgimage-custom-color/clear.svg" + "/" + Style.ncTextColor
+                bgColor: Style.lightHover
+                bgNormalOpacity: 0
+                toolTipText: qsTr("Dismiss")
+
+                onClicked: root.closeShareDetails()
+            }
+
+            Label {
+                id: secondaryLabel
+
+                Layout.fillWidth: true
+                Layout.rightMargin: root.padding
+
+                text: root.fileDetails.name
+                color: Style.ncSecondaryTextColor
+                wrapMode: Text.Wrap
+            }
+        }
+    }
+
+    ColumnLayout {
+        id: moreMenu
+
+        property int rowIconWidth: 16
+        property int indicatorItemWidth: 20
+        property int indicatorSpacing: Style.standardSpacing
+        property int itemPadding: Style.smallSpacing
+
+        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) {
+                    passwordErrorBoxLoader.message = "";
+                    root.setPassword(text);
+                    root.waitingForPasswordChange = true;
+                }
+
+                NCBusyIndicator {
+                    anchors.fill: parent
+                    visible: root.waitingForPasswordChange ||
+                             root.waitingForPasswordProtectEnabledChange
+                    running: visible
+                    z: 1
+                }
+            }
+        }
+
+        Loader {
+            id: passwordErrorBoxLoader
+
+            property string message: ""
+
+            anchors.left: parent.left
+            anchors.right: parent.right
+            height: message !== "" ? implicitHeight : 0
+
+            active: message !== ""
+            visible: active
+
+            sourceComponent: Item {
+                anchors.top: parent.top
+                anchors.left: parent.left
+                anchors.right: parent.right
+                // Artificially add vertical padding
+                implicitHeight: passwordErrorBox.implicitHeight + (Style.smallSpacing * 2)
+
+                ErrorBox {
+                    id: passwordErrorBox
+                    anchors.left: parent.left
+                    anchors.right: parent.right
+                    anchors.verticalCenter: parent.verticalCenter
+
+                    text: passwordErrorBoxLoader.message
+                }
+            }
+        }
+
+        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() {
+                        const defaultParsedDate = Date.fromLocaleDateString(Qt.locale(), dateString, Locale.NarrowFormat);
+                        // JS always generates date in system locale, eliminate timezone difference to UTC
+                        const msecsSinceEpoch = defaultParsedDate.getTime() - (defaultParsedDate.getTimezoneOffset() * 60 * 1000);
+                        return new Date(msecsSinceEpoch);
+                    }
+
+                    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]);
+
+                    console.log(dayNum, monthIndexNum, fixedYearNum);
+
+                    // 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: {
+                    if (!enabled || !activeFocus) {
+                        return;
+                    }
+
+                    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()
+        }
+    }
+
+}
index ebb04d21cce4a36572981247b8b2324d66c6c3d4..8bc90ba66ad831a037168e2643ef3c6068a603a6 100644 (file)
@@ -71,6 +71,8 @@ ColumnLayout {
         }
     }
 
+    property StackView rootStackView: StackView {}
+
     Dialog {
         id: shareRequiresPasswordDialog
 
@@ -216,12 +218,13 @@ ColumnLayout {
                             if(shareId !== model.shareId) {
                                 return;
                             }
-
                             shareDelegate.resetMenu();
                         }
                     }
 
                     iconSize: root.iconSize
+                    fileDetails: root.fileDetails
+                    rootStackView: root.rootStackView
                     canCreateLinkShares: root.publicLinkSharingPossible
 
                     onCreateNewLinkShare: {