Add copy share link button to share details page footer, remove unneeded columnlayout...
authorClaudio Cambra <claudio.cambra@nextcloud.com>
Wed, 23 Nov 2022 14:25:09 +0000 (15:25 +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/ShareDetailsPage.qml

index 8f66615fa990bbd55b298ba54cf0d8718bb3fbed..e0149da3be555a246b1bc7ac10aced2b1e9cd606 100644 (file)
@@ -250,517 +250,538 @@ Page {
         }
     }
 
-    contentItem: ColumnLayout {
-        ScrollView {
-            Layout.fillWidth: true
-            Layout.fillHeight: true
+    contentItem: ScrollView {
+        contentWidth: availableWidth
+        clip: true
 
-            contentWidth: availableWidth
+        ColumnLayout {
+            id: moreMenu
 
-            clip: true
+            property int rowIconWidth: 16
+            property int indicatorItemWidth: 20
+            property int indicatorSpacing: Style.standardSpacing
+            property int itemPadding: Style.smallSpacing
 
-            ColumnLayout {
-                id: moreMenu
+            width: parent.width
 
-                property int rowIconWidth: 16
-                property int indicatorItemWidth: 20
-                property int indicatorSpacing: Style.standardSpacing
-                property int itemPadding: Style.smallSpacing
-
-                width: parent.width
-
-                RowLayout {
-                    Layout.fillWidth: true
-                    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
+            RowLayout {
+                Layout.fillWidth: true
+                height: visible ? implicitHeight : 0
+                spacing: moreMenu.indicatorSpacing
 
-                        Layout.fillWidth: true
-                        height: visible ? implicitHeight : 0
+                visible: root.isLinkShare
 
-                        text: root.linkShareLabel
-                        placeholderText: qsTr("Share label")
+                Image {
+                    Layout.preferredWidth: moreMenu.indicatorItemWidth
+                    Layout.fillHeight: true
 
-                        enabled: root.isLinkShare &&
-                                 !root.waitingForLinkShareLabelChange
+                    verticalAlignment: Image.AlignVCenter
+                    horizontalAlignment: Image.AlignHCenter
+                    fillMode: Image.Pad
 
-                        onAccepted: if(text !== root.linkShareLabel) {
-                            root.setLinkShareLabel(text);
-                            root.waitingForLinkShareLabelChange = true;
-                        }
-
-                        NCBusyIndicator {
-                            anchors.fill: parent
-                            visible: root.waitingForLinkShareLabelChange
-                            running: visible
-                            z: 1
-                        }
-                    }
+                    source: "image://svgimage-custom-color/edit.svg/" + Style.menuBorder
+                    sourceSize.width: moreMenu.rowIconWidth
+                    sourceSize.height: moreMenu.rowIconWidth
                 }
 
-                // On these checkables, the clicked() signal is called after
-                // the check state changes.
-                CheckBox {
-                    id: editingAllowedMenuItem
+                NCInputTextField {
+                    id: linkShareLabelTextField
 
                     Layout.fillWidth: true
+                    height: visible ? implicitHeight : 0
 
-                    spacing: moreMenu.indicatorSpacing
-                    padding: moreMenu.itemPadding
-                    indicator.width: moreMenu.indicatorItemWidth
-                    indicator.height: moreMenu.indicatorItemWidth
+                    text: root.linkShareLabel
+                    placeholderText: qsTr("Share label")
 
-                    checkable: true
-                    checked: root.editingAllowed
-                    text: qsTr("Allow editing")
-                    enabled: !root.waitingForEditingAllowedChange
+                    enabled: root.isLinkShare &&
+                             !root.waitingForLinkShareLabelChange
 
-                    onClicked: {
-                        root.toggleAllowEditing(checked);
-                        root.waitingForEditingAllowedChange = true;
+                    onAccepted: if(text !== root.linkShareLabel) {
+                        root.setLinkShareLabel(text);
+                        root.waitingForLinkShareLabelChange = true;
                     }
 
                     NCBusyIndicator {
                         anchors.fill: parent
-                        visible: root.waitingForEditingAllowedChange
+                        visible: root.waitingForLinkShareLabelChange
                         running: visible
                         z: 1
                     }
                 }
+            }
 
-                CheckBox {
-                    id: passwordProtectEnabledMenuItem
+            // On these checkables, the clicked() signal is called after
+            // the check state changes.
+            CheckBox {
+                id: editingAllowedMenuItem
 
-                    Layout.fillWidth: true
+                Layout.fillWidth: true
 
-                    spacing: moreMenu.indicatorSpacing
-                    padding: moreMenu.itemPadding
-                    indicator.width: moreMenu.indicatorItemWidth
-                    indicator.height: moreMenu.indicatorItemWidth
+                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
+                checkable: true
+                checked: root.editingAllowed
+                text: qsTr("Allow editing")
+                enabled: !root.waitingForEditingAllowedChange
 
-                    onClicked: {
-                        root.togglePasswordProtect(checked);
-                        root.waitingForPasswordProtectEnabledChange = true;
-                    }
+                onClicked: {
+                    root.toggleAllowEditing(checked);
+                    root.waitingForEditingAllowedChange = true;
+                }
 
-                    NCBusyIndicator {
-                        anchors.fill: parent
-                        visible: root.waitingForPasswordProtectEnabledChange
-                        running: visible
-                        z: 1
-                    }
+                NCBusyIndicator {
+                    anchors.fill: parent
+                    visible: root.waitingForEditingAllowedChange
+                    running: visible
+                    z: 1
                 }
+            }
 
-                RowLayout {
-                    Layout.fillWidth: true
+            CheckBox {
+                id: passwordProtectEnabledMenuItem
 
-                    height: visible ? implicitHeight : 0
-                    spacing: moreMenu.indicatorSpacing
+                Layout.fillWidth: true
 
-                    visible: root.passwordProtectEnabled
+                spacing: moreMenu.indicatorSpacing
+                padding: moreMenu.itemPadding
+                indicator.width: moreMenu.indicatorItemWidth
+                indicator.height: moreMenu.indicatorItemWidth
 
-                    Image {
-                        Layout.preferredWidth: moreMenu.indicatorItemWidth
-                        Layout.fillHeight: true
+                checkable: true
+                checked: root.passwordProtectEnabled
+                text: qsTr("Password protect")
+                enabled: !root.waitingForPasswordProtectEnabledChange && !root.passwordEnforced
 
-                        verticalAlignment: Image.AlignVCenter
-                        horizontalAlignment: Image.AlignHCenter
-                        fillMode: Image.Pad
+                onClicked: {
+                    root.togglePasswordProtect(checked);
+                    root.waitingForPasswordProtectEnabledChange = true;
+                }
 
-                        source: "image://svgimage-custom-color/lock-https.svg/" + Style.menuBorder
-                        sourceSize.width: moreMenu.rowIconWidth
-                        sourceSize.height: moreMenu.rowIconWidth
-                    }
+                NCBusyIndicator {
+                    anchors.fill: parent
+                    visible: root.waitingForPasswordProtectEnabledChange
+                    running: visible
+                    z: 1
+                }
+            }
 
-                    NCInputTextField {
-                        id: passwordTextField
+            RowLayout {
+                Layout.fillWidth: true
 
-                        Layout.fillWidth: true
-                        height: visible ? implicitHeight : 0
+                height: visible ? implicitHeight : 0
+                spacing: moreMenu.indicatorSpacing
 
-                        text: root.password !== "" ? root.password : root.passwordPlaceholder
-                        enabled: root.passwordProtectEnabled &&
-                                 !root.waitingForPasswordChange &&
-                                 !root.waitingForPasswordProtectEnabledChange
+                visible: root.passwordProtectEnabled
 
-                        onAccepted: if(text !== root.password && text !== root.passwordPlaceholder) {
-                            passwordErrorBoxLoader.message = "";
-                            root.setPassword(text);
-                            root.waitingForPasswordChange = true;
-                        }
+                Image {
+                    Layout.preferredWidth: moreMenu.indicatorItemWidth
+                    Layout.fillHeight: true
 
-                        NCBusyIndicator {
-                            anchors.fill: parent
-                            visible: root.waitingForPasswordChange ||
-                                     root.waitingForPasswordProtectEnabledChange
-                            running: visible
-                            z: 1
-                        }
+                    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
+            Loader {
+                id: passwordErrorBoxLoader
 
-                    property string message: ""
+                property string message: ""
 
-                    Layout.fillWidth: true
-                    height: message !== "" ? implicitHeight : 0
+                Layout.fillWidth: true
+                height: message !== "" ? implicitHeight : 0
+
+                active: message !== ""
+                visible: active
 
-                    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)
 
-                    sourceComponent: Item {
-                        anchors.top: parent.top
+                    ErrorBox {
+                        id: passwordErrorBox
                         anchors.left: parent.left
                         anchors.right: parent.right
-                        // Artificially add vertical padding
-                        implicitHeight: passwordErrorBox.implicitHeight + (Style.smallSpacing * 2)
+                        anchors.verticalCenter: parent.verticalCenter
 
-                        ErrorBox {
-                            id: passwordErrorBox
-                            anchors.left: parent.left
-                            anchors.right: parent.right
-                            anchors.verticalCenter: parent.verticalCenter
-
-                            text: passwordErrorBoxLoader.message
-                        }
+                        text: passwordErrorBoxLoader.message
                     }
                 }
+            }
 
-                CheckBox {
-                    id: expireDateEnabledMenuItem
+            CheckBox {
+                id: expireDateEnabledMenuItem
 
-                    Layout.fillWidth: true
+                Layout.fillWidth: true
 
-                    spacing: moreMenu.indicatorSpacing
-                    padding: moreMenu.itemPadding
-                    indicator.width: moreMenu.indicatorItemWidth
-                    indicator.height: moreMenu.indicatorItemWidth
+                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
+                checkable: true
+                checked: root.expireDateEnabled
+                text: qsTr("Set expiration date")
+                enabled: !root.waitingForExpireDateEnabledChange && !root.expireDateEnforced
 
-                    onClicked: {
-                        root.toggleExpirationDate(checked);
-                        root.waitingForExpireDateEnabledChange = true;
-                    }
+                onClicked: {
+                    root.toggleExpirationDate(checked);
+                    root.waitingForExpireDateEnabledChange = true;
+                }
 
-                    NCBusyIndicator {
-                        anchors.fill: parent
-                        visible: root.waitingForExpireDateEnabledChange
-                        running: visible
-                        z: 1
-                    }
+                NCBusyIndicator {
+                    anchors.fill: parent
+                    visible: root.waitingForExpireDateEnabledChange
+                    running: visible
+                    z: 1
                 }
+            }
 
-                RowLayout {
-                    Layout.fillWidth: true
-                    height: visible ? implicitHeight : 0
-                    spacing: moreMenu.indicatorSpacing
+            RowLayout {
+                Layout.fillWidth: true
+                height: visible ? implicitHeight : 0
+                spacing: moreMenu.indicatorSpacing
 
-                    visible: root.expireDateEnabled
+                visible: root.expireDateEnabled
 
-                    Image {
-                        Layout.preferredWidth: moreMenu.indicatorItemWidth
-                        Layout.fillHeight: true
+                Image {
+                    Layout.preferredWidth: moreMenu.indicatorItemWidth
+                    Layout.fillHeight: true
 
-                        verticalAlignment: Image.AlignVCenter
-                        horizontalAlignment: Image.AlignHCenter
-                        fillMode: Image.Pad
+                    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
+                    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
                     }
 
-                    // 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);
                         }
 
-                        // 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));
+                        const dateStringDelimiterMatches = dateString.match(/\D/);
+                        if(dateStringDelimiterMatches.length === 0) {
+                            // Let the date method figure out this weirdness
+                            return defaultParse();
                         }
 
-                        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 {}
-
-                        padding: 0
-                        background: null
-                        contentItem: NCInputTextField {
-                            text: expireDateSpinBox.textFromValue(expireDateSpinBox.value, expireDateSpinBox.locale)
-                            readOnly: !expireDateSpinBox.editable
-                            validator: expireDateSpinBox.validator
-                            inputMethodHints: Qt.ImhFormattedNumbersOnly
-                            onAccepted: {
-                                expireDateSpinBox.value = expireDateSpinBox.valueFromText(text, expireDateSpinBox.locale);
-                                expireDateSpinBox.valueModified();
-                            }
-                        }
+                        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));
 
-                        value: expireDateReduced
-                        from: minimumExpireDateReduced
-                        to: maximumExpireDateReduced
+                        let splitDateString = dateString.split(dateStringDelimiter);
+                        let userProvidedYear = splitDateString[localisedDateYearPosition]
 
-                        textFromValue: (value, locale) => {
-                            const dateFromValue = new Date(value * dayInMSecs);
-                            return dateFromValue.toLocaleDateString(Qt.locale(), Locale.NarrowFormat);
+                        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();
                         }
-                        valueFromText: (text, locale) => {
-                            const dateFromText = parseDateString(text);
-                            return Math.floor(dateFromText.getTime() / dayInMSecs);
+
+                        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]);
 
-                        editable: true
-                        inputMethodHints: Qt.ImhDate | Qt.ImhFormattedNumbersOnly
+                        console.log(dayNum, monthIndexNum, fixedYearNum);
 
-                        enabled: root.expireDateEnabled &&
-                                 !root.waitingForExpireDateChange &&
-                                 !root.waitingForExpireDateEnabledChange
+                        // Modification: return date in UTC
+                        return new Date(Date.UTC(fixedYearNum, monthIndexNum, dayNum));
+                    }
 
-                        onValueModified: {
-                            if (!enabled || !activeFocus) {
-                                return;
-                            }
+                    Layout.fillWidth: true
+                    height: visible ? implicitHeight : 0
 
-                            root.setExpireDate(value * dayInMSecs);
-                            root.waitingForExpireDateChange = true;
-                        }
 
-                        NCBusyIndicator {
-                            anchors.fill: parent
-                            visible: root.waitingForExpireDateEnabledChange ||
-                                     root.waitingForExpireDateChange
-                            running: visible
-                            z: 1
+                    // 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 {}
+
+                    padding: 0
+                    background: null
+                    contentItem: NCInputTextField {
+                        text: expireDateSpinBox.textFromValue(expireDateSpinBox.value, expireDateSpinBox.locale)
+                        readOnly: !expireDateSpinBox.editable
+                        validator: expireDateSpinBox.validator
+                        inputMethodHints: Qt.ImhFormattedNumbersOnly
+                        onAccepted: {
+                            expireDateSpinBox.value = expireDateSpinBox.valueFromText(text, expireDateSpinBox.locale);
+                            expireDateSpinBox.valueModified();
                         }
                     }
-                }
 
-                CheckBox {
-                    id: noteEnabledMenuItem
+                    value: expireDateReduced
+                    from: minimumExpireDateReduced
+                    to: maximumExpireDateReduced
 
-                    Layout.fillWidth: true
+                    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);
+                    }
 
-                    spacing: moreMenu.indicatorSpacing
-                    padding: moreMenu.itemPadding
-                    indicator.width: moreMenu.indicatorItemWidth
-                    indicator.height: moreMenu.indicatorItemWidth
+                    editable: true
+                    inputMethodHints: Qt.ImhDate | Qt.ImhFormattedNumbersOnly
 
-                    checkable: true
-                    checked: root.noteEnabled
-                    text: qsTr("Note to recipient")
-                    enabled: !root.waitingForNoteEnabledChange
+                    enabled: root.expireDateEnabled &&
+                             !root.waitingForExpireDateChange &&
+                             !root.waitingForExpireDateEnabledChange
 
-                    onClicked: {
-                        root.toggleNoteToRecipient(checked);
-                        root.waitingForNoteEnabledChange = true;
+                    onValueModified: {
+                        if (!enabled || !activeFocus) {
+                            return;
+                        }
+
+                        root.setExpireDate(value * dayInMSecs);
+                        root.waitingForExpireDateChange = true;
                     }
 
                     NCBusyIndicator {
                         anchors.fill: parent
-                        visible: root.waitingForNoteEnabledChange
+                        visible: root.waitingForExpireDateEnabledChange ||
+                                 root.waitingForExpireDateChange
                         running: visible
                         z: 1
                     }
                 }
+            }
 
-                RowLayout {
-                    Layout.fillWidth: true
-                    height: visible ? implicitHeight : 0
-                    spacing: moreMenu.indicatorSpacing
+            CheckBox {
+                id: noteEnabledMenuItem
 
-                    visible: root.noteEnabled
+                Layout.fillWidth: true
 
-                    Image {
-                        Layout.preferredWidth: moreMenu.indicatorItemWidth
-                        Layout.fillHeight: true
+                spacing: moreMenu.indicatorSpacing
+                padding: moreMenu.itemPadding
+                indicator.width: moreMenu.indicatorItemWidth
+                indicator.height: moreMenu.indicatorItemWidth
 
-                        verticalAlignment: Image.AlignVCenter
-                        horizontalAlignment: Image.AlignHCenter
-                        fillMode: Image.Pad
+                checkable: true
+                checked: root.noteEnabled
+                text: qsTr("Note to recipient")
+                enabled: !root.waitingForNoteEnabledChange
 
-                        source: "image://svgimage-custom-color/edit.svg/" + Style.menuBorder
-                        sourceSize.width: moreMenu.rowIconWidth
-                        sourceSize.height: moreMenu.rowIconWidth
-                    }
+                onClicked: {
+                    root.toggleNoteToRecipient(checked);
+                    root.waitingForNoteEnabledChange = true;
+                }
 
-                    NCInputTextEdit {
-                        id: noteTextEdit
+                NCBusyIndicator {
+                    anchors.fill: parent
+                    visible: root.waitingForNoteEnabledChange
+                    running: visible
+                    z: 1
+                }
+            }
 
-                        Layout.fillWidth: true
-                        height: visible ? Math.max(Style.talkReplyTextFieldPreferredHeight, contentHeight) : 0
-                        submitButton.height: Math.min(Style.talkReplyTextFieldPreferredHeight, height - 2)
+            RowLayout {
+                Layout.fillWidth: true
+                height: visible ? implicitHeight : 0
+                spacing: moreMenu.indicatorSpacing
 
-                        text: root.note
-                        enabled: root.noteEnabled &&
-                                 !root.waitingForNoteChange &&
-                                 !root.waitingForNoteEnabledChange
+                visible: root.noteEnabled
 
-                        onEditingFinished: if(text !== root.note) {
-                            root.setNote(text);
-                            root.waitingForNoteChange = true;
-                        }
+                Image {
+                    Layout.preferredWidth: moreMenu.indicatorItemWidth
+                    Layout.fillHeight: true
 
-                        NCBusyIndicator {
-                            anchors.fill: parent
-                            visible: root.waitingForNoteChange ||
-                                     root.waitingForNoteEnabledChange
-                            running: visible
-                            z: 1
-                        }
-                    }
-                }
+                    verticalAlignment: Image.AlignVCenter
+                    horizontalAlignment: Image.AlignHCenter
+                    fillMode: Image.Pad
 
-                CustomButton {
-                    height: Style.standardPrimaryButtonHeight
-
-                    imageSource: "image://svgimage-custom-color/close.svg/" + Style.errorBoxBackgroundColor
-                    imageSourceHover: "image://svgimage-custom-color/close.svg/" + Style.ncHeaderTextColor
-                    text: qsTr("Unshare")
-                    textColor: Style.errorBoxBackgroundColor
-                    textColorHovered: "white"
-                    contentsFont.bold: true
-                    bgNormalColor: Style.buttonBackgroundColor
-                    bgHoverColor: Style.errorBoxBackgroundColor
-                    bgNormalOpacity: 1.0
-                    bgHoverOpacity: 1.0
-
-                    onClicked: root.deleteShare()
+                    source: "image://svgimage-custom-color/edit.svg/" + Style.menuBorder
+                    sourceSize.width: moreMenu.rowIconWidth
+                    sourceSize.height: moreMenu.rowIconWidth
                 }
 
-                CustomButton {
-                    height: Style.standardPrimaryButtonHeight
+                NCInputTextEdit {
+                    id: noteTextEdit
 
-                    imageSource: "image://svgimage-custom-color/add.svg/" + Style.ncBlue
-                    imageSourceHover: "image://svgimage-custom-color/add.svg/" + Style.ncHeaderTextColor
-                    text: qsTr("Add another link")
-                    textColor: Style.ncBlue
-                    textColorHovered: Style.ncHeaderTextColor
-                    contentsFont.bold: true
-                    bgNormalColor: Style.buttonBackgroundColor
-                    bgHoverColor: Style.ncBlue
-                    bgNormalOpacity: 1.0
-                    bgHoverOpacity: 1.0
+                    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
 
-                    visible: root.isLinkShare && root.canCreateLinkShares
-                    enabled: visible
+                    onEditingFinished: if(text !== root.note) {
+                        root.setNote(text);
+                        root.waitingForNoteChange = true;
+                    }
 
-                    onClicked: root.createNewLinkShare()
+                    NCBusyIndicator {
+                        anchors.fill: parent
+                        visible: root.waitingForNoteChange ||
+                                 root.waitingForNoteEnabledChange
+                        running: visible
+                        z: 1
+                    }
                 }
             }
-        }
-
-        RowLayout {
-            Layout.fillWidth: true
 
+            CustomButton {
+                height: Style.standardPrimaryButtonHeight
+
+                imageSource: "image://svgimage-custom-color/close.svg/" + Style.errorBoxBackgroundColor
+                imageSourceHover: "image://svgimage-custom-color/close.svg/" + Style.ncHeaderTextColor
+                text: qsTr("Unshare")
+                textColor: Style.errorBoxBackgroundColor
+                textColorHovered: "white"
+                contentsFont.bold: true
+                bgNormalColor: Style.buttonBackgroundColor
+                bgHoverColor: Style.errorBoxBackgroundColor
+                bgNormalOpacity: 1.0
+                bgHoverOpacity: 1.0
+
+                onClicked: root.deleteShare()
+            }
 
+            CustomButton {
+                height: Style.standardPrimaryButtonHeight
+
+                imageSource: "image://svgimage-custom-color/add.svg/" + Style.ncBlue
+                imageSourceHover: "image://svgimage-custom-color/add.svg/" + Style.ncHeaderTextColor
+                text: qsTr("Add another link")
+                textColor: Style.ncBlue
+                textColorHovered: Style.ncHeaderTextColor
+                contentsFont.bold: true
+                bgNormalColor: Style.buttonBackgroundColor
+                bgHoverColor: Style.ncBlue
+                bgNormalOpacity: 1.0
+                bgHoverOpacity: 1.0
+
+                visible: root.isLinkShare && root.canCreateLinkShares
+                enabled: visible
+
+                onClicked: root.createNewLinkShare()
+            }
+        }
+    }
 
+    footer: DialogButtonBox {
+        topPadding: 0
+        bottomPadding: root.padding
+        rightPadding: root.padding
+        leftPadding: root.padding
+        alignment: Qt.AlignRight | Qt.AlignVCenter
+        visible: copyShareLinkButton.visible
+
+        CustomButton {
+            id: copyShareLinkButton
+
+            height: Style.standardPrimaryButtonHeight
+
+            imageSource: "image://svgimage-custom-color/copy.svg/" + Style.ncHeaderTextColor
+            text: qsTr("Copy share link")
+            textColor: Style.ncHeaderTextColor
+            contentsFont.bold: true
+            bgColor: Style.ncBlue
+            bgNormalOpacity: 1.0
+            bgHoverOpacity: Style.hoverOpacity
+
+            visible: root.isLinkShare
+            enabled: visible
+
+            onClicked: {
+                clipboardHelper.text = root.link;
+                clipboardHelper.selectAll();
+                clipboardHelper.copy();
+                clipboardHelper.clear();
+            }
 
+            TextEdit { id: clipboardHelper; visible: false }
         }
     }
 }