From 27e560d2322295f3073d6133ad9f32d8056717bc Mon Sep 17 00:00:00 2001 From: Claudio Cambra Date: Mon, 7 Aug 2023 18:19:39 +0800 Subject: [PATCH] Extract date field into a separate file Signed-off-by: Claudio Cambra --- resources.qrc | 1 + src/gui/filedetails/NCInputDateField.qml | 120 ++++++++++++++++++++++ src/gui/filedetails/ShareDetailsPage.qml | 125 ++--------------------- 3 files changed, 132 insertions(+), 114 deletions(-) create mode 100644 src/gui/filedetails/NCInputDateField.qml diff --git a/resources.qrc b/resources.qrc index fdd4bcf57..7a53e12e6 100644 --- a/resources.qrc +++ b/resources.qrc @@ -59,5 +59,6 @@ src/gui/ResolveConflictsDialog.qml src/gui/ConflictDelegate.qml src/gui/ConflictItemFileInfo.qml + src/gui/filedetails/NCInputDateField.qml diff --git a/src/gui/filedetails/NCInputDateField.qml b/src/gui/filedetails/NCInputDateField.qml new file mode 100644 index 000000000..bba717e83 --- /dev/null +++ b/src/gui/filedetails/NCInputDateField.qml @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2022 by Claudio Cambra + * + * 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. + */ + +// 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. +NCInputTextField { + id: root + + function updateText() { + text = _textFromValue(_value, root.locale); + } + + // 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(root.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 = root.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)); + } + + function _textFromValue(value, locale) { + const dateFromValue = new Date(value * dayInMSecs); + return dateFromValue.toLocaleDateString(root.locale, Locale.NarrowFormat); + } + + function _valueFromText(text, locale) { + const dateFromText = _parseDateString(text); + return Math.floor(dateFromText.getTime() / dayInMSecs); + } + + property var date: new Date().getTime() * 1000 // QDateTime msecsFromEpoch + property var minimumDate: 0 + property var maximumDate: Number.MAX_SAFE_INTEGER + + // 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.date / dayInMSecs) + // Reset the model data after binding broken on user interact + onExpireDateReducedChanged: { + value = expireDateReduced; + updateText(); + } + + readonly property int _maximumExpireDateReduced: Math.floor(maximumDate / dayInMSecs) + readonly property int _minimumExpireDateReduced: Math.floor(minimumDate / dayInMSecs) + + readonly property var _from: minimumExpireDateReduced + readonly property var _to: maximumExpireDateReduced + readonly property var _value: expireDateReduced + + validInput: { + const value = valueFromText(text); + return value >= _from && value <= _to; + } + + text: _textFromValue(_value, locale) + inputMethodHints: Qt.ImhDate + + onAccepted: root.date = _valueFromText(text, locale); +} \ No newline at end of file diff --git a/src/gui/filedetails/ShareDetailsPage.qml b/src/gui/filedetails/ShareDetailsPage.qml index 09c3855d1..380fd1142 100644 --- a/src/gui/filedetails/ShareDetailsPage.qml +++ b/src/gui/filedetails/ShareDetailsPage.qml @@ -731,127 +731,24 @@ Page { sourceSize.height: scrollContentsColumn.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. - NCInputTextField { + NCInputDateField { id: expireDateField - function updateText() { - text = textFromValue(value, locale); - } - - // 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; - updateText(); - } - - // 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 - } - - readonly property var from: minimumExpireDateReduced - readonly property var to: maximumExpireDateReduced - property var value: expireDateReduced - - // 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)); - } - - function textFromValue(value, locale) { - const dateFromValue = new Date(value * dayInMSecs); - return dateFromValue.toLocaleDateString(Qt.locale(), Locale.NarrowFormat); - } - - function valueFromText(text, locale) { - const dateFromText = parseDateString(text); - return Math.floor(dateFromText.getTime() / dayInMSecs); - } - - Layout.fillWidth: true - height: visible ? implicitHeight : 0 - - validInput: { - const value = valueFromText(text); - return value >= from && value <= to; - } - - text: textFromValue(expireDateSpinBox.value, expireDateSpinBox.locale) - inputMethodHints: expireDateSpinBox.inputMethodHints - - onAccepted: { + date: root.expireDate + onDateChanged: { const value = valueFromText(text, expireDateSpinBox.locale); root.setExpireDate(value * dayInMSecs); root.waitingForExpireDateChange = true; } - inputMethodHints: Qt.ImhDate + maximumDate: root.maximumExpireDate + minimumDate: { + const currentDate = new Date(); + // Start of day at 00:00:0000 UTC + return new Date(Date.UTC(currentDate.getFullYear(), + currentDate.getMonth(), + currentDate.getDate() + 1)); + } enabled: root.expireDateEnabled && !root.waitingForExpireDateChange &&