Further work on Login workflow

This commit is contained in:
Scharel Clemens 2021-01-05 21:40:53 +01:00
parent af5acbc4a1
commit 4e2a06bcde
9 changed files with 162 additions and 99 deletions

View file

@ -9,6 +9,11 @@ Currently the app can be downloaded from [OpenRepos](https://openrepos.net/conte
You can preview some screenshots [here](https://cloud.scharel.name/apps/gallery/s/harbour-nextcloudnotes#Screenshots). You can preview some screenshots [here](https://cloud.scharel.name/apps/gallery/s/harbour-nextcloudnotes#Screenshots).
## Cloning the repo
If you want to clone the repo add `--recurse-submodules` to the git clone command.
This will also clone the ShowdownJS repo to the corresponding directory.
## Features ## Features
### Implemented ### Implemented

View file

@ -15,7 +15,7 @@ ApplicationWindow
property bool initialized: false property bool initialized: false
property var accounts: value("accounts", [], Array) property var accounts: value("accounts", [], Array)
property string currentAccountIndex: value("currentAccountIndex", -1, Number) property int 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)
@ -30,6 +30,9 @@ ApplicationWindow
account = accounts[currentAccountIndex] account = accounts[currentAccountIndex]
console.log("Current account: " + account.username + "@" + account.url) console.log("Current account: " + account.username + "@" + account.url)
} }
else {
account = null
}
} }
onSortByChanged: { onSortByChanged: {
@ -42,17 +45,30 @@ ApplicationWindow
notesProxyModel.favoritesOnTop = favoritesOnTop notesProxyModel.favoritesOnTop = favoritesOnTop
} }
function createAccount(user, url) { function createAccount(user, password, url) {
var hash = accountHash.hash(user, url) var hash = accountHash.hash(user, url)
console.log("Hash(" + user + "@" + url + ") = " + hash) console.log("Hash(" + user + "@" + url + ") = " + hash)
return hash return hash
} }
function removeAccount(hash) { function removeAccount(hash) {
accounts[hash] = null accounts[hash] = null
currentAccount = -1 } currentAccount = -1
}
} }
property var account property var account
onAccountChanged: {
if (account) {
notesApi.server = server
notesApi.username = username
notesApi.password = password
}
else {
notesApi.server = ""
notesApi.username = ""
notesApi.password = ""
}
}
Notification { Notification {
id: offlineNotification id: offlineNotification
@ -90,6 +106,9 @@ ApplicationWindow
if (interval > 0) { if (interval > 0) {
console.log("Auto-Sync every " + interval / 1000 + " seconds") console.log("Auto-Sync every " + interval / 1000 + " seconds")
} }
else {
console.log("Auto-Sync disabled")
}
} }
} }

View file

@ -8,22 +8,23 @@ Dialog {
canAccept: false canAccept: false
property int peviousAccountIndex: appSettings.currentAccountIndex
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 doNotVerifySsl: false
property bool allowUnecrypted: false property bool allowUnecrypted: false
property string productName Component.onCompleted: {
property string version appSettings.currentAccountIndex = -1
}
onRejected: { onRejected: {
appSettings.currentAccountIndex = peviousAccountIndex
} }
onAccepted: { onAccepted: {
appSettings.createAccount(username, server) appSettings.createAccount(notesApi.username, notesApi.password, notesApi.server)
} }
Timer { Timer {
@ -55,16 +56,13 @@ Dialog {
flowLoginV2Possible = false flowLoginV2Possible = false
} }
} }
onStatusVersionStringChanged: {
if (notesApi.statusVersionString) {
version = notesApi.statusVersionString
console.log(notesApi.statusVersionString)
}
}
onStatusProductNameChanged: { onStatusProductNameChanged: {
if (notesApi.statusProductName) { if (notesApi.statusProductName) {
productName = notesApi.statusProductName productName = notesApi.statusProductName
console.log(notesApi.statusProductName) console.log(productName)
}
else {
productName = null
} }
} }
onLoginStatusChanged: { onLoginStatusChanged: {
@ -111,24 +109,6 @@ Dialog {
Qt.openUrlExternally(notesApi.loginUrl) Qt.openUrlExternally(notesApi.loginUrl)
} }
} }
onServerChanged: {
if (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
}
}
} }
SilicaFlickable { SilicaFlickable {
@ -157,6 +137,10 @@ Dialog {
id: apiProgressBar id: apiProgressBar
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
width: parent.width width: parent.width
label: verifyServerTimer.running ? qsTr("Verifying address") : " "
indeterminate: notesApi.loginStatus === NotesApi.LoginFlowV2Initiating ||
notesApi.loginStatus === NotesApi.LoginFlowV2Polling ||
notesApi.ncStatusStatus === notesApi.NextcloudBusy || (verifyServerTimer.running)
} }
Row { Row {
@ -164,18 +148,18 @@ Dialog {
TextField { TextField {
id: serverField id: serverField
width: parent.width - statusIcon.width - Theme.horizontalPageMargin width: parent.width - statusIcon.width - Theme.horizontalPageMargin
text: server text: notesApi.server
placeholderText: productName ? productName : qsTr("Nextcloud server") placeholderText: qsTr("Enter Nextcloud address")
label: placeholderText label: notesApi.statusProductName ? notesApi.statusProductName : qsTr("Nextcloud address")
validator: RegExpValidator { regExp: unencryptedConnectionTextSwitch.checked ? /^https?:\/\/([-a-zA-Z0-9@:%._\+~#=].*)/: /^https:\/\/([-a-zA-Z0-9@:%._\+~#=].*)/ } validator: RegExpValidator { regExp: allowUnecrypted ? /^https?:\/\/([-a-zA-Z0-9@:%._\+~#=].*)/: /^https:\/\/([-a-zA-Z0-9@:%._\+~#=].*)/ }
inputMethodHints: Qt.ImhUrlCharactersOnly inputMethodHints: Qt.ImhUrlCharactersOnly
onClicked: if (text === "") text = allowUnecrypted ? "http://" : "https://" onClicked: if (text === "") text = allowUnecrypted ? "http://" : "https://"
onTextChanged: { onTextChanged: {
loginDialog.canAccept = false
if (acceptableInput) { if (acceptableInput) {
notesApi.server = text notesApi.server = text
verifyServerTimer.restart()
} }
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"
@ -189,13 +173,8 @@ Dialog {
} }
Icon { Icon {
id: statusIcon id: statusIcon
highlighted: serverField.highlighted source: notesApi.statusInstalled ? "image://theme/icon-m-accept" : "image://theme/icon-m-cancel"
source: notesApi.statusInstalled ? "image://theme/icon-m-acknowledge" : "image://theme/icon-m-question" color: notesApi.statusInstalled ? "green" : Theme.errorColor
BusyIndicator {
anchors.centerIn: parent
size: BusyIndicatorSize.Medium
running: notesApi.ncStatusStatus === notesApi.NextcloudBusy || (verifyServerTimer.running)
}
} }
} }
@ -234,11 +213,15 @@ Dialog {
TextField { TextField {
id: usernameField id: usernameField
width: parent.width width: parent.width
text: username text: notesApi.username
placeholderText: qsTr("Username") placeholderText: qsTr("Enter Username")
label: placeholderText label: qsTr("Username")
inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase
errorHighlight: text.length === 0// && focus === true errorHighlight: text.length === 0// && focus === true
onTextChanged: {
loginDialog.canAccept = false
notesApi.username = text
}
EnterKey.enabled: text.length > 0 EnterKey.enabled: text.length > 0
EnterKey.iconSource: "image://theme/icon-m-enter-next" EnterKey.iconSource: "image://theme/icon-m-enter-next"
EnterKey.onClicked: passwordField.focus = true EnterKey.onClicked: passwordField.focus = true
@ -246,10 +229,14 @@ Dialog {
PasswordField { PasswordField {
id: passwordField id: passwordField
width: parent.width width: parent.width
text: password text: notesApi.password
placeholderText: qsTr("Password") placeholderText: qsTr("Enter Password")
label: placeholderText label: qsTr("Password")
errorHighlight: text.length === 0// && focus === true errorHighlight: text.length === 0// && focus === true
onTextChanged: {
loginDialog.canAccept = false
notesApi.password = text
}
EnterKey.enabled: text.length > 0 EnterKey.enabled: text.length > 0
EnterKey.iconSource: "image://theme/icon-m-enter-accept" EnterKey.iconSource: "image://theme/icon-m-enter-accept"
EnterKey.onClicked: notesApi.verifyLogin(usernameField.text, passwordField.text) EnterKey.onClicked: notesApi.verifyLogin(usernameField.text, passwordField.text)

View file

@ -32,16 +32,16 @@ Page {
} }
MenuItem { MenuItem {
text: qsTr("Add note") text: qsTr("Add note")
enabled: account != null && 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: account != null && notesApi.networkAccessible && !notesApi.busy enabled: account !== null && notesApi.networkAccessible && !notesApi.busy
onClicked: notes.getAllNotes() onClicked: notes.getAllNotes()
} }
MenuLabel { MenuLabel {
visible: account != null 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) :

View file

@ -91,7 +91,7 @@ Page {
text: qsTr("Add account") text: qsTr("Add account")
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
onClicked: { onClicked: {
var login = pageStack.push(Qt.resolvedUrl("LoginPage.qml"), { accountId: "" }) var login = pageStack.push(Qt.resolvedUrl("LoginPage.qml"))
} }
} }

View file

@ -387,7 +387,7 @@ void NotesApi::replyFinished(QNetworkReply *reply) {
emit noteError(CommunicationError); emit noteError(CommunicationError);
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);
@ -470,10 +470,9 @@ void NotesApi::replyFinished(QNetworkReply *reply) {
} }
else if (m_ocsReplies.contains(reply)) { else if (m_ocsReplies.contains(reply)) {
qDebug() << "OCS reply"; qDebug() << "OCS reply";
QString xml(data); if (reply->error() == QNetworkReply::NoError && updateCapabilities(json.object())) {
if (reply->error() == QNetworkReply::NoError && xml.contains("<status>ok</status>")) {
qDebug() << "Login Success!";
setLoginStatus(LoginSuccess); setLoginStatus(LoginSuccess);
qDebug() << "Login Succcessfull!";
} }
else { else {
qDebug() << "Login Failed!"; qDebug() << "Login Failed!";
@ -653,6 +652,7 @@ bool NotesApi::updateLoginCredentials(const QJsonObject &credentials) {
setPassword(appPassword); setPassword(appPassword);
} }
if (!serverAddr.isEmpty() && !loginName.isEmpty() && !appPassword.isEmpty()) { if (!serverAddr.isEmpty() && !loginName.isEmpty() && !appPassword.isEmpty()) {
abortFlowV2Login();
qDebug() << "Login successfull for user" << loginName << "on" << serverAddr; qDebug() << "Login successfull for user" << loginName << "on" << serverAddr;
setLoginStatus(LoginStatus::LoginFlowV2Success); setLoginStatus(LoginStatus::LoginFlowV2Success);
return true; return true;

View file

@ -118,10 +118,6 @@
<source>Nextcloud Login</source> <source>Nextcloud Login</source>
<translation>Nextcloud Login</translation> <translation>Nextcloud Login</translation>
</message> </message>
<message>
<source>Nextcloud server</source>
<translation>Nextcloud Server</translation>
</message>
<message> <message>
<source>Abort</source> <source>Abort</source>
<translation>Abbrechen</translation> <translation>Abbrechen</translation>
@ -198,6 +194,26 @@
<source>Enforce legacy login</source> <source>Enforce legacy login</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Nextcloud address</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Verifying address</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Enter Nextcloud address</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Enter Username</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Enter Password</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>MITLicense</name> <name>MITLicense</name>

View file

@ -118,10 +118,6 @@
<source>Nextcloud Login</source> <source>Nextcloud Login</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Nextcloud server</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<source>Abort</source> <source>Abort</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
@ -198,6 +194,26 @@
<source>Enforce legacy login</source> <source>Enforce legacy login</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Nextcloud address</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Verifying address</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Enter Nextcloud address</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Enter Username</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Enter Password</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>MITLicense</name> <name>MITLicense</name>

View file

@ -138,103 +138,123 @@
<context> <context>
<name>LoginPage</name> <name>LoginPage</name>
<message> <message>
<location filename="../qml/pages/LoginPage.qml" line="147"/> <location filename="../qml/pages/LoginPage.qml" line="125"/>
<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="170"/> <location filename="../qml/pages/LoginPage.qml" line="217"/>
<source>Nextcloud server</source>
<translation type="unfinished"></translation>
</message>
<message>
<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="252"/> <location filename="../qml/pages/LoginPage.qml" line="229"/>
<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="225"/> <location filename="../qml/pages/LoginPage.qml" line="201"/>
<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="86"/> <location filename="../qml/pages/LoginPage.qml" line="82"/>
<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="99"/> <location filename="../qml/pages/LoginPage.qml" line="95"/>
<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="95"/> <location filename="../qml/pages/LoginPage.qml" line="91"/>
<location filename="../qml/pages/LoginPage.qml" line="104"/> <location filename="../qml/pages/LoginPage.qml" line="100"/>
<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="78"/> <location filename="../qml/pages/LoginPage.qml" line="74"/>
<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="207"/> <location filename="../qml/pages/LoginPage.qml" line="153"/>
<source>Nextcloud address</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/LoginPage.qml" line="140"/>
<source>Verifying address</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/LoginPage.qml" line="152"/>
<source>Enter Nextcloud address</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/LoginPage.qml" line="183"/>
<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="225"/> <location filename="../qml/pages/LoginPage.qml" line="201"/>
<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="225"/> <location filename="../qml/pages/LoginPage.qml" line="201"/>
<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="261"/> <location filename="../qml/pages/LoginPage.qml" line="216"/>
<source>Enter Username</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/LoginPage.qml" line="228"/>
<source>Enter Password</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/LoginPage.qml" line="237"/>
<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="267"/> <location filename="../qml/pages/LoginPage.qml" line="243"/>
<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="275"/> <location filename="../qml/pages/LoginPage.qml" line="251"/>
<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="279"/> <location filename="../qml/pages/LoginPage.qml" line="255"/>
<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="286"/> <location filename="../qml/pages/LoginPage.qml" line="262"/>
<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="291"/> <location filename="../qml/pages/LoginPage.qml" line="267"/>
<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="292"/> <location filename="../qml/pages/LoginPage.qml" line="268"/>
<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="301"/> <location filename="../qml/pages/LoginPage.qml" line="277"/>
<source>Allow unencrypted connections</source> <source>Allow unencrypted connections</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
@ -860,27 +880,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="60"/> <location filename="../qml/harbour-nextcloudnotes.qml" line="76"/>
<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="61"/> <location filename="../qml/harbour-nextcloudnotes.qml" line="77"/>
<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="62"/> <location filename="../qml/harbour-nextcloudnotes.qml" line="78"/>
<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="76"/> <location filename="../qml/harbour-nextcloudnotes.qml" line="92"/>
<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="69"/> <location filename="../qml/harbour-nextcloudnotes.qml" line="85"/>
<source>File error</source> <source>File error</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>