mirror of
https://github.com/nextcloud/desktop.git
synced 2024-11-25 14:36:01 +03:00
Improve layout of share details page
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
This commit is contained in:
parent
046f8ea910
commit
cfccd02a26
1 changed files with 461 additions and 444 deletions
|
@ -180,7 +180,7 @@ Page {
|
|||
}
|
||||
|
||||
header: ColumnLayout {
|
||||
spacing: root.intendedPadding
|
||||
spacing: root.padding
|
||||
|
||||
GridLayout {
|
||||
id: headerGridLayout
|
||||
|
@ -250,488 +250,505 @@ Page {
|
|||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: moreMenu
|
||||
contentItem: ColumnLayout {
|
||||
ScrollView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
property int rowIconWidth: 16
|
||||
property int indicatorItemWidth: 20
|
||||
property int indicatorSpacing: Style.standardSpacing
|
||||
property int itemPadding: Style.smallSpacing
|
||||
contentWidth: availableWidth
|
||||
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: moreMenu.itemPadding
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: moreMenu.itemPadding
|
||||
height: visible ? implicitHeight : 0
|
||||
spacing: moreMenu.indicatorSpacing
|
||||
clip: true
|
||||
|
||||
visible: root.isLinkShare
|
||||
ColumnLayout {
|
||||
id: moreMenu
|
||||
|
||||
Image {
|
||||
Layout.preferredWidth: moreMenu.indicatorItemWidth
|
||||
Layout.fillHeight: true
|
||||
property int rowIconWidth: 16
|
||||
property int indicatorItemWidth: 20
|
||||
property int indicatorSpacing: Style.standardSpacing
|
||||
property int itemPadding: Style.smallSpacing
|
||||
|
||||
verticalAlignment: Image.AlignVCenter
|
||||
horizontalAlignment: Image.AlignHCenter
|
||||
fillMode: Image.Pad
|
||||
width: parent.width
|
||||
|
||||
source: "image://svgimage-custom-color/edit.svg/" + Style.menuBorder
|
||||
sourceSize.width: moreMenu.rowIconWidth
|
||||
sourceSize.height: moreMenu.rowIconWidth
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
height: visible ? implicitHeight : 0
|
||||
spacing: moreMenu.indicatorSpacing
|
||||
|
||||
NCInputTextField {
|
||||
id: linkShareLabelTextField
|
||||
visible: root.isLinkShare
|
||||
|
||||
Layout.fillWidth: true
|
||||
height: visible ? implicitHeight : 0
|
||||
Image {
|
||||
Layout.preferredWidth: moreMenu.indicatorItemWidth
|
||||
Layout.fillHeight: true
|
||||
|
||||
text: root.linkShareLabel
|
||||
placeholderText: qsTr("Share label")
|
||||
verticalAlignment: Image.AlignVCenter
|
||||
horizontalAlignment: Image.AlignHCenter
|
||||
fillMode: Image.Pad
|
||||
|
||||
enabled: root.isLinkShare &&
|
||||
!root.waitingForLinkShareLabelChange
|
||||
source: "image://svgimage-custom-color/edit.svg/" + Style.menuBorder
|
||||
sourceSize.width: moreMenu.rowIconWidth
|
||||
sourceSize.height: moreMenu.rowIconWidth
|
||||
}
|
||||
|
||||
onAccepted: if(text !== root.linkShareLabel) {
|
||||
root.setLinkShareLabel(text);
|
||||
root.waitingForLinkShareLabelChange = true;
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// On these checkables, the clicked() signal is called after
|
||||
// the check state changes.
|
||||
CheckBox {
|
||||
id: editingAllowedMenuItem
|
||||
CheckBox {
|
||||
id: passwordProtectEnabledMenuItem
|
||||
|
||||
spacing: moreMenu.indicatorSpacing
|
||||
padding: moreMenu.itemPadding
|
||||
indicator.width: moreMenu.indicatorItemWidth
|
||||
indicator.height: moreMenu.indicatorItemWidth
|
||||
Layout.fillWidth: true
|
||||
|
||||
checkable: true
|
||||
checked: root.editingAllowed
|
||||
text: qsTr("Allow editing")
|
||||
enabled: !root.waitingForEditingAllowedChange
|
||||
spacing: moreMenu.indicatorSpacing
|
||||
padding: moreMenu.itemPadding
|
||||
indicator.width: moreMenu.indicatorItemWidth
|
||||
indicator.height: moreMenu.indicatorItemWidth
|
||||
|
||||
onClicked: {
|
||||
root.toggleAllowEditing(checked);
|
||||
root.waitingForEditingAllowedChange = true;
|
||||
}
|
||||
checkable: true
|
||||
checked: root.passwordProtectEnabled
|
||||
text: qsTr("Password protect")
|
||||
enabled: !root.waitingForPasswordProtectEnabledChange && !root.passwordEnforced
|
||||
|
||||
NCBusyIndicator {
|
||||
anchors.fill: parent
|
||||
visible: root.waitingForEditingAllowedChange
|
||||
running: visible
|
||||
z: 1
|
||||
}
|
||||
}
|
||||
onClicked: {
|
||||
root.togglePasswordProtect(checked);
|
||||
root.waitingForPasswordProtectEnabledChange = true;
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: passwordProtectEnabledMenuItem
|
||||
NCBusyIndicator {
|
||||
anchors.fill: parent
|
||||
visible: root.waitingForPasswordProtectEnabledChange
|
||||
running: visible
|
||||
z: 1
|
||||
}
|
||||
}
|
||||
|
||||
spacing: moreMenu.indicatorSpacing
|
||||
padding: moreMenu.itemPadding
|
||||
indicator.width: moreMenu.indicatorItemWidth
|
||||
indicator.height: moreMenu.indicatorItemWidth
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
checkable: true
|
||||
checked: root.passwordProtectEnabled
|
||||
text: qsTr("Password protect")
|
||||
enabled: !root.waitingForPasswordProtectEnabledChange && !root.passwordEnforced
|
||||
height: visible ? implicitHeight : 0
|
||||
spacing: moreMenu.indicatorSpacing
|
||||
|
||||
onClicked: {
|
||||
root.togglePasswordProtect(checked);
|
||||
root.waitingForPasswordProtectEnabledChange = true;
|
||||
}
|
||||
visible: root.passwordProtectEnabled
|
||||
|
||||
NCBusyIndicator {
|
||||
anchors.fill: parent
|
||||
visible: root.waitingForPasswordProtectEnabledChange
|
||||
running: visible
|
||||
z: 1
|
||||
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: ""
|
||||
|
||||
Layout.fillWidth: true
|
||||
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
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
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 {
|
||||
Layout.fillWidth: true
|
||||
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
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
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 {
|
||||
Layout.fillWidth: true
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
CustomButton {
|
||||
Layout.fillWidth: true
|
||||
height: visible ? implicitHeight : 0
|
||||
implicitWidth: parent.width / 2
|
||||
height: Style.standardPrimaryButtonHeight
|
||||
|
||||
text: root.password !== "" ? root.password : root.passwordPlaceholder
|
||||
enabled: root.passwordProtectEnabled &&
|
||||
!root.waitingForPasswordChange &&
|
||||
!root.waitingForPasswordProtectEnabledChange
|
||||
imageSource: "image://svgimage-custom-color/close.svg/" + Style.ncHeaderTextColor
|
||||
text: qsTr("Unshare")
|
||||
textColor: Style.ncHeaderTextColor
|
||||
contentsFont.bold: true
|
||||
bgColor: Style.errorBoxBackgroundColor
|
||||
bgNormalOpacity: 1.0
|
||||
bgHoverOpacity: Style.hoverOpacity
|
||||
|
||||
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;
|
||||
onClicked: root.deleteShare()
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
CustomButton {
|
||||
Layout.fillWidth: true
|
||||
height: visible ? implicitHeight : 0
|
||||
implicitWidth: parent.width / 2
|
||||
height: Style.standardPrimaryButtonHeight
|
||||
|
||||
imageSource: "image://svgimage-custom-color/add.svg/" + Style.ncHeaderTextColor
|
||||
text: qsTr("Add another link")
|
||||
textColor: Style.ncHeaderTextColor
|
||||
contentsFont.bold: true
|
||||
bgColor: Style.ncBlue
|
||||
bgNormalOpacity: 1.0
|
||||
bgHoverOpacity: Style.hoverOpacity
|
||||
|
||||
// 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 {}
|
||||
visible: root.isLinkShare && root.canCreateLinkShares
|
||||
enabled: visible
|
||||
|
||||
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
|
||||
}
|
||||
onClicked: root.createNewLinkShare()
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue