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\\\"
HEADERS += src/note.h \
src/accounthash.h \
src/notesapi.h \
src/notesmodel.h

View file

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

View file

@ -8,43 +8,14 @@ ApplicationWindow
{
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
ConfigurationGroup {
id: appSettings
path: "/apps/harbour-nextcloudnotes/settings"
path: "/apps/harbour-nextcloudnotes"
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 previewLineCount: value("previewLineCount", 4, Number)
property bool favoritesOnTop: value("favoritesOnTop", true, Boolean)
@ -53,9 +24,12 @@ ApplicationWindow
property bool useMonoFont: value("useMonoFont", false, Boolean)
property bool useCapitalX: value("useCapitalX", false, Boolean)
onCurrentAccountChanged: {
account.path = "/apps/harbour-nextcloudnotes/accounts/" + currentAccount
notesModel.account = currentAccount
onCurrentAccountIndexChanged: {
console.log("Current account index: " + currentAccountIndex)
if (currentAccountIndex >= 0 && currentAccountIndex < accounts.length) {
account = accounts[currentAccountIndex]
console.log("Current account: " + account.username + "@" + account.url)
}
}
onSortByChanged: {
@ -68,53 +42,18 @@ ApplicationWindow
notesProxyModel.favoritesOnTop = favoritesOnTop
}
function addAccount() {
var uuid = uuidv4()
var tmpIDs = accounts.value
tmpIDs.push(uuid)
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 createAccount(user, url) {
var hash = accountHash.hash(user, url)
console.log("Hash(" + user + "@" + url + ") = " + hash)
return hash
}
function removeAccount(hash) {
accounts[hash] = null
currentAccount = -1 }
}
property var account
Notification {
id: offlineNotification
expireTimeout: 0

View file

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

View file

@ -6,7 +6,7 @@ Page {
onStatusChanged: {
if (status === PageStatus.Activating) {
if (accounts.value.length <= 0) {
if (appSettings.accounts.length <= 0) {
addAccountHint.restart()
}
else {
@ -32,16 +32,16 @@ Page {
}
MenuItem {
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 } )
}
MenuItem {
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()
}
MenuLabel {
visible: appSettings.currentAccount.length > 0
visible: account != null
text: qsTr("Last update") + ": " + (
new Date(account.update).valueOf() !== 0 ?
new Date(account.update).toLocaleString(Qt.locale(), Locale.ShortFormat) :
@ -224,7 +224,7 @@ Page {
ViewPlaceholder {
id: noLoginPlaceholder
enabled: accounts.value.length <= 0
enabled: appSettings.accounts.length <= 0
text: qsTr("No account yet")
hintText: qsTr("Got to the settings to add an account")
}

View file

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

View file

@ -17,7 +17,7 @@ Dialog {
DialogHeader {
}
Label {
LinkedLabel {
x: Theme.horizontalPageMargin
width: parent.width - 2*x
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 <QtQml>
#include <QObject>
#include "accounthash.h"
#include "note.h"
#include "notesapi.h"
#include "notesmodel.h"
@ -17,6 +18,7 @@ int main(int argc, char *argv[])
qDebug() << app->applicationDisplayName() << app->applicationVersion();
AccountHash* accountHash = new AccountHash;
qRegisterMetaType<Note>();
NotesModel* notesModel = new NotesModel;
NotesProxyModel* notesProxyModel = new NotesProxyModel;
@ -29,12 +31,15 @@ int main(int argc, char *argv[])
NotesApi* notesApi = new NotesApi;
notesModel->setNotesApi(notesApi);
qmlRegisterType<NotesApi>("NextcloudNotes", 1, 0, "NotesApi");
QQuickView* view = SailfishApp::createView();
#ifdef QT_DEBUG
view->rootContext()->setContextProperty("debug", QVariant(true));
#else
view->rootContext()->setContextProperty("debug", QVariant(false));
#endif
view->rootContext()->setContextProperty("accountHash", accountHash);
view->rootContext()->setContextProperty("notesModel", notesModel);
view->rootContext()->setContextProperty("notesProxyModel", notesProxyModel);
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.setHeader(QNetworkRequest::UserAgentHeader, QGuiApplication::applicationDisplayName() + " " + QGuiApplication::applicationVersion() + " - " + QSysInfo::machineHostName());
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_authenticatedRequest = m_request;
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>)));
}
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;
}
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());
}
// Generic API properties
void NotesApi::setVerifySsl(bool verify) {
if (verify != (m_request.sslConfiguration().peerVerifyMode() == QSslSocket::VerifyPeer)) {
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() {
QUrl url = apiEndpointUrl(m_statusEndpoint);
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 {
QString message;
switch (error) {
@ -385,7 +388,7 @@ void NotesApi::replyFinished(QNetworkReply *reply) {
QByteArray data = reply->readAll();
//qDebug() << data;
qDebug() << reply->rawHeader("X-Notes-API-Versions");
//qDebug() << reply->rawHeader("X-Notes-API-Versions");
QJsonDocument json = QJsonDocument::fromJson(data);
if (m_getAllNotesReplies.contains(reply)) {
@ -435,26 +438,28 @@ void NotesApi::replyFinished(QNetworkReply *reply) {
updateLoginFlow(json.object());
else {
m_loginStatus = LoginStatus::LoginFailed;
emit loginStatusChanged(m_loginStatus);
setLoginStatus(m_loginStatus);
}
m_loginReplies.removeOne(reply);
}
else if (m_pollReplies.contains(reply)) {
qDebug() << "Poll reply, finished";
if (reply->error() == QNetworkReply::NoError && json.isObject())
qDebug() << "Poll reply";
if (reply->error() == QNetworkReply::NoError && json.isObject()) {
updateLoginCredentials(json.object());
m_pollReplies.removeOne(reply);
}
else if (reply->error() == QNetworkReply::ContentNotFoundError) {
qDebug() << "Polling not finished yet" << reply->url().toDisplayString();
m_loginStatus = LoginStatus::LoginFlowV2Polling;
emit loginStatusChanged(m_loginStatus);
setLoginStatus(m_loginStatus);
}
else {
m_loginStatus = LoginStatus::LoginFailed;
emit loginStatusChanged(m_loginStatus);
}
setLoginStatus(m_loginStatus);
m_pollReplies.removeOne(reply);
abortFlowV2Login();
}
}
else if (m_statusReplies.contains(reply)) {
qDebug() << "Status reply";
if (reply->error() == QNetworkReply::NoError && json.isObject())
@ -658,8 +663,9 @@ bool NotesApi::updateLoginCredentials(const QJsonObject &credentials) {
void NotesApi::setLoginStatus(LoginStatus status, bool *changed) {
if (status != m_loginStatus) {
if (changed)
if (changed) {
*changed = true;
}
m_loginStatus = status;
emit loginStatusChanged(m_loginStatus);
}

View file

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

View file

@ -104,7 +104,7 @@ void NotesModel::setNotesApi(NotesApi *notesApi) {
mp_notesApi = notesApi;
if (mp_notesApi) {
// 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(noteUpdated(int,QJsonObject)), this, SLOT(update(int,QJsonObject)));
connect(mp_notesApi, SIGNAL(noteDeleted(int)), this, SLOT(remove(int)));

View file

@ -144,7 +144,7 @@
</message>
<message>
<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>
<source>Allow unencrypted connections</source>

View file

@ -138,103 +138,103 @@
<context>
<name>LoginPage</name>
<message>
<location filename="../qml/pages/LoginPage.qml" line="23"/>
<location filename="../qml/pages/LoginPage.qml" line="147"/>
<source>Nextcloud Login</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/LoginPage.qml" line="173"/>
<location filename="../qml/pages/LoginPage.qml" line="170"/>
<source>Nextcloud server</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/LoginPage.qml" line="246"/>
<location filename="../qml/pages/LoginPage.qml" line="240"/>
<source>Username</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/LoginPage.qml" line="257"/>
<location filename="../qml/pages/LoginPage.qml" line="252"/>
<source>Password</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/LoginPage.qml" line="232"/>
<location filename="../qml/pages/LoginPage.qml" line="225"/>
<source>Abort</source>
<translation type="unfinished"></translation>
</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>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/LoginPage.qml" line="109"/>
<location filename="../qml/pages/LoginPage.qml" line="99"/>
<source>Login successfull!</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/LoginPage.qml" line="106"/>
<location filename="../qml/pages/LoginPage.qml" line="115"/>
<location filename="../qml/pages/LoginPage.qml" line="95"/>
<location filename="../qml/pages/LoginPage.qml" line="104"/>
<source>Login failed!</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/LoginPage.qml" line="95"/>
<location filename="../qml/pages/LoginPage.qml" line="78"/>
<source>Enter your credentials</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/LoginPage.qml" line="214"/>
<location filename="../qml/pages/LoginPage.qml" line="207"/>
<source>Enforce legacy login</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/LoginPage.qml" line="232"/>
<location filename="../qml/pages/LoginPage.qml" line="225"/>
<source>Login</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/LoginPage.qml" line="232"/>
<location filename="../qml/pages/LoginPage.qml" line="225"/>
<source>Re-Login</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/LoginPage.qml" line="266"/>
<location filename="../qml/pages/LoginPage.qml" line="261"/>
<source>Test Login</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/LoginPage.qml" line="272"/>
<location filename="../qml/pages/LoginPage.qml" line="267"/>
<source>Note</source>
<translation type="unfinished"></translation>
</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>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/LoginPage.qml" line="284"/>
<location filename="../qml/pages/LoginPage.qml" line="279"/>
<source>Security</source>
<translation type="unfinished"></translation>
</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>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/LoginPage.qml" line="295"/>
<location filename="../qml/pages/LoginPage.qml" line="291"/>
<source>Do not check certificates</source>
<translation type="unfinished"></translation>
</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>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/LoginPage.qml" line="305"/>
<location filename="../qml/pages/LoginPage.qml" line="301"/>
<source>Allow unencrypted connections</source>
<translation type="unfinished"></translation>
</message>
@ -321,32 +321,32 @@
<context>
<name>NotesApi</name>
<message>
<location filename="../src/notesapi.cpp" line="330"/>
<location filename="../src/notesapi.cpp" line="333"/>
<source>No error</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../src/notesapi.cpp" line="333"/>
<location filename="../src/notesapi.cpp" line="336"/>
<source>No network connection available</source>
<translation type="unfinished"></translation>
</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>
<translation type="unfinished"></translation>
</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>
<translation type="unfinished"></translation>
</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>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../src/notesapi.cpp" line="345"/>
<location filename="../src/notesapi.cpp" line="348"/>
<source>Unknown error</source>
<translation type="unfinished"></translation>
</message>
@ -503,152 +503,152 @@
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/SettingsPage.qml" line="100"/>
<location filename="../qml/pages/SettingsPage.qml" line="99"/>
<source>Synchronization</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/SettingsPage.qml" line="104"/>
<location filename="../qml/pages/SettingsPage.qml" line="103"/>
<source>Auto-Sync</source>
<translation type="unfinished"></translation>
</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>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/SettingsPage.qml" line="112"/>
<location filename="../qml/pages/SettingsPage.qml" line="111"/>
<source>Disabled</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/SettingsPage.qml" line="112"/>
<location filename="../qml/pages/SettingsPage.qml" line="111"/>
<source>every</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/SettingsPage.qml" line="114"/>
<location filename="../qml/pages/SettingsPage.qml" line="113"/>
<source>Minutes</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/SettingsPage.qml" line="115"/>
<location filename="../qml/pages/SettingsPage.qml" line="114"/>
<source>Seconds</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/SettingsPage.qml" line="136"/>
<location filename="../qml/pages/SettingsPage.qml" line="135"/>
<source>The Answer is 42</source>
<translation type="unfinished"></translation>
</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>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/SettingsPage.qml" line="145"/>
<location filename="../qml/pages/SettingsPage.qml" line="144"/>
<source>Appearance</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/SettingsPage.qml" line="153"/>
<location filename="../qml/pages/SettingsPage.qml" line="152"/>
<source>No sorting</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/SettingsPage.qml" line="177"/>
<location filename="../qml/pages/SettingsPage.qml" line="176"/>
<source>Favorites on top</source>
<translation type="unfinished"></translation>
</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>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/SettingsPage.qml" line="216"/>
<location filename="../qml/pages/SettingsPage.qml" line="215"/>
<source>Reset</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/SettingsPage.qml" line="219"/>
<location filename="../qml/pages/SettingsPage.qml" line="218"/>
<source>Reset app settings</source>
<translation type="unfinished"></translation>
</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>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/SettingsPage.qml" line="150"/>
<location filename="../qml/pages/SettingsPage.qml" line="149"/>
<source>Last edited</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/SettingsPage.qml" line="151"/>
<location filename="../qml/pages/SettingsPage.qml" line="150"/>
<source>Category</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/SettingsPage.qml" line="152"/>
<location filename="../qml/pages/SettingsPage.qml" line="151"/>
<source>Title alphabetically</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/SettingsPage.qml" line="155"/>
<location filename="../qml/pages/SettingsPage.qml" line="154"/>
<source>Sort notes by</source>
<translation type="unfinished"></translation>
</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>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/SettingsPage.qml" line="183"/>
<location filename="../qml/pages/SettingsPage.qml" line="182"/>
<source>Show separator</source>
<translation type="unfinished"></translation>
</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>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/SettingsPage.qml" line="194"/>
<location filename="../qml/pages/SettingsPage.qml" line="193"/>
<source>lines</source>
<translation type="unfinished"></translation>
</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>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/SettingsPage.qml" line="200"/>
<location filename="../qml/pages/SettingsPage.qml" line="199"/>
<source>Editing</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/SettingsPage.qml" line="203"/>
<location filename="../qml/pages/SettingsPage.qml" line="202"/>
<source>Monospaced font</source>
<translation type="unfinished"></translation>
</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>
<translation type="unfinished"></translation>
</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>
<translation type="unfinished"></translation>
</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>
<translation type="unfinished"></translation>
</message>
@ -860,27 +860,27 @@ You can also use other markdown syntax inside them.</source>
<context>
<name>harbour-nextcloudnotes</name>
<message>
<location filename="../qml/harbour-nextcloudnotes.qml" line="121"/>
<location filename="../qml/harbour-nextcloudnotes.qml" line="60"/>
<source>Notes</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/harbour-nextcloudnotes.qml" line="122"/>
<location filename="../qml/harbour-nextcloudnotes.qml" line="61"/>
<source>Offline</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/harbour-nextcloudnotes.qml" line="123"/>
<location filename="../qml/harbour-nextcloudnotes.qml" line="62"/>
<source>Synced</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/harbour-nextcloudnotes.qml" line="137"/>
<location filename="../qml/harbour-nextcloudnotes.qml" line="76"/>
<source>API error</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/harbour-nextcloudnotes.qml" line="130"/>
<location filename="../qml/harbour-nextcloudnotes.qml" line="69"/>
<source>File error</source>
<translation type="unfinished"></translation>
</message>