mirror of
https://github.com/nextcloud/desktop.git
synced 2024-10-24 13:25:52 +03:00
Merge pull request #5194 from nextcloud/feature/share-details-page
Replace share settings popup with a page on a StackView
This commit is contained in:
commit
0aea5cb0d0
9 changed files with 889 additions and 608 deletions
|
@ -9,12 +9,14 @@
|
|||
<file>src/gui/ErrorBox.qml</file>
|
||||
<file>src/gui/filedetails/FileActivityView.qml</file>
|
||||
<file>src/gui/filedetails/FileDetailsPage.qml</file>
|
||||
<file>src/gui/filedetails/FileDetailsView.qml</file>
|
||||
<file>src/gui/filedetails/FileDetailsWindow.qml</file>
|
||||
<file>src/gui/filedetails/NCInputTextEdit.qml</file>
|
||||
<file>src/gui/filedetails/NCInputTextField.qml</file>
|
||||
<file>src/gui/filedetails/NCTabButton.qml</file>
|
||||
<file>src/gui/filedetails/ShareeDelegate.qml</file>
|
||||
<file>src/gui/filedetails/ShareDelegate.qml</file>
|
||||
<file>src/gui/filedetails/ShareDetailsPage.qml</file>
|
||||
<file>src/gui/filedetails/ShareeSearchField.qml</file>
|
||||
<file>src/gui/filedetails/ShareView.qml</file>
|
||||
<file>src/gui/tray/Window.qml</file>
|
||||
|
|
|
@ -24,7 +24,7 @@ Page {
|
|||
id: root
|
||||
|
||||
property var accountState: ({})
|
||||
property string localPath: ({})
|
||||
property string localPath: ""
|
||||
|
||||
// We want the SwipeView to "spill" over the edges of the window to really
|
||||
// make it look nice. If we apply page-wide padding, however, the swipe
|
||||
|
@ -33,6 +33,7 @@ Page {
|
|||
// padding, which we have to apply selectively to achieve our desired effect.
|
||||
property int intendedPadding: Style.standardSpacing * 2
|
||||
property int iconSize: 32
|
||||
property StackView rootStackView: StackView {}
|
||||
|
||||
property FileDetails fileDetails: FileDetails {
|
||||
id: fileDetails
|
||||
|
@ -186,6 +187,7 @@ Page {
|
|||
fileDetails: root.fileDetails
|
||||
horizontalPadding: root.intendedPadding
|
||||
iconSize: root.iconSize
|
||||
rootStackView: root.rootStackView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
35
src/gui/filedetails/FileDetailsView.qml
Normal file
35
src/gui/filedetails/FileDetailsView.qml
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import QtQuick.Controls 2.15
|
||||
|
||||
import com.nextcloud.desktopclient 1.0
|
||||
import Style 1.0
|
||||
|
||||
StackView {
|
||||
id: root
|
||||
|
||||
property var accountState: ({})
|
||||
property string localPath: ""
|
||||
|
||||
initialItem: FileDetailsPage {
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
accountState: root.accountState
|
||||
localPath: root.localPath
|
||||
rootStackView: root
|
||||
}
|
||||
}
|
|
@ -33,7 +33,7 @@ ApplicationWindow {
|
|||
|
||||
title: qsTr("File details of %1 · %2").arg(fileDetailsPage.fileDetails.name).arg(Systray.windowTitle)
|
||||
|
||||
FileDetailsPage {
|
||||
FileDetailsView {
|
||||
id: fileDetailsPage
|
||||
anchors.fill: parent
|
||||
accountState: root.accountState
|
||||
|
|
|
@ -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,6 +44,20 @@ GridLayout {
|
|||
signal setPassword(string password)
|
||||
signal setNote(string note)
|
||||
|
||||
property int iconSize: 32
|
||||
property FileDetails fileDetails: FileDetails {}
|
||||
property StackView rootStackView: StackView {}
|
||||
|
||||
property bool canCreateLinkShares: true
|
||||
|
||||
readonly property bool isLinkShare: model.shareType === ShareModel.ShareTypeLink
|
||||
readonly property bool isPlaceholderLinkShare: model.shareType === ShareModel.ShareTypePlaceholderLink
|
||||
|
||||
readonly property string text: model.display ?? ""
|
||||
readonly property string detailText: model.detailText ?? ""
|
||||
readonly property string iconUrl: model.iconUrl ?? ""
|
||||
readonly property string avatarUrl: model.avatarUrl ?? ""
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
|
@ -49,122 +67,6 @@ GridLayout {
|
|||
columnSpacing: Style.standardSpacing / 2
|
||||
rowSpacing: Style.standardSpacing / 2
|
||||
|
||||
property int iconSize: 32
|
||||
|
||||
property var share: model.share ?? ({})
|
||||
|
||||
property string iconUrl: model.iconUrl ?? ""
|
||||
property string avatarUrl: model.avatarUrl ?? ""
|
||||
property string text: model.display ?? ""
|
||||
property string detailText: model.detailText ?? ""
|
||||
property string link: model.link ?? ""
|
||||
property string note: model.note ?? ""
|
||||
property string password: model.password ?? ""
|
||||
property string passwordPlaceholder: "●●●●●●●●●●"
|
||||
|
||||
property var expireDate: model.expireDate // Don't use int as we are limited
|
||||
property var maximumExpireDate: model.enforcedMaximumExpireDate
|
||||
|
||||
property string linkShareLabel: model.linkShareLabel ?? ""
|
||||
|
||||
property bool editingAllowed: model.editingAllowed
|
||||
property bool noteEnabled: model.noteEnabled
|
||||
property bool expireDateEnabled: model.expireDateEnabled
|
||||
property bool expireDateEnforced: model.expireDateEnforced
|
||||
property bool passwordProtectEnabled: model.passwordProtectEnabled
|
||||
property bool passwordEnforced: model.passwordEnforced
|
||||
|
||||
property bool isLinkShare: model.shareType === ShareModel.ShareTypeLink
|
||||
property bool isPlaceholderLinkShare: model.shareType === ShareModel.ShareTypePlaceholderLink
|
||||
|
||||
property bool canCreateLinkShares: true
|
||||
|
||||
property bool waitingForEditingAllowedChange: false
|
||||
property bool waitingForNoteEnabledChange: false
|
||||
property bool waitingForExpireDateEnabledChange: false
|
||||
property bool waitingForPasswordProtectEnabledChange: false
|
||||
property bool waitingForExpireDateChange: false
|
||||
property bool waitingForLinkShareLabelChange: false
|
||||
property bool waitingForPasswordChange: false
|
||||
property bool waitingForNoteChange: false
|
||||
|
||||
function 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()
|
||||
|
||||
Item {
|
||||
id: imageItem
|
||||
|
||||
|
@ -310,506 +212,50 @@ GridLayout {
|
|||
visible: !root.isPlaceholderLinkShare
|
||||
enabled: visible
|
||||
|
||||
onClicked: moreMenu.popup()
|
||||
onClicked: root.rootStackView.push(shareDetailsPageComponent, {}, StackView.PushTransition)
|
||||
|
||||
Menu {
|
||||
id: moreMenu
|
||||
Component {
|
||||
id: shareDetailsPageComponent
|
||||
ShareDetailsPage {
|
||||
id: shareDetailsPage
|
||||
|
||||
property int rowIconWidth: 16
|
||||
property int indicatorItemWidth: 20
|
||||
property int indicatorSpacing: Style.standardSpacing
|
||||
property int itemPadding: Style.smallSpacing
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
|
||||
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
|
||||
}
|
||||
fileDetails: root.fileDetails
|
||||
shareModelData: model
|
||||
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: moreMenu.itemPadding
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: moreMenu.itemPadding
|
||||
height: visible ? implicitHeight : 0
|
||||
spacing: moreMenu.indicatorSpacing
|
||||
canCreateLinkShares: root.canCreateLinkShares
|
||||
|
||||
visible: root.isLinkShare
|
||||
onCloseShareDetails: root.rootStackView.pop(root.rootStackView.initialItem, StackView.PopTransition)
|
||||
|
||||
Image {
|
||||
Layout.preferredWidth: moreMenu.indicatorItemWidth
|
||||
Layout.fillHeight: true
|
||||
onToggleAllowEditing: root.toggleAllowEditing(enable)
|
||||
onToggleAllowResharing: root.toggleAllowResharing(enable)
|
||||
onTogglePasswordProtect: root.togglePasswordProtect(enable)
|
||||
onToggleExpirationDate: root.toggleExpirationDate(enable)
|
||||
onToggleNoteToRecipient: root.toggleNoteToRecipient(enable)
|
||||
|
||||
verticalAlignment: Image.AlignVCenter
|
||||
horizontalAlignment: Image.AlignHCenter
|
||||
fillMode: Image.Pad
|
||||
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)
|
||||
|
||||
source: "image://svgimage-custom-color/edit.svg/" + Style.menuBorder
|
||||
sourceSize.width: moreMenu.rowIconWidth
|
||||
sourceSize.height: moreMenu.rowIconWidth
|
||||
onDeleteShare: {
|
||||
root.deleteShare();
|
||||
closeShareDetails();
|
||||
}
|
||||
onCreateNewLinkShare: {
|
||||
root.createNewLinkShare();
|
||||
closeShareDetails();
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Connections {
|
||||
target: root
|
||||
function onResetMenu() { shareDetailsPage.resetMenu() }
|
||||
function onResetPasswordField() { shareDetailsPage.resetPasswordField() }
|
||||
function onShowPasswordSetError(errorMessage) { shareDetailsPage.showPasswordSetError(errorMessage) }
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
787
src/gui/filedetails/ShareDetailsPage.qml
Normal file
787
src/gui/filedetails/ShareDetailsPage.qml
Normal file
|
@ -0,0 +1,787 @@
|
|||
/*
|
||||
* 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.padding
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
EnforcedPlainTextLabel {
|
||||
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()
|
||||
}
|
||||
|
||||
EnforcedPlainTextLabel {
|
||||
id: secondaryLabel
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: root.padding
|
||||
|
||||
text: root.fileDetails.name
|
||||
color: Style.ncSecondaryTextColor
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: ScrollView {
|
||||
contentWidth: availableWidth
|
||||
clip: true
|
||||
|
||||
ColumnLayout {
|
||||
id: moreMenu
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: passwordProtectEnabledMenuItem
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
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 {
|
||||
Layout.fillWidth: true
|
||||
|
||||
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: ""
|
||||
|
||||
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 {}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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: {
|
||||
|
|
|
@ -18,6 +18,8 @@ Button {
|
|||
property alias contentsFont: contents.font
|
||||
|
||||
property alias bgColor: bgRectangle.color
|
||||
property alias bgNormalColor: bgRectangle.normalColor
|
||||
property alias bgHoverColor: bgRectangle.hoverColor
|
||||
property alias bgNormalOpacity: bgRectangle.normalOpacity
|
||||
property alias bgHoverOpacity: bgRectangle.hoverOpacity
|
||||
|
||||
|
|
|
@ -14,12 +14,16 @@
|
|||
|
||||
import QtQuick 2.15
|
||||
|
||||
import Style 1.0
|
||||
|
||||
Rectangle {
|
||||
property bool hovered: false
|
||||
property real normalOpacity: 0.3
|
||||
property real hoverOpacity: 1.0
|
||||
property color normalColor: Style.buttonBackgroundColor
|
||||
property color hoverColor: Style.buttonBackgroundColor
|
||||
|
||||
color: "transparent"
|
||||
color: hovered ? hoverColor : normalColor
|
||||
opacity: hovered ? hoverOpacity : normalOpacity
|
||||
radius: width / 2
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue