Some work on login workflow and configuration handling

This commit is contained in:
Scharel Clemens 2021-01-03 19:07:47 +01:00
parent 6d0a6d67d9
commit af5acbc4a1
14 changed files with 319 additions and 350 deletions

View file

@ -17,6 +17,7 @@ CONFIG += sailfishapp
DEFINES += APP_VERSION=\\\"$$VERSION\\\" DEFINES += APP_VERSION=\\\"$$VERSION\\\"
HEADERS += src/note.h \ HEADERS += src/note.h \
src/accounthash.h \
src/notesapi.h \ src/notesapi.h \
src/notesmodel.h src/notesmodel.h

View file

@ -12,7 +12,7 @@ CoverBackground {
CoverActionList { CoverActionList {
id: coverAction id: coverAction
enabled: appSettings.currentAccount.length > 0 enabled: account != null
CoverAction { CoverAction {
iconSource: "image://theme/icon-cover-new" iconSource: "image://theme/icon-cover-new"

View file

@ -8,43 +8,14 @@ ApplicationWindow
{ {
id: appWindow id: appWindow
// All configured accounts
ConfigurationValue {
id: accounts
key: appSettings.path + "/accountIDs"
defaultValue: [ ]
}
// Current account in use
ConfigurationGroup {
id: account
path: "/apps/harbour-nextcloudnotes/accounts/" + appSettings.currentAccount
property string name: value("name", "", String)
property url server: value("server", "", String)
property string version: value("version", "v0.2", String)
property string username: value("username", "", String)
property string password: account.value("password", "", String)
property bool doNotVerifySsl: account.value("doNotVerifySsl", false, Boolean)
property bool allowUnecrypted: account.value("allowUnecrypted", false, Boolean)
property date update: value("update", "", Date)
onServerChanged: notesApi.server = server
onUsernameChanged: {
console.log("Username: " + username)
notesApi.username = username
}
onPasswordChanged: notesApi.password = password
onDoNotVerifySslChanged: notesApi.verifySsl = !doNotVerifySsl
onNameChanged: console.log("Using account: " + name)
}
// General settings of the app // General settings of the app
ConfigurationGroup { ConfigurationGroup {
id: appSettings id: appSettings
path: "/apps/harbour-nextcloudnotes/settings" path: "/apps/harbour-nextcloudnotes"
property bool initialized: false property bool initialized: false
property string currentAccount: value("currentAccount", "", String) property var accounts: value("accounts", [], Array)
property string currentAccountIndex: value("currentAccountIndex", -1, Number)
property int autoSyncInterval: value("autoSyncInterval", 0, Number) property int autoSyncInterval: value("autoSyncInterval", 0, Number)
property int previewLineCount: value("previewLineCount", 4, Number) property int previewLineCount: value("previewLineCount", 4, Number)
property bool favoritesOnTop: value("favoritesOnTop", true, Boolean) property bool favoritesOnTop: value("favoritesOnTop", true, Boolean)
@ -53,9 +24,12 @@ ApplicationWindow
property bool useMonoFont: value("useMonoFont", false, Boolean) property bool useMonoFont: value("useMonoFont", false, Boolean)
property bool useCapitalX: value("useCapitalX", false, Boolean) property bool useCapitalX: value("useCapitalX", false, Boolean)
onCurrentAccountChanged: { onCurrentAccountIndexChanged: {
account.path = "/apps/harbour-nextcloudnotes/accounts/" + currentAccount console.log("Current account index: " + currentAccountIndex)
notesModel.account = currentAccount if (currentAccountIndex >= 0 && currentAccountIndex < accounts.length) {
account = accounts[currentAccountIndex]
console.log("Current account: " + account.username + "@" + account.url)
}
} }
onSortByChanged: { onSortByChanged: {
@ -68,53 +42,18 @@ ApplicationWindow
notesProxyModel.favoritesOnTop = favoritesOnTop notesProxyModel.favoritesOnTop = favoritesOnTop
} }
function addAccount() { function createAccount(user, url) {
var uuid = uuidv4() var hash = accountHash.hash(user, url)
var tmpIDs = accounts.value console.log("Hash(" + user + "@" + url + ") = " + hash)
tmpIDs.push(uuid) return hash
accounts.value = tmpIDs
accounts.sync()
return uuid
}
ConfigurationGroup {
id: removeHelperConfGroup
}
function removeAccount(uuid) {
autoSyncTimer.stop()
var tmpIDs = accounts.value
removeHelperConfGroup.path = "/apps/harbour-nextcloudnotes/accounts/" + uuid
for (var i = tmpIDs.length-1; i >= 0; i--) {
console.log(tmpIDs)
console.log("Checking:" + tmpIDs[i])
if (tmpIDs[i] === uuid) {
console.log("Found! Removing ...")
tmpIDs.splice(i, 1)
}
console.log(tmpIDs)
}
if (appSettings.currentAccount === uuid) {
appSettings.currentAccount = ""
for (var i = tmpIDs.length-1; i >= 0 && appSettings.currentAccount === ""; i--) {
if (tmpIDs[i] !== uuid) {
appSettings.currentAccount = tmpIDs[i]
}
}
}
removeHelperConfGroup.clear()
if (autoSyncInterval > 0 && appWindow.visible) {
autoSyncTimer.start()
}
accounts.value = tmpIDs
accounts.sync()
}
function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
} }
function removeAccount(hash) {
accounts[hash] = null
currentAccount = -1 }
} }
property var account
Notification { Notification {
id: offlineNotification id: offlineNotification
expireTimeout: 0 expireTimeout: 0

View file

@ -1,65 +1,41 @@
import QtQuick 2.2 import QtQuick 2.2
import Sailfish.Silica 1.0 import Sailfish.Silica 1.0
import Nemo.Configuration 1.0 import Nemo.Configuration 1.0
import NextcloudNotes 1.0
Dialog { Dialog {
id: loginDialog id: loginDialog
property string accountId canAccept: false
property bool legacyLoginPossible: false property bool legacyLoginPossible: false
property bool flowLoginV2Possible: false property bool flowLoginV2Possible: false
property url server
property string username
property string password
property bool doNotVerifySsl: false
property bool allowUnecrypted: false
property string productName
property string version
onRejected: { onRejected: {
appSettings.removeAccount(accountId)
} }
onAccepted: { onAccepted: {
appSettings.createAccount(username, server)
} }
ConfigurationGroup { Timer {
id: account id: verifyServerTimer
path: "/apps/harbour-nextcloudnotes/accounts/" + accountId onTriggered: notesApi.getNcStatus()
property string name: value("name", qsTr("Nextcloud Login"), String)
property url server: value("server", "", String)
property string version: value("version", "v0.2", String)
property string username: value("username", "", String)
property string password: account.value("password", "", String)
property bool doNotVerifySsl: account.value("doNotVerifySsl", false, Boolean)
property bool allowUnecrypted: account.value("allowUnecrypted", false, Boolean)
Component.onCompleted: {
dialogHeader.title = name
serverField.text = server ? server : allowUnecrypted ? "http://" : "https://"
usernameField.text = username
passwordField.text = password
unsecureConnectionTextSwitch.checked = doNotVerifySsl
unencryptedConnectionTextSwitch.checked = allowUnecrypted
if (username !== "" && password !== "") {
notesApi.server = server
notesApi.username = username
notesApi.password = password
notesApi.verifySsl = !doNotVerifySsl
notesApi.verifyLogin()
} }
}
}
/*onStatusChanged: {
if (status === PageStatus.Activating)
notesApi.getNcStatus()
if (status === PageStatus.Deactivating)
notesApi.abortFlowV2Login()
}*/
Connections { Connections {
target: notesApi target: notesApi
onStatusInstalledChanged: { onStatusInstalledChanged: {
if (notesApi.statusInstalled) if (notesApi.statusInstalled)
serverField.focus = false serverField.focus = false
else {
dialogHeader.title
}
} }
onStatusVersionChanged: { onStatusVersionChanged: {
if (notesApi.statusVersion) { if (notesApi.statusVersion) {
@ -80,58 +56,77 @@ Dialog {
} }
} }
onStatusVersionStringChanged: { onStatusVersionStringChanged: {
if (notesApi.statusVersionString) if (notesApi.statusVersionString) {
dialogHeader.description = "Nextcloud " + notesApi.statusVersionString version = notesApi.statusVersionString
console.log(notesApi.statusVersionString)
}
} }
onStatusProductNameChanged: { onStatusProductNameChanged: {
if (notesApi.statusProductName) { if (notesApi.statusProductName) {
dialogHeader.title = notesApi.statusProductName productName = notesApi.statusProductName
account.name = notesApi.statusProductName console.log(notesApi.statusProductName)
} }
} }
onLoginStatusChanged: { onLoginStatusChanged: {
loginDialog.canAccept = false
apiProgressBar.indeterminate = false
switch(notesApi.loginStatus) { switch(notesApi.loginStatus) {
case notesApi.LoginLegacyReady: case NotesApi.LoginLegacyReady:
console.log("LoginLegacyReady")
apiProgressBar.label = qsTr("Enter your credentials") apiProgressBar.label = qsTr("Enter your credentials")
break; break;
//case notesApi.LoginFlowV2Initiating: case NotesApi.LoginFlowV2Initiating:
// break; console.log("LoginFlowV2Initiating")
case notesApi.LoginFlowV2Polling: apiProgressBar.indeterminate = true
apiProgressBar.label = qsTr("Follow the instructions in the browser")
break; break;
case notesApi.LoginFlowV2Success: case NotesApi.LoginFlowV2Polling:
console.log("LoginFlowV2Polling")
apiProgressBar.label = qsTr("Follow the instructions in the browser")
apiProgressBar.indeterminate = true
break;
case NotesApi.LoginFlowV2Success:
console.log("LoginFlowV2Success")
notesApi.verifyLogin() notesApi.verifyLogin()
break; break;
case notesApi.LoginFlowV2Failed: case NotesApi.LoginFlowV2Failed:
console.log("LoginFlowV2Failed")
apiProgressBar.label = qsTr("Login failed!") apiProgressBar.label = qsTr("Login failed!")
break break
case notesApi.LoginSuccess: case NotesApi.LoginSuccess:
console.log("LoginSuccess")
apiProgressBar.label = qsTr("Login successfull!") apiProgressBar.label = qsTr("Login successfull!")
account.username = notesApi.username loginDialog.canAccept = true
account.password = notesApi.password
appSettings.currentAccount = accountId
break; break;
case notesApi.LoginFailed: case NotesApi.LoginFailed:
console.log("LoginFailed")
apiProgressBar.label = qsTr("Login failed!") apiProgressBar.label = qsTr("Login failed!")
break; break;
default: default:
console.log("None")
apiProgressBar.label = "" apiProgressBar.label = ""
break;
} }
} }
onLoginUrlChanged: { onLoginUrlChanged: {
if (notesApi.loginUrl) { if (notesApi.loginUrl) {
Qt.openUrlExternally(notesApi.loginUrl) Qt.openUrlExternally(notesApi.loginUrl)
} }
else {
console.log("Login successfull")
}
} }
onServerChanged: { onServerChanged: {
if (notesApi.server) { if (notesApi.server) {
console.log("Login server: " + notesApi.server) console.log(notesApi.server)
account.server = notesApi.server server = notesApi.server
serverField.text = notesApi.server }
}
onUsernameChanged: {
if (notesApi.username) {
console.log(notesApi.username)
username = notesApi.username
}
}
onPasswordChanged: {
if (notesApi.password) {
console.log("***")
password = notesApi.password
} }
} }
} }
@ -147,6 +142,7 @@ Dialog {
DialogHeader { DialogHeader {
id: dialogHeader id: dialogHeader
title: qsTr("Nextcloud Login")
} }
Image { Image {
@ -161,8 +157,6 @@ Dialog {
id: apiProgressBar id: apiProgressBar
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
width: parent.width width: parent.width
indeterminate: notesApi.loginStatus === notesApi.LoginFlowV2Initiating ||
notesApi.loginStatus === notesApi.LoginFlowV2Polling
} }
Row { Row {
@ -170,17 +164,18 @@ Dialog {
TextField { TextField {
id: serverField id: serverField
width: parent.width - statusIcon.width - Theme.horizontalPageMargin width: parent.width - statusIcon.width - Theme.horizontalPageMargin
placeholderText: qsTr("Nextcloud server") text: server
placeholderText: productName ? productName : qsTr("Nextcloud server")
label: placeholderText label: placeholderText
validator: RegExpValidator { regExp: unencryptedConnectionTextSwitch.checked ? /^https?:\/\/([-a-zA-Z0-9@:%._\+~#=].*)/: /^https:\/\/([-a-zA-Z0-9@:%._\+~#=].*)/ } validator: RegExpValidator { regExp: unencryptedConnectionTextSwitch.checked ? /^https?:\/\/([-a-zA-Z0-9@:%._\+~#=].*)/: /^https:\/\/([-a-zA-Z0-9@:%._\+~#=].*)/ }
inputMethodHints: Qt.ImhUrlCharactersOnly inputMethodHints: Qt.ImhUrlCharactersOnly
onClicked: if (text === "") text = "https://" onClicked: if (text === "") text = allowUnecrypted ? "http://" : "https://"
onTextChanged: { onTextChanged: {
statusBusyIndicatorTimer.restart()
if (acceptableInput) { if (acceptableInput) {
notesApi.server = text notesApi.server = text
notesApi.getNcStatus()
} }
verifyServerTimer.restart()
notesApi.getNcStatus()
} }
//EnterKey.enabled: text.length > 0 //EnterKey.enabled: text.length > 0
EnterKey.iconSource: legacyLoginPossible ? "image://theme/icon-m-enter-next" : flowLoginV2Possible ? "image://theme/icon-m-enter-accept" : "image://theme/icon-m-enter-close" EnterKey.iconSource: legacyLoginPossible ? "image://theme/icon-m-enter-next" : flowLoginV2Possible ? "image://theme/icon-m-enter-accept" : "image://theme/icon-m-enter-close"
@ -199,11 +194,7 @@ Dialog {
BusyIndicator { BusyIndicator {
anchors.centerIn: parent anchors.centerIn: parent
size: BusyIndicatorSize.Medium size: BusyIndicatorSize.Medium
running: notesApi.ncStatusStatus === notesApi.NextcloudBusy || (serverField.focus && statusBusyIndicatorTimer.running && !notesApi.statusInstalled) running: notesApi.ncStatusStatus === notesApi.NextcloudBusy || (verifyServerTimer.running)
Timer {
id: statusBusyIndicatorTimer
interval: 200
}
} }
} }
} }
@ -212,10 +203,10 @@ Dialog {
id: forceLegacyButton id: forceLegacyButton
visible: debug || !notesApi.statusInstalled visible: debug || !notesApi.statusInstalled
text: qsTr("Enforce legacy login") text: qsTr("Enforce legacy login")
automaticCheck: true
onCheckedChanged: { onCheckedChanged: {
checked != checked
if (!checked) { if (!checked) {
notesApi.getNcStatus() verifyServerTimer.restart()
} }
} }
} }
@ -243,6 +234,7 @@ Dialog {
TextField { TextField {
id: usernameField id: usernameField
width: parent.width width: parent.width
text: username
placeholderText: qsTr("Username") placeholderText: qsTr("Username")
label: placeholderText label: placeholderText
inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase
@ -254,6 +246,7 @@ Dialog {
PasswordField { PasswordField {
id: passwordField id: passwordField
width: parent.width width: parent.width
text: password
placeholderText: qsTr("Password") placeholderText: qsTr("Password")
label: placeholderText label: placeholderText
errorHighlight: text.length === 0// && focus === true errorHighlight: text.length === 0// && focus === true
@ -292,29 +285,30 @@ Dialog {
} }
TextSwitch { TextSwitch {
id: unsecureConnectionTextSwitch id: unsecureConnectionTextSwitch
checked: doNotVerifySsl
text: qsTr("Do not check certificates") text: qsTr("Do not check certificates")
description: qsTr("Enable this option to allow selfsigned certificates") description: qsTr("Enable this option to allow selfsigned certificates")
onCheckedChanged: { onCheckedChanged: {
account.doNotVerifySsl = checked notesApi.verifySsl = !checked
notesApi.verifySsl = !account.doNotVerifySsl
} }
} }
TextSwitch { TextSwitch {
id: unencryptedConnectionTextSwitch id: unencryptedConnectionTextSwitch
checked: allowUnecrypted
automaticCheck: false automaticCheck: false
text: qsTr("Allow unencrypted connections") text: qsTr("Allow unencrypted connections")
description: qsTr("") //description: qsTr("")
onClicked: { onClicked: {
if (checked) { if (checked) {
checked = false allowUnecrypted = !checked
} }
else { else {
var dialog = pageStack.push(Qt.resolvedUrl("UnencryptedDialog.qml")) var dialog = pageStack.push(Qt.resolvedUrl("UnencryptedDialog.qml"))
dialog.accepted.connect(function() { dialog.accepted.connect(function() {
checked = true allowUnecrypted = true
}) })
dialog.rejected.connect(function() { dialog.rejected.connect(function() {
checked = false allowUnecrypted = false
}) })
} }
} }

View file

@ -6,7 +6,7 @@ Page {
onStatusChanged: { onStatusChanged: {
if (status === PageStatus.Activating) { if (status === PageStatus.Activating) {
if (accounts.value.length <= 0) { if (appSettings.accounts.length <= 0) {
addAccountHint.restart() addAccountHint.restart()
} }
else { else {
@ -32,16 +32,16 @@ Page {
} }
MenuItem { MenuItem {
text: qsTr("Add note") text: qsTr("Add note")
enabled: appSettings.currentAccount.length > 0 && notesApi.networkAccessible enabled: account != null && notesApi.networkAccessible
onClicked: notesApi.createNote( { 'content': "", 'modified': new Date().valueOf() / 1000 } ) onClicked: notesApi.createNote( { 'content': "", 'modified': new Date().valueOf() / 1000 } )
} }
MenuItem { MenuItem {
text: notesApi.networkAccessible && !notesApi.busy ? qsTr("Reload") : qsTr("Updating...") text: notesApi.networkAccessible && !notesApi.busy ? qsTr("Reload") : qsTr("Updating...")
enabled: appSettings.currentAccount.length > 0 && notesApi.networkAccessible && !notesApi.busy enabled: account != null && notesApi.networkAccessible && !notesApi.busy
onClicked: notes.getAllNotes() onClicked: notes.getAllNotes()
} }
MenuLabel { MenuLabel {
visible: appSettings.currentAccount.length > 0 visible: account != null
text: qsTr("Last update") + ": " + ( text: qsTr("Last update") + ": " + (
new Date(account.update).valueOf() !== 0 ? new Date(account.update).valueOf() !== 0 ?
new Date(account.update).toLocaleString(Qt.locale(), Locale.ShortFormat) : new Date(account.update).toLocaleString(Qt.locale(), Locale.ShortFormat) :
@ -224,7 +224,7 @@ Page {
ViewPlaceholder { ViewPlaceholder {
id: noLoginPlaceholder id: noLoginPlaceholder
enabled: accounts.value.length <= 0 enabled: appSettings.accounts.length <= 0
text: qsTr("No account yet") text: qsTr("No account yet")
hintText: qsTr("Got to the settings to add an account") hintText: qsTr("Got to the settings to add an account")
} }

View file

@ -32,7 +32,7 @@ Page {
} }
Label { Label {
id: noAccountsLabel id: noAccountsLabel
visible: accounts.value.length <= 0 visible: appSettings.accounts.length <= 0
text: qsTr("No Nextcloud account yet") text: qsTr("No Nextcloud account yet")
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
color: Theme.secondaryHighlightColor color: Theme.secondaryHighlightColor
@ -43,7 +43,7 @@ Page {
} }
Repeater { Repeater {
id: accountRepeater id: accountRepeater
model: accounts.value model: appSettings.accounts
delegate: ListItem { delegate: ListItem {
id: accountListItem id: accountListItem
@ -91,8 +91,7 @@ Page {
text: qsTr("Add account") text: qsTr("Add account")
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
onClicked: { onClicked: {
var newAccountID = appSettings.addAccount() var login = pageStack.push(Qt.resolvedUrl("LoginPage.qml"), { accountId: "" })
var login = pageStack.push(Qt.resolvedUrl("LoginPage.qml"), { accountId: newAccountID, addingNew: true })
} }
} }

View file

@ -17,7 +17,7 @@ Dialog {
DialogHeader { DialogHeader {
} }
Label { LinkedLabel {
x: Theme.horizontalPageMargin x: Theme.horizontalPageMargin
width: parent.width - 2*x width: parent.width - 2*x
wrapMode: Text.Wrap wrapMode: Text.Wrap

14
src/accounthash.h Normal file
View file

@ -0,0 +1,14 @@
#ifndef ACCOUNTHASH_H
#define ACCOUNTHASH_H
#include <QObject>
#include <QCryptographicHash>
class AccountHash : public QObject {
Q_OBJECT
public:
Q_INVOKABLE QByteArray hash(const QString username, const QString url) {
return QCryptographicHash::hash(QString("%1@%2").arg(username).arg(url).toUtf8(), QCryptographicHash::Sha256);
}
};
#endif // ACCOUNTHASH_H

View file

@ -2,6 +2,7 @@
#include <sailfishapp.h> #include <sailfishapp.h>
#include <QtQml> #include <QtQml>
#include <QObject> #include <QObject>
#include "accounthash.h"
#include "note.h" #include "note.h"
#include "notesapi.h" #include "notesapi.h"
#include "notesmodel.h" #include "notesmodel.h"
@ -17,6 +18,7 @@ int main(int argc, char *argv[])
qDebug() << app->applicationDisplayName() << app->applicationVersion(); qDebug() << app->applicationDisplayName() << app->applicationVersion();
AccountHash* accountHash = new AccountHash;
qRegisterMetaType<Note>(); qRegisterMetaType<Note>();
NotesModel* notesModel = new NotesModel; NotesModel* notesModel = new NotesModel;
NotesProxyModel* notesProxyModel = new NotesProxyModel; NotesProxyModel* notesProxyModel = new NotesProxyModel;
@ -29,12 +31,15 @@ int main(int argc, char *argv[])
NotesApi* notesApi = new NotesApi; NotesApi* notesApi = new NotesApi;
notesModel->setNotesApi(notesApi); notesModel->setNotesApi(notesApi);
qmlRegisterType<NotesApi>("NextcloudNotes", 1, 0, "NotesApi");
QQuickView* view = SailfishApp::createView(); QQuickView* view = SailfishApp::createView();
#ifdef QT_DEBUG #ifdef QT_DEBUG
view->rootContext()->setContextProperty("debug", QVariant(true)); view->rootContext()->setContextProperty("debug", QVariant(true));
#else #else
view->rootContext()->setContextProperty("debug", QVariant(false)); view->rootContext()->setContextProperty("debug", QVariant(false));
#endif #endif
view->rootContext()->setContextProperty("accountHash", accountHash);
view->rootContext()->setContextProperty("notesModel", notesModel); view->rootContext()->setContextProperty("notesModel", notesModel);
view->rootContext()->setContextProperty("notesProxyModel", notesProxyModel); view->rootContext()->setContextProperty("notesProxyModel", notesProxyModel);
view->rootContext()->setContextProperty("notesApi", notesApi); view->rootContext()->setContextProperty("notesApi", notesApi);

View file

@ -32,7 +32,7 @@ NotesApi::NotesApi(const QString statusEndpoint, const QString loginEndpoint, co
m_request.setSslConfiguration(QSslConfiguration::defaultConfiguration()); m_request.setSslConfiguration(QSslConfiguration::defaultConfiguration());
m_request.setHeader(QNetworkRequest::UserAgentHeader, QGuiApplication::applicationDisplayName() + " " + QGuiApplication::applicationVersion() + " - " + QSysInfo::machineHostName()); m_request.setHeader(QNetworkRequest::UserAgentHeader, QGuiApplication::applicationDisplayName() + " " + QGuiApplication::applicationVersion() + " - " + QSysInfo::machineHostName());
m_request.setHeader(QNetworkRequest::ContentTypeHeader, QString("application/x-www-form-urlencoded").toUtf8()); m_request.setHeader(QNetworkRequest::ContentTypeHeader, QString("application/x-www-form-urlencoded").toUtf8());
m_request.setRawHeader("OCS-APIREQUEST", "true"); m_request.setRawHeader("OCS-APIRequest", "true");
m_request.setRawHeader("Accept", "application/json"); m_request.setRawHeader("Accept", "application/json");
m_authenticatedRequest = m_request; m_authenticatedRequest = m_request;
m_authenticatedRequest.setHeader(QNetworkRequest::ContentTypeHeader, QString("application/json").toUtf8()); m_authenticatedRequest.setHeader(QNetworkRequest::ContentTypeHeader, QString("application/json").toUtf8());
@ -47,102 +47,7 @@ NotesApi::~NotesApi() {
disconnect(&m_manager, SIGNAL(sslErrors(QNetworkReply*,QList<QSslError>)), this, SLOT(sslError(QNetworkReply*,QList<QSslError>))); disconnect(&m_manager, SIGNAL(sslErrors(QNetworkReply*,QList<QSslError>)), this, SLOT(sslError(QNetworkReply*,QList<QSslError>)));
} }
const QList<int> NotesApi::noteIds() { // Generic API properties
return m_syncedNotes.keys();
}
bool NotesApi::noteExists(const int id) {
return m_syncedNotes.contains(id);
}
int NotesApi::noteModified(const int id) {
return m_syncedNotes.value(id, -1);
}
bool NotesApi::getAllNotes(const QStringList& exclude) {
qDebug() << "Getting all notes";
QUrl url = apiEndpointUrl(m_notesEndpoint);
if (!exclude.isEmpty())
url.setQuery(QString(EXCLUDE_QUERY).append(exclude.join(",")));
if (url.isValid() && !url.scheme().isEmpty() && !url.host().isEmpty()) {
qDebug() << "GET" << url.toDisplayString();
m_authenticatedRequest.setUrl(url);
m_getAllNotesReplies << m_manager.get(m_authenticatedRequest);
emit busyChanged(true);
return true;
}
return false;
}
bool NotesApi::getNote(const int id, const QStringList& exclude) {
qDebug() << "Getting note: " << id;
QUrl url = apiEndpointUrl(m_notesEndpoint + QString("/%1").arg(id));
if (!exclude.isEmpty())
url.setQuery(QString(EXCLUDE_QUERY).append(exclude.join(",")));
if (url.isValid() && !url.scheme().isEmpty() && !url.host().isEmpty()) {
qDebug() << "GET" << url.toDisplayString();
m_authenticatedRequest.setUrl(url);
m_getNoteReplies << m_manager.get(m_authenticatedRequest);
emit busyChanged(true);
return true;
}
return false;
}
bool NotesApi::createNote(const QJsonObject& note) {
qDebug() << "Creating note";
QUrl url = apiEndpointUrl(m_notesEndpoint);
if (url.isValid() && !url.scheme().isEmpty() && !url.host().isEmpty()) {
qDebug() << "POST" << url.toDisplayString();
m_authenticatedRequest.setUrl(url);
m_createNoteReplies << m_manager.post(m_authenticatedRequest, QJsonDocument(note).toJson());
emit busyChanged(true);
return true;
}
return false;
}
bool NotesApi::updateNote(const int id, const QJsonObject& note) {
qDebug() << "Updating note: " << id;
QUrl url = apiEndpointUrl(m_notesEndpoint + QString("/%1").arg(id));
if (id >= 0 && url.isValid() && !url.scheme().isEmpty() && !url.host().isEmpty()) {
qDebug() << "PUT" << url.toDisplayString();
m_authenticatedRequest.setUrl(url);
m_updateNoteReplies << m_manager.put(m_authenticatedRequest, QJsonDocument(note).toJson());
emit busyChanged(true);
return true;
}
return false;
}
bool NotesApi::deleteNote(const int id) {
qDebug() << "Deleting note: " << id;
QUrl url = apiEndpointUrl(m_notesEndpoint + QString("/%1").arg(id));
if (url.isValid() && !url.scheme().isEmpty() && !url.host().isEmpty()) {
qDebug() << "DELETE" << url.toDisplayString();
m_authenticatedRequest.setUrl(url);
m_deleteNoteReplies << m_manager.deleteResource(m_authenticatedRequest);
emit busyChanged(true);
return true;
}
return false;
}
bool NotesApi::busy() const {
return !(m_getAllNotesReplies.empty() &&
m_getNoteReplies.empty() &&
m_createNoteReplies.empty() &&
m_updateNoteReplies.empty() &&
m_deleteNoteReplies.empty() &&
m_statusReplies.empty() &&
m_loginReplies.empty() &&
m_pollReplies.empty() &&
m_ocsReplies.empty());
}
void NotesApi::setVerifySsl(bool verify) { void NotesApi::setVerifySsl(bool verify) {
if (verify != (m_request.sslConfiguration().peerVerifyMode() == QSslSocket::VerifyPeer)) { if (verify != (m_request.sslConfiguration().peerVerifyMode() == QSslSocket::VerifyPeer)) {
m_request.sslConfiguration().setPeerVerifyMode(verify ? QSslSocket::VerifyPeer : QSslSocket::VerifyNone); m_request.sslConfiguration().setPeerVerifyMode(verify ? QSslSocket::VerifyPeer : QSslSocket::VerifyNone);
@ -249,6 +154,20 @@ void NotesApi::setPath(QString path) {
} }
} }
// Class status information
bool NotesApi::busy() const {
return !(m_getAllNotesReplies.empty() &&
m_getNoteReplies.empty() &&
m_createNoteReplies.empty() &&
m_updateNoteReplies.empty() &&
m_deleteNoteReplies.empty() &&
m_statusReplies.empty() &&
m_loginReplies.empty() &&
m_pollReplies.empty() &&
m_ocsReplies.empty());
}
// Callable functions
bool NotesApi::getNcStatus() { bool NotesApi::getNcStatus() {
QUrl url = apiEndpointUrl(m_statusEndpoint); QUrl url = apiEndpointUrl(m_statusEndpoint);
qDebug() << "GET" << url.toDisplayString(); qDebug() << "GET" << url.toDisplayString();
@ -323,6 +242,90 @@ void NotesApi::verifyLogin(QString username, QString password) {
} }
} }
const QList<int> NotesApi::noteIds() {
return m_syncedNotes.keys();
}
bool NotesApi::noteExists(const int id) {
return m_syncedNotes.contains(id);
}
int NotesApi::noteModified(const int id) {
return m_syncedNotes.value(id, -1);
}
bool NotesApi::getAllNotes(const QStringList& exclude) {
qDebug() << "Getting all notes";
QUrl url = apiEndpointUrl(m_notesEndpoint);
if (!exclude.isEmpty())
url.setQuery(QString(EXCLUDE_QUERY).append(exclude.join(",")));
if (url.isValid() && !url.scheme().isEmpty() && !url.host().isEmpty()) {
qDebug() << "GET" << url.toDisplayString();
m_authenticatedRequest.setUrl(url);
m_getAllNotesReplies << m_manager.get(m_authenticatedRequest);
emit busyChanged(true);
return true;
}
return false;
}
bool NotesApi::getNote(const int id, const QStringList& exclude) {
qDebug() << "Getting note: " << id;
QUrl url = apiEndpointUrl(m_notesEndpoint + QString("/%1").arg(id));
if (!exclude.isEmpty())
url.setQuery(QString(EXCLUDE_QUERY).append(exclude.join(",")));
if (url.isValid() && !url.scheme().isEmpty() && !url.host().isEmpty()) {
qDebug() << "GET" << url.toDisplayString();
m_authenticatedRequest.setUrl(url);
m_getNoteReplies << m_manager.get(m_authenticatedRequest);
emit busyChanged(true);
return true;
}
return false;
}
bool NotesApi::createNote(const QJsonObject& note) {
qDebug() << "Creating note";
QUrl url = apiEndpointUrl(m_notesEndpoint);
if (url.isValid() && !url.scheme().isEmpty() && !url.host().isEmpty()) {
qDebug() << "POST" << url.toDisplayString();
m_authenticatedRequest.setUrl(url);
m_createNoteReplies << m_manager.post(m_authenticatedRequest, QJsonDocument(note).toJson());
emit busyChanged(true);
return true;
}
return false;
}
bool NotesApi::updateNote(const int id, const QJsonObject& note) {
qDebug() << "Updating note: " << id;
QUrl url = apiEndpointUrl(m_notesEndpoint + QString("/%1").arg(id));
if (id >= 0 && url.isValid() && !url.scheme().isEmpty() && !url.host().isEmpty()) {
qDebug() << "PUT" << url.toDisplayString();
m_authenticatedRequest.setUrl(url);
m_updateNoteReplies << m_manager.put(m_authenticatedRequest, QJsonDocument(note).toJson());
emit busyChanged(true);
return true;
}
return false;
}
bool NotesApi::deleteNote(const int id) {
qDebug() << "Deleting note: " << id;
QUrl url = apiEndpointUrl(m_notesEndpoint + QString("/%1").arg(id));
if (url.isValid() && !url.scheme().isEmpty() && !url.host().isEmpty()) {
qDebug() << "DELETE" << url.toDisplayString();
m_authenticatedRequest.setUrl(url);
m_deleteNoteReplies << m_manager.deleteResource(m_authenticatedRequest);
emit busyChanged(true);
return true;
}
return false;
}
const QString NotesApi::errorMessage(int error) const { const QString NotesApi::errorMessage(int error) const {
QString message; QString message;
switch (error) { switch (error) {
@ -385,7 +388,7 @@ void NotesApi::replyFinished(QNetworkReply *reply) {
QByteArray data = reply->readAll(); QByteArray data = reply->readAll();
//qDebug() << data; //qDebug() << data;
qDebug() << reply->rawHeader("X-Notes-API-Versions"); //qDebug() << reply->rawHeader("X-Notes-API-Versions");
QJsonDocument json = QJsonDocument::fromJson(data); QJsonDocument json = QJsonDocument::fromJson(data);
if (m_getAllNotesReplies.contains(reply)) { if (m_getAllNotesReplies.contains(reply)) {
@ -435,26 +438,28 @@ void NotesApi::replyFinished(QNetworkReply *reply) {
updateLoginFlow(json.object()); updateLoginFlow(json.object());
else { else {
m_loginStatus = LoginStatus::LoginFailed; m_loginStatus = LoginStatus::LoginFailed;
emit loginStatusChanged(m_loginStatus); setLoginStatus(m_loginStatus);
} }
m_loginReplies.removeOne(reply); m_loginReplies.removeOne(reply);
} }
else if (m_pollReplies.contains(reply)) { else if (m_pollReplies.contains(reply)) {
qDebug() << "Poll reply, finished"; qDebug() << "Poll reply";
if (reply->error() == QNetworkReply::NoError && json.isObject()) if (reply->error() == QNetworkReply::NoError && json.isObject()) {
updateLoginCredentials(json.object()); updateLoginCredentials(json.object());
m_pollReplies.removeOne(reply);
}
else if (reply->error() == QNetworkReply::ContentNotFoundError) { else if (reply->error() == QNetworkReply::ContentNotFoundError) {
qDebug() << "Polling not finished yet" << reply->url().toDisplayString(); qDebug() << "Polling not finished yet" << reply->url().toDisplayString();
m_loginStatus = LoginStatus::LoginFlowV2Polling; m_loginStatus = LoginStatus::LoginFlowV2Polling;
emit loginStatusChanged(m_loginStatus); setLoginStatus(m_loginStatus);
} }
else { else {
m_loginStatus = LoginStatus::LoginFailed; m_loginStatus = LoginStatus::LoginFailed;
emit loginStatusChanged(m_loginStatus); setLoginStatus(m_loginStatus);
}
m_pollReplies.removeOne(reply); m_pollReplies.removeOne(reply);
abortFlowV2Login(); abortFlowV2Login();
} }
}
else if (m_statusReplies.contains(reply)) { else if (m_statusReplies.contains(reply)) {
qDebug() << "Status reply"; qDebug() << "Status reply";
if (reply->error() == QNetworkReply::NoError && json.isObject()) if (reply->error() == QNetworkReply::NoError && json.isObject())
@ -658,8 +663,9 @@ bool NotesApi::updateLoginCredentials(const QJsonObject &credentials) {
void NotesApi::setLoginStatus(LoginStatus status, bool *changed) { void NotesApi::setLoginStatus(LoginStatus status, bool *changed) {
if (status != m_loginStatus) { if (status != m_loginStatus) {
if (changed) if (changed) {
*changed = true; *changed = true;
}
m_loginStatus = status; m_loginStatus = status;
emit loginStatusChanged(m_loginStatus); emit loginStatusChanged(m_loginStatus);
} }

View file

@ -70,11 +70,12 @@ public:
QObject *parent = nullptr); QObject *parent = nullptr);
virtual ~NotesApi(); virtual ~NotesApi();
// Status codes
enum CapabilitiesStatus { enum CapabilitiesStatus {
CapabilitiesUnknown, // Initial unknown state CapabilitiesUnknown, // Initial unknown state
CapabilitiesBusy, // Gettin information CapabilitiesBusy, // Gettin information
CapabilitiesSuccess, // Capabilities successfully read CapabilitiesSuccess, // Capabilities successfully read
CapabilitiesStatusFailed // Faild to retreive capabilities CapabilitiesFailed // Faild to retreive capabilities
}; };
Q_ENUM(CapabilitiesStatus) Q_ENUM(CapabilitiesStatus)
@ -82,7 +83,7 @@ public:
NextcloudUnknown, // Initial unknown state NextcloudUnknown, // Initial unknown state
NextcloudBusy, // Getting information from the nextcloud server NextcloudBusy, // Getting information from the nextcloud server
NextcloudSuccess, // Got information about the nextcloud server NextcloudSuccess, // Got information about the nextcloud server
NextcloudFailed // Error getting information from the nextcloud server, see error() NextcloudFailed // Error getting information from the nextcloud server, see ErrorCodes
}; };
Q_ENUM(NextcloudStatus) Q_ENUM(NextcloudStatus)
@ -94,18 +95,17 @@ public:
LoginFlowV2Success, // Finished login flow v2 LoginFlowV2Success, // Finished login flow v2
LoginFlowV2Failed, // An error in login flow v2 LoginFlowV2Failed, // An error in login flow v2
LoginSuccess, // Login has been verified successfull LoginSuccess, // Login has been verified successfull
LoginFailed // Login has failed, see error() LoginFailed // Login has failed, see ErrorCodes
}; };
Q_ENUM(LoginStatus) Q_ENUM(LoginStatus)
// Generic API properties
bool verifySsl() const { return m_authenticatedRequest.sslConfiguration().peerVerifyMode() == QSslSocket::VerifyPeer; } bool verifySsl() const { return m_authenticatedRequest.sslConfiguration().peerVerifyMode() == QSslSocket::VerifyPeer; }
void setVerifySsl(bool verify); void setVerifySsl(bool verify);
QUrl url() const { return m_url; } QUrl url() const { return m_url; }
void setUrl(QUrl url); void setUrl(QUrl url);
bool urlValid() const { return m_url.isValid(); }
QString server() const; QString server() const;
void setServer(QString server); void setServer(QString server);
@ -127,17 +127,19 @@ public:
QString path() const { return m_url.path(); } QString path() const { return m_url.path(); }
void setPath(QString path); void setPath(QString path);
// Class status information
bool urlValid() const { return m_url.isValid(); }
bool networkAccessible() const { return m_manager.networkAccessible() == QNetworkAccessManager::Accessible; } bool networkAccessible() const { return m_manager.networkAccessible() == QNetworkAccessManager::Accessible; }
QDateTime lastSync() const { return m_lastSync; } QDateTime lastSync() const { return m_lastSync; }
bool busy() const; bool busy() const;
// Nextcloud capabilities
CapabilitiesStatus capabilitiesStatus() const { return m_capabilitiesStatus; } CapabilitiesStatus capabilitiesStatus() const { return m_capabilitiesStatus; }
bool notesAppInstalled() const { return m_capabilities_notesInstalled; } bool notesAppInstalled() const { return m_capabilities_notesInstalled; }
QStringList notesAppApiVersions() const { return m_capabilities_notesApiVersions; } QStringList notesAppApiVersions() const { return m_capabilities_notesApiVersions; }
static QString notesAppApiUsedVersion() { return m_capabilities_implementedApiVersion.toString(); } static QString notesAppApiUsedVersion() { return m_capabilities_implementedApiVersion.toString(); }
// Nextcloud status (status.php)
NextcloudStatus ncStatusStatus() const { return m_ncStatusStatus; } NextcloudStatus ncStatusStatus() const { return m_ncStatusStatus; }
bool statusInstalled() const { return m_status_installed; } bool statusInstalled() const { return m_status_installed; }
bool statusMaintenance() const { return m_status_maintenance; } bool statusMaintenance() const { return m_status_maintenance; }
@ -148,9 +150,11 @@ public:
QString statusProductName() const { return m_status_productname; } QString statusProductName() const { return m_status_productname; }
bool statusExtendedSupport() const { return m_status_extendedSupport; } bool statusExtendedSupport() const { return m_status_extendedSupport; }
// Login status
LoginStatus loginStatus() const { return m_loginStatus; } LoginStatus loginStatus() const { return m_loginStatus; }
QUrl loginUrl() const { return m_loginUrl; } QUrl loginUrl() const { return m_loginUrl; }
// Callable functions
Q_INVOKABLE bool getNcStatus(); Q_INVOKABLE bool getNcStatus();
Q_INVOKABLE bool initiateFlowV2Login(); Q_INVOKABLE bool initiateFlowV2Login();
Q_INVOKABLE void abortFlowV2Login(); Q_INVOKABLE void abortFlowV2Login();
@ -174,6 +178,7 @@ public:
int noteModified(const int id); int noteModified(const int id);
public slots: public slots:
// Notes API calls
Q_INVOKABLE bool getAllNotes(const QStringList& exclude = QStringList()); Q_INVOKABLE bool getAllNotes(const QStringList& exclude = QStringList());
Q_INVOKABLE bool getNote(const int id, const QStringList& exclude = QStringList()); Q_INVOKABLE bool getNote(const int id, const QStringList& exclude = QStringList());
Q_INVOKABLE bool createNote(const QJsonObject& note); Q_INVOKABLE bool createNote(const QJsonObject& note);
@ -181,9 +186,9 @@ public slots:
Q_INVOKABLE bool deleteNote(const int id); Q_INVOKABLE bool deleteNote(const int id);
signals: signals:
// Generic API properties
void verifySslChanged(bool verify); void verifySslChanged(bool verify);
void urlChanged(QUrl url); void urlChanged(QUrl url);
void urlValidChanged(bool valid);
void serverChanged(QString server); void serverChanged(QString server);
void schemeChanged(QString scheme); void schemeChanged(QString scheme);
void hostChanged(QString host); void hostChanged(QString host);
@ -191,16 +196,20 @@ signals:
void usernameChanged(QString username); void usernameChanged(QString username);
void passwordChanged(QString password); void passwordChanged(QString password);
void pathChanged(QString path); void pathChanged(QString path);
void dataFileChanged(QString dataFile);
// Class status information
void urlValidChanged(bool valid);
void networkAccessibleChanged(bool accessible); void networkAccessibleChanged(bool accessible);
void lastSyncChanged(QDateTime lastSync); void lastSyncChanged(QDateTime lastSync);
void busyChanged(bool busy); void busyChanged(bool busy);
// Nextcloud capabilities
void capabilitiesStatusChanged(CapabilitiesStatus status); void capabilitiesStatusChanged(CapabilitiesStatus status);
void notesAppInstalledChanged(bool installed); void notesAppInstalledChanged(bool installed);
void notesAppApiVersionsChanged(QStringList versions); void notesAppApiVersionsChanged(QStringList versions);
void notesAppApiUsedVersionChanged(QString version); void notesAppApiUsedVersionChanged(QString version);
// Nextcloud status (status.php)
void ncStatusStatusChanged(NextcloudStatus status); void ncStatusStatusChanged(NextcloudStatus status);
void statusInstalledChanged(bool installed); void statusInstalledChanged(bool installed);
void statusMaintenanceChanged(bool maintenance); void statusMaintenanceChanged(bool maintenance);
@ -211,9 +220,11 @@ signals:
void statusProductNameChanged(QString productName); void statusProductNameChanged(QString productName);
void statusExtendedSupportChanged(bool extendedSupport); void statusExtendedSupportChanged(bool extendedSupport);
// Login status
void loginStatusChanged(LoginStatus status); void loginStatusChanged(LoginStatus status);
void loginUrlChanged(QUrl url); void loginUrlChanged(QUrl url);
// Notes API updates
void noteCreated(int id, const QJsonObject& note); void noteCreated(int id, const QJsonObject& note);
void noteUpdated(int id, const QJsonObject& note); void noteUpdated(int id, const QJsonObject& note);
void noteDeleted(int id); void noteDeleted(int id);

View file

@ -104,7 +104,7 @@ void NotesModel::setNotesApi(NotesApi *notesApi) {
mp_notesApi = notesApi; mp_notesApi = notesApi;
if (mp_notesApi) { if (mp_notesApi) {
// connect stuff // connect stuff
//connect(mp_notesApi, SIGNAL(accountChanged(QString)), this, SIGNAL(accountChanged(QString))); connect(mp_notesApi, SIGNAL(accountChanged(QString)), this, SIGNAL(accountChanged(QString)));
connect(mp_notesApi, SIGNAL(noteCreated(int,QJsonObject)), this, SLOT(insert(int,QJsonObject))); connect(mp_notesApi, SIGNAL(noteCreated(int,QJsonObject)), this, SLOT(insert(int,QJsonObject)));
connect(mp_notesApi, SIGNAL(noteUpdated(int,QJsonObject)), this, SLOT(update(int,QJsonObject))); connect(mp_notesApi, SIGNAL(noteUpdated(int,QJsonObject)), this, SLOT(update(int,QJsonObject)));
connect(mp_notesApi, SIGNAL(noteDeleted(int)), this, SLOT(remove(int))); connect(mp_notesApi, SIGNAL(noteDeleted(int)), this, SLOT(remove(int)));

View file

@ -144,7 +144,7 @@
</message> </message>
<message> <message>
<source>Enable this option to allow selfsigned certificates</source> <source>Enable this option to allow selfsigned certificates</source>
<translation>Auswählen im selbst signierte Zertifikate zu erlauben</translation> <translation>Auswählen um selbst signierte Zertifikate zu erlauben</translation>
</message> </message>
<message> <message>
<source>Allow unencrypted connections</source> <source>Allow unencrypted connections</source>

View file

@ -138,103 +138,103 @@
<context> <context>
<name>LoginPage</name> <name>LoginPage</name>
<message> <message>
<location filename="../qml/pages/LoginPage.qml" line="23"/> <location filename="../qml/pages/LoginPage.qml" line="147"/>
<source>Nextcloud Login</source> <source>Nextcloud Login</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/LoginPage.qml" line="173"/> <location filename="../qml/pages/LoginPage.qml" line="170"/>
<source>Nextcloud server</source> <source>Nextcloud server</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/LoginPage.qml" line="246"/> <location filename="../qml/pages/LoginPage.qml" line="240"/>
<source>Username</source> <source>Username</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/LoginPage.qml" line="257"/> <location filename="../qml/pages/LoginPage.qml" line="252"/>
<source>Password</source> <source>Password</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/LoginPage.qml" line="232"/> <location filename="../qml/pages/LoginPage.qml" line="225"/>
<source>Abort</source> <source>Abort</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/LoginPage.qml" line="100"/> <location filename="../qml/pages/LoginPage.qml" line="86"/>
<source>Follow the instructions in the browser</source> <source>Follow the instructions in the browser</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/LoginPage.qml" line="109"/> <location filename="../qml/pages/LoginPage.qml" line="99"/>
<source>Login successfull!</source> <source>Login successfull!</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/LoginPage.qml" line="106"/> <location filename="../qml/pages/LoginPage.qml" line="95"/>
<location filename="../qml/pages/LoginPage.qml" line="115"/> <location filename="../qml/pages/LoginPage.qml" line="104"/>
<source>Login failed!</source> <source>Login failed!</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/LoginPage.qml" line="95"/> <location filename="../qml/pages/LoginPage.qml" line="78"/>
<source>Enter your credentials</source> <source>Enter your credentials</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/LoginPage.qml" line="214"/> <location filename="../qml/pages/LoginPage.qml" line="207"/>
<source>Enforce legacy login</source> <source>Enforce legacy login</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/LoginPage.qml" line="232"/> <location filename="../qml/pages/LoginPage.qml" line="225"/>
<source>Login</source> <source>Login</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/LoginPage.qml" line="232"/> <location filename="../qml/pages/LoginPage.qml" line="225"/>
<source>Re-Login</source> <source>Re-Login</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/LoginPage.qml" line="266"/> <location filename="../qml/pages/LoginPage.qml" line="261"/>
<source>Test Login</source> <source>Test Login</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/LoginPage.qml" line="272"/> <location filename="../qml/pages/LoginPage.qml" line="267"/>
<source>Note</source> <source>Note</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/LoginPage.qml" line="280"/> <location filename="../qml/pages/LoginPage.qml" line="275"/>
<source>The &lt;a href=&quot;https://apps.nextcloud.com/apps/notes&quot;&gt;Notes&lt;/a&gt; app needs to be installed on the Nextcloud server for this app to work.</source> <source>The &lt;a href=&quot;https://apps.nextcloud.com/apps/notes&quot;&gt;Notes&lt;/a&gt; app needs to be installed on the Nextcloud server for this app to work.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/LoginPage.qml" line="284"/> <location filename="../qml/pages/LoginPage.qml" line="279"/>
<source>Security</source> <source>Security</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/LoginPage.qml" line="291"/> <location filename="../qml/pages/LoginPage.qml" line="286"/>
<source>&lt;strong&gt;CAUTION: Your password will be saved without any encryption on the device!&lt;/strong&gt;&lt;br&gt;Please consider creating a dedicated app password! Open your Nextcloud in a browser and go to &lt;i&gt;Settings&lt;/i&gt; &lt;i&gt;Security&lt;/i&gt;.</source> <source>&lt;strong&gt;CAUTION: Your password will be saved without any encryption on the device!&lt;/strong&gt;&lt;br&gt;Please consider creating a dedicated app password! Open your Nextcloud in a browser and go to &lt;i&gt;Settings&lt;/i&gt; &lt;i&gt;Security&lt;/i&gt;.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/LoginPage.qml" line="295"/> <location filename="../qml/pages/LoginPage.qml" line="291"/>
<source>Do not check certificates</source> <source>Do not check certificates</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/LoginPage.qml" line="296"/> <location filename="../qml/pages/LoginPage.qml" line="292"/>
<source>Enable this option to allow selfsigned certificates</source> <source>Enable this option to allow selfsigned certificates</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/LoginPage.qml" line="305"/> <location filename="../qml/pages/LoginPage.qml" line="301"/>
<source>Allow unencrypted connections</source> <source>Allow unencrypted connections</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -321,32 +321,32 @@
<context> <context>
<name>NotesApi</name> <name>NotesApi</name>
<message> <message>
<location filename="../src/notesapi.cpp" line="330"/> <location filename="../src/notesapi.cpp" line="333"/>
<source>No error</source> <source>No error</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../src/notesapi.cpp" line="333"/> <location filename="../src/notesapi.cpp" line="336"/>
<source>No network connection available</source> <source>No network connection available</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../src/notesapi.cpp" line="336"/> <location filename="../src/notesapi.cpp" line="339"/>
<source>Failed to communicate with the Nextcloud server</source> <source>Failed to communicate with the Nextcloud server</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../src/notesapi.cpp" line="339"/> <location filename="../src/notesapi.cpp" line="342"/>
<source>An error occured while establishing an encrypted connection</source> <source>An error occured while establishing an encrypted connection</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../src/notesapi.cpp" line="342"/> <location filename="../src/notesapi.cpp" line="345"/>
<source>Could not authenticate to the Nextcloud instance</source> <source>Could not authenticate to the Nextcloud instance</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../src/notesapi.cpp" line="345"/> <location filename="../src/notesapi.cpp" line="348"/>
<source>Unknown error</source> <source>Unknown error</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -503,152 +503,152 @@
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/SettingsPage.qml" line="100"/> <location filename="../qml/pages/SettingsPage.qml" line="99"/>
<source>Synchronization</source> <source>Synchronization</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/SettingsPage.qml" line="104"/> <location filename="../qml/pages/SettingsPage.qml" line="103"/>
<source>Auto-Sync</source> <source>Auto-Sync</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/SettingsPage.qml" line="105"/> <location filename="../qml/pages/SettingsPage.qml" line="104"/>
<source>Periodically pull notes from the server</source> <source>Periodically pull notes from the server</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/SettingsPage.qml" line="112"/> <location filename="../qml/pages/SettingsPage.qml" line="111"/>
<source>Disabled</source> <source>Disabled</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/SettingsPage.qml" line="112"/> <location filename="../qml/pages/SettingsPage.qml" line="111"/>
<source>every</source> <source>every</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/SettingsPage.qml" line="114"/> <location filename="../qml/pages/SettingsPage.qml" line="113"/>
<source>Minutes</source> <source>Minutes</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/SettingsPage.qml" line="115"/> <location filename="../qml/pages/SettingsPage.qml" line="114"/>
<source>Seconds</source> <source>Seconds</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/SettingsPage.qml" line="136"/> <location filename="../qml/pages/SettingsPage.qml" line="135"/>
<source>The Answer is 42</source> <source>The Answer is 42</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/SettingsPage.qml" line="137"/> <location filename="../qml/pages/SettingsPage.qml" line="136"/>
<source>Congratulation you found the Answer to the Ultimate Question of Life, The Universe, and Everything!</source> <source>Congratulation you found the Answer to the Ultimate Question of Life, The Universe, and Everything!</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/SettingsPage.qml" line="145"/> <location filename="../qml/pages/SettingsPage.qml" line="144"/>
<source>Appearance</source> <source>Appearance</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/SettingsPage.qml" line="153"/> <location filename="../qml/pages/SettingsPage.qml" line="152"/>
<source>No sorting</source> <source>No sorting</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/SettingsPage.qml" line="177"/> <location filename="../qml/pages/SettingsPage.qml" line="176"/>
<source>Favorites on top</source> <source>Favorites on top</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/SettingsPage.qml" line="178"/> <location filename="../qml/pages/SettingsPage.qml" line="177"/>
<source>Show notes marked as favorite above the others</source> <source>Show notes marked as favorite above the others</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/SettingsPage.qml" line="216"/> <location filename="../qml/pages/SettingsPage.qml" line="215"/>
<source>Reset</source> <source>Reset</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/SettingsPage.qml" line="219"/> <location filename="../qml/pages/SettingsPage.qml" line="218"/>
<source>Reset app settings</source> <source>Reset app settings</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/SettingsPage.qml" line="229"/> <location filename="../qml/pages/SettingsPage.qml" line="228"/>
<source>Resetting the app wipes all application data from the device! This includes offline synced notes, app settings and accounts.</source> <source>Resetting the app wipes all application data from the device! This includes offline synced notes, app settings and accounts.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/SettingsPage.qml" line="150"/> <location filename="../qml/pages/SettingsPage.qml" line="149"/>
<source>Last edited</source> <source>Last edited</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/SettingsPage.qml" line="151"/> <location filename="../qml/pages/SettingsPage.qml" line="150"/>
<source>Category</source> <source>Category</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/SettingsPage.qml" line="152"/> <location filename="../qml/pages/SettingsPage.qml" line="151"/>
<source>Title alphabetically</source> <source>Title alphabetically</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/SettingsPage.qml" line="155"/> <location filename="../qml/pages/SettingsPage.qml" line="154"/>
<source>Sort notes by</source> <source>Sort notes by</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/SettingsPage.qml" line="156"/> <location filename="../qml/pages/SettingsPage.qml" line="155"/>
<source>This will also change how the notes are grouped</source> <source>This will also change how the notes are grouped</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/SettingsPage.qml" line="183"/> <location filename="../qml/pages/SettingsPage.qml" line="182"/>
<source>Show separator</source> <source>Show separator</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/SettingsPage.qml" line="184"/> <location filename="../qml/pages/SettingsPage.qml" line="183"/>
<source>Show a separator line between the notes</source> <source>Show a separator line between the notes</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/SettingsPage.qml" line="194"/> <location filename="../qml/pages/SettingsPage.qml" line="193"/>
<source>lines</source> <source>lines</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/SettingsPage.qml" line="195"/> <location filename="../qml/pages/SettingsPage.qml" line="194"/>
<source>Number of lines in the preview</source> <source>Number of lines in the preview</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/SettingsPage.qml" line="200"/> <location filename="../qml/pages/SettingsPage.qml" line="199"/>
<source>Editing</source> <source>Editing</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/SettingsPage.qml" line="203"/> <location filename="../qml/pages/SettingsPage.qml" line="202"/>
<source>Monospaced font</source> <source>Monospaced font</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/SettingsPage.qml" line="204"/> <location filename="../qml/pages/SettingsPage.qml" line="203"/>
<source>Use a monospeced font to edit a note</source> <source>Use a monospeced font to edit a note</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/SettingsPage.qml" line="209"/> <location filename="../qml/pages/SettingsPage.qml" line="208"/>
<source>Capital &apos;X&apos; in checkboxes</source> <source>Capital &apos;X&apos; in checkboxes</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/pages/SettingsPage.qml" line="210"/> <location filename="../qml/pages/SettingsPage.qml" line="209"/>
<source>For interoperability with other apps such as Joplin</source> <source>For interoperability with other apps such as Joplin</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -860,27 +860,27 @@ You can also use other markdown syntax inside them.</source>
<context> <context>
<name>harbour-nextcloudnotes</name> <name>harbour-nextcloudnotes</name>
<message> <message>
<location filename="../qml/harbour-nextcloudnotes.qml" line="121"/> <location filename="../qml/harbour-nextcloudnotes.qml" line="60"/>
<source>Notes</source> <source>Notes</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/harbour-nextcloudnotes.qml" line="122"/> <location filename="../qml/harbour-nextcloudnotes.qml" line="61"/>
<source>Offline</source> <source>Offline</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/harbour-nextcloudnotes.qml" line="123"/> <location filename="../qml/harbour-nextcloudnotes.qml" line="62"/>
<source>Synced</source> <source>Synced</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/harbour-nextcloudnotes.qml" line="137"/> <location filename="../qml/harbour-nextcloudnotes.qml" line="76"/>
<source>API error</source> <source>API error</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<location filename="../qml/harbour-nextcloudnotes.qml" line="130"/> <location filename="../qml/harbour-nextcloudnotes.qml" line="69"/>
<source>File error</source> <source>File error</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>