diff --git a/qml/harbour-nextcloudnotes.qml b/qml/harbour-nextcloudnotes.qml index 24e01c8..585a648 100644 --- a/qml/harbour-nextcloudnotes.qml +++ b/qml/harbour-nextcloudnotes.qml @@ -10,9 +10,13 @@ ApplicationWindow ConfigurationGroup { id: appSettings path: "/apps/harbour-nextcloudnotes/settings" - synchronous: true + //synchronous: true property int currentAccount: value("currentAccount", -1) + property int autoSyncInterval: value("autoSyncInterval", 0) + property int previewLineCount: value("previewLineCount", 4) + property string groupBy: value("groupBy", "date") + property bool showSeparator: value("showSeparator", false) } ConfigurationValue { diff --git a/qml/pages/NotePage.qml b/qml/pages/NotePage.qml index 895732d..031e8ab 100644 --- a/qml/pages/NotePage.qml +++ b/qml/pages/NotePage.qml @@ -4,22 +4,36 @@ import Sailfish.Silica 1.0 Dialog { id: noteDialog + function reloadContent() { + dialogHeader.title = account.model.get(noteIndex).title + contentLabel.plainText = account.model.get(noteIndex).content + contentLabel.parse() + } + acceptDestination: Qt.resolvedUrl("EditPage.qml") acceptDestinationProperties: { account: account; noteIndex: noteIndex } Component.onCompleted: acceptDestinationProperties = { account: account, noteIndex: noteIndex } onStatusChanged: { if (status === PageStatus.Active) { - dialogHeader.title = account.model.get(noteIndex).title - contentLabel.plainText = account.model.get(noteIndex).content - contentLabel.parse() + account.getNote(account.model.get(noteIndex).id) + reloadContent() } } Connections { - target: account.model.get(noteIndex) - onTitleChanged: dialogHeader.title = account.model.get(noteIndex).title + target: account/*.model.get(noteIndex) + onTitleChanged: { + console.log("Title changed") + dialogHeader.title = account.model.get(noteIndex).title + } onContentChanged: { + console.log("Content changed") contentLabel.plainText = account.model.get(noteIndex).content contentLabel.parse() + }*/ + onBusyChanged: { + if (account.busy === false) { + reloadContent() + } } } @@ -46,6 +60,34 @@ Dialog { Column { id: column width: parent.width + RemorsePopup { + id: remorse + onTriggered: pageStack.pop() + } + PullDownMenu { + busy: account ? account.busy : false + + MenuItem { + text: qsTr("Delete") + enabled: account ? true : false + //visible: appSettings.currentAccount >= 0 + onClicked: remorse.execute("Deleting", function() { account.deleteNote(account.model.get(noteIndex).id) } ) + } + MenuItem { + text: enabled ? qsTr("Reload") : qsTr("Updating...") + enabled: account ? !account.busy : false + //visible: appSettings.currentAccount >= 0 + onClicked: account.getNote(account.model.get(noteIndex).id) + } + MenuLabel { + visible: appSettings.currentAccount >= 0 + text: account ? ( + qsTr("Last update") + ": " + ( + new Date(account.update).valueOf() !== 0 ? + new Date(account.update).toLocaleString(Qt.locale(), Locale.ShortFormat) : + qsTr("never"))) : "" + } + } DialogHeader { id: dialogHeader @@ -67,7 +109,7 @@ Dialog { tmpText = tmpText.replace(markdown[i].regex, markdown[i].replace) } text = tmpText - console.log(text) + //console.log(text) } } } diff --git a/qml/pages/NotesApi.qml b/qml/pages/NotesApi.qml index 915dc8e..04675de 100644 --- a/qml/pages/NotesApi.qml +++ b/qml/pages/NotesApi.qml @@ -6,6 +6,8 @@ Item { property string uuid property string name property url server + property url url + property string version: "v0.2" property string username property string password property date update @@ -13,9 +15,13 @@ Item { property bool unencryptedConnection property var model: ListModel { } + property var json: [ ] //property string file: StandardPaths.data + "/" + uuid + ".json" //property bool saveFile: false property bool busy: false + property int status: 204 + property string statusText: "No Content" + //TODO: put content into an array ConfigurationGroup { id: account @@ -25,6 +31,7 @@ Item { Component.onCompleted: { name = account.value("name", "", String) server = account.value("server", "", String) + url = server + "/index.php/apps/notes/api/" + version + "/notes" username = account.value("username", "", String) password = account.value("password", "", String) update = account.value("update", "", Date) @@ -49,7 +56,7 @@ Item { function callApi(method, data) { busy = true - var endpoint = server + "/index.php/apps/notes/api/v0.2/notes" + var endpoint = url if (data && (method === "GET" || method === "PUT" || method === "DELETE")) { if (data.id) { endpoint = endpoint + "/" + data.id @@ -65,37 +72,39 @@ Item { apiReq.onreadystatechange = function() { if (apiReq.readyState === XMLHttpRequest.DONE) { if (apiReq.status === 200) { - console.log("Successfull API request!") - //console.log(apiReq.responseText) + //console.log("Successfull API request!") + console.log("Network status: " + apiReq.statusText + " (" + apiReq.status + ")") - var json = JSON.parse(apiReq.responseText) + json = JSON.parse(apiReq.responseText) switch(method) { case "GET": if (Array.isArray(json)) { - console.log("Got all notes") + console.log("Received all notes via API: " + endpoint) model.clear() for (var element in json) { model.append(json[element]) + model.setProperty(element, "date", getDisplayDate(json[element].modified)) } update = new Date() } else { - console.log("Got a single note") + console.log("Received a single note via API: " + endpoint) for (var i = 0; i < model.count; i++) { var listItem = model.get(i) if (listItem.id === json.id){ model.set(i, json) + model.setProperty(i, "date", getDisplayDate(json.modified)) } } } break; case "POST": - console.log("Created a note") + console.log("Created a note via API: " + endpoint) model.append(json) model.move(model.count-1, 0, 1) break; case "PUT": - console.log("Updated a note") + console.log("Updated a note via API: " + endpoint) for (var i = 0; i < model.count; i++) { var listItem = model.get(i) if (listItem.id === json.id){ @@ -104,7 +113,7 @@ Item { } break; case "DELETE": - console.log("Deleted a note") + console.log("Deleted a note via API: " + endpoint) for (var i = 0; i < model.count; i++) { var listItem = model.get(i) if (listItem.id === data.id){ @@ -127,13 +136,13 @@ Item { console.log("Note does not exist!") }*/ else { - console.log("Networking error: " + apiReq.statusText + " (" + apiReq.status + ")") + console.log("Network error: " + apiReq.statusText + " (" + apiReq.status + ")") } + status = apiReq.status + statusText = apiReq.statusText + //model.sync() busy = false } - else { - //console.log("HTTP ready state: " + apiReq.readyState) - } } if (method === "GET") { apiReq.send() @@ -192,6 +201,27 @@ Item { } } + // source: https://stackoverflow.com/a/14339782 + function getDisplayDate(date) { + var today = new Date() + today.setHours(0) + today.setMinutes(0) + today.setSeconds(0) + today.setMilliseconds(0) + var compDate = new Date(date*1000) + compDate.setHours(0) + compDate.setMinutes(0) + compDate.setSeconds(0) + compDate.setMilliseconds(0) + if (compDate.getTime() === today.getTime()) { + return qsTr("Today") + } else if ((today.getTime() - compDate.getTime()) <= (24 * 60 * 60 *1000)) { + return qsTr("Yesterday") + } else { + return compDate.toLocaleDateString(Qt.locale(), Locale.ShortFormat) + } + } + /*Component.onCompleted: { if (saveFile) { if (account.name === "") { diff --git a/qml/pages/NotesPage.qml b/qml/pages/NotesPage.qml index 4f66f45..564d0f1 100644 --- a/qml/pages/NotesPage.qml +++ b/qml/pages/NotesPage.qml @@ -4,10 +4,20 @@ import Sailfish.Silica 1.0 Page { id: page + Timer { + id: autoSyncTimer + interval: appSettings.autoSyncInterval * 1000 + repeat: true + running: interval > 0 && appWindow.visible + triggeredOnStart: true + onTriggered: nextcloudAccounts.itemAt(appSettings.currentAccount).getNotes() + onIntervalChanged: console.log("Auto-Sync every " + interval / 1000 + " seconds") + } + onStatusChanged: { if (status === PageStatus.Active) { if (nextcloudAccounts.count > 0) { - nextcloudAccounts.itemAt(appSettings.currentAccount).getNotes() + autoSyncTimer.restart() } else { addAccountHint.restart() @@ -18,7 +28,6 @@ Page { SilicaListView { id: notesList anchors.fill: parent - spacing: Theme.paddingLarge PullDownMenu { busy: nextcloudAccounts.itemAt(appSettings.currentAccount) ? nextcloudAccounts.itemAt(appSettings.currentAccount).busy : false @@ -29,12 +38,12 @@ Page { } MenuItem { text: qsTr("Add note") - enabled: nextcloudAccounts.itemAt(appSettings.currentAccount) ? !nextcloudAccounts.itemAt(appSettings.currentAccount).busy : false + enabled: nextcloudAccounts.itemAt(appSettings.currentAccount) ? true : false visible: appSettings.currentAccount >= 0 onClicked: nextcloudAccounts.itemAt(appSettings.currentAccount).createNote() } MenuItem { - text: qsTr("Reload") + text: enabled ? qsTr("Reload") : qsTr("Updating...") enabled: nextcloudAccounts.itemAt(appSettings.currentAccount) ? !nextcloudAccounts.itemAt(appSettings.currentAccount).busy : false visible: appSettings.currentAccount >= 0 onClicked: nextcloudAccounts.itemAt(appSettings.currentAccount).getNotes() @@ -52,6 +61,12 @@ Page { header: PageHeader { title: nextcloudAccounts.itemAt(appSettings.currentAccount).name //qsTr("Nextclound Notes") description: nextcloudAccounts.itemAt(appSettings.currentAccount).username + "@" + nextcloudAccounts.itemAt(appSettings.currentAccount).server + /*BusyIndicator { + running: nextcloudAccounts.itemAt(appSettings.currentAccount) ? nextcloudAccounts.itemAt(appSettings.currentAccount).busy : false + x: Theme.horizontalPageMargin + anchors.verticalCenter: parent.verticalCenter + }*/ + /*SearchField { width: parent.width placeholderText: qsTr("Nextcloud Notes") @@ -74,12 +89,17 @@ Page { id: note contentHeight: titleLabel.height + previewLabel.height + 2*Theme.paddingSmall + Separator { + width: parent.width + color: Theme.primaryColor + anchors.top: titleLabel.top + visible: appSettings.showSeparator && index !== 0 + } + IconButton { id: isFavoriteIcon anchors.left: parent.left - anchors.leftMargin: Theme.paddingSmall anchors.top: parent.top - width: Theme.iconSizeMedium icon.source: (favorite ? "image://theme/icon-m-favorite-selected?" : "image://theme/icon-m-favorite?") + (note.highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor) onClicked: { @@ -92,7 +112,7 @@ Page { id: titleLabel anchors.left: isFavoriteIcon.right anchors.leftMargin: Theme.paddingSmall - anchors.right: parent.right + anchors.right: categoryRectangle.left anchors.rightMargin: Theme.horizontalPageMargin anchors.top: parent.top text: title @@ -100,6 +120,27 @@ Page { color: note.highlighted ? Theme.highlightColor : Theme.primaryColor } + Rectangle { + id: categoryRectangle + anchors.right: parent.right + anchors.rightMargin: Theme.horizontalPageMargin + anchors.top: parent.top + anchors.topMargin: Theme.paddingSmall + width: categoryLabel.width + Theme.paddingLarge + height: categoryLabel.height + Theme.paddingSmall + color: "transparent" + border.color: Theme.highlightColor + radius: height / 4 + visible: appSettings.groupBy !== "category" && categoryLabel.text.length > 0 + Label { + id: categoryLabel + anchors.centerIn: parent + text: category + color: Theme.secondaryColor + font.pixelSize: Theme.fontSizeExtraSmall + } + } + Label { id: previewLabel anchors.left: isFavoriteIcon.right @@ -107,14 +148,20 @@ Page { anchors.right: parent.right anchors.rightMargin: Theme.horizontalPageMargin anchors.top: titleLabel.bottom - anchors.topMargin: Theme.paddingMedium - height: Theme.itemSizeExtraLarge text: content font.pixelSize: Theme.fontSizeExtraSmall textFormat: Text.PlainText wrapMode: Text.Wrap elide: Text.ElideRight + maximumLineCount: appSettings.previewLineCount > 0 ? appSettings.previewLineCount : 1 + visible: appSettings.previewLineCount > 0 color: note.highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + Component.onCompleted: { + var lines = text.split('\n') + lines.splice(0,1); + text = lines.join('\n'); + text = text.replace(/^\s*$(?:\r\n?|\n)/gm, "") + } } onClicked: pageStack.push(Qt.resolvedUrl("NotePage.qml"), { account: nextcloudAccounts.itemAt(appSettings.currentAccount), noteIndex: index } ) @@ -138,7 +185,14 @@ Page { } } - section.property: "category" + /*section.property: "category" + section.criteria: ViewSection.FullString + section.labelPositioning: ViewSection.InlineLabels + section.delegate: SectionHeader { + text: section + }*/ + + section.property: appSettings.groupBy section.criteria: ViewSection.FullString section.labelPositioning: ViewSection.InlineLabels section.delegate: SectionHeader { @@ -149,16 +203,10 @@ Page { id: busyIndicator anchors.centerIn: parent size: BusyIndicatorSize.Large - visible: nextcloudAccounts.itemAt(appSettings.currentAccount) ? nextcloudAccounts.itemAt(appSettings.currentAccount).busy && notesList.count === 0 : false + visible: nextcloudAccounts.itemAt(appSettings.currentAccount) ? (notesList.count === 0 && nextcloudAccounts.itemAt(appSettings.currentAccount).busy) : false running: visible } - ViewPlaceholder { - enabled: notesList.count === 0 && !busyIndicator.running && !noLoginPlaceholder.enabled - text: qsTr("No notes yet") - hintText: qsTr("Pull down to add a note") - } - ViewPlaceholder { id: noLoginPlaceholder enabled: nextcloudUUIDs.value.length <= 0 @@ -166,6 +214,20 @@ Page { hintText: qsTr("Got to the settings to add an account") } + ViewPlaceholder { + id: noNotesPlaceholder + enabled: nextcloudAccounts.itemAt(appSettings.currentAccount) ? (nextcloudAccounts.itemAt(appSettings.currentAccount).status === 204 && !busyIndicator.running && !noLoginPlaceholder.enabled) : false + text: qsTr("No notes yet") + hintText: qsTr("Pull down to add a note") + } + + ViewPlaceholder { + id: errorPlaceholder + enabled: notesList.count === 0 && !busyIndicator.running && !noNotesPlaceholder.enabled && !noLoginPlaceholder.enabled + text: qsTr("An error occurred") + hintText: nextcloudAccounts.itemAt(appSettings.currentAccount).statusText + } + TouchInteractionHint { id: addAccountHint interactionMode: TouchInteraction.Pull diff --git a/qml/pages/SettingsPage.qml b/qml/pages/SettingsPage.qml index 95d6fee..f417049 100644 --- a/qml/pages/SettingsPage.qml +++ b/qml/pages/SettingsPage.qml @@ -29,7 +29,6 @@ Page { SectionHeader { text: qsTr("Accounts") } - Label { id: noAccountsLabel visible: nextcloudAccounts.count <= 0 @@ -38,16 +37,15 @@ Page { color: Theme.secondaryHighlightColor anchors.horizontalCenter: parent.horizontalCenter } - Repeater { model: nextcloudAccounts.count delegate: ListItem { id: accountListItem - contentHeight: textSwitch.height - highlighted: textSwitch.down + contentHeight: accountTextSwitch.height + highlighted: accountTextSwitch.down TextSwitch { - id: textSwitch + id: accountTextSwitch automaticCheck: false checked: index === appSettings.currentAccount text: nextcloudAccounts.itemAt(index).name.length <= 0 ? qsTr("Unnamed account") : nextcloudAccounts.itemAt(index).name @@ -79,7 +77,6 @@ Page { } } } - Button { text: qsTr("Add account") anchors.horizontalCenter: parent.horizontalCenter @@ -97,6 +94,76 @@ Page { }) } } + + SectionHeader { + text: qsTr("Appearance") + } + ComboBox { + id: groupByComboBox + property var names: [qsTr("Date"), qsTr("Category")] + label: qsTr("Group notes by") + menu: ContextMenu { + Repeater { + id: groupByRepeater + model: ["date", "category"] + MenuItem { + text: groupByComboBox.names[index] + Component.onCompleted: { + if (modelData === appSettings.groupBy) { + groupByComboBox.currentIndex = index + } + } + } + } + } + onCurrentIndexChanged: { + appSettings.groupBy = groupByRepeater.model[currentIndex] + } + } + TextSwitch { + text: qsTr("Show separator") + description: qsTr("Show a separator line between the notes") + checked: appSettings.showSeparator + onCheckedChanged: appSettings.showSeparator = checked + } + Slider { + width: parent.width + minimumValue: 0 + maximumValue: 20 + stepSize: 1 + value: appSettings.previewLineCount + valueText: sliderValue + " " + qsTr("lines") + label: qsTr("Number of lines to preview in the list view") + onSliderValueChanged: appSettings.previewLineCount = sliderValue + } + + SectionHeader { + text: qsTr("Synchronization") + } + ComboBox { + id: autoSyncComboBox + label: qsTr("Auto-Sync") + description: qsTr("Periodically pull notes from the server") + menu: ContextMenu { + Repeater { + id: autoSyncIntervalRepeater + model: [0, 3, 5, 10, 20, 30, 60, 120, 300, 600] + MenuItem { + text: modelData === 0 ? + qsTr("Disabled") : (qsTr("every") + " " + + (parseInt(modelData / 60) ? + (parseInt(modelData / 60) + " " + qsTr("Minutes")) : + (modelData + " " + qsTr("Seconds")))) + Component.onCompleted: { + if (modelData === appSettings.autoSyncInterval) { + autoSyncComboBox.currentIndex = index + } + } + } + } + } + onCurrentIndexChanged: appSettings.autoSyncInterval = autoSyncIntervalRepeater.model[currentIndex] + } } VerticalScrollDecorator {} diff --git a/translations/harbour-nextcloudnotes.ts b/translations/harbour-nextcloudnotes.ts index ffe4cfa..ce33f37 100644 --- a/translations/harbour-nextcloudnotes.ts +++ b/translations/harbour-nextcloudnotes.ts @@ -137,10 +137,6 @@ No account yet - - Got to the settings to add an account - - Open the settings to configure your Nextcloud accounts @@ -149,6 +145,18 @@ Deleting note + + Got to the settings to add an account + + + + An error occurred + + + + Updating... + + SettingsPage @@ -188,6 +196,30 @@ Deleting account + + Synchronization + + + + Auto-Sync + + + + Periodically pull notes from the server + + + + Disabled + + + + Minutes + + + + Seconds + + UnencryptedDialog