diff --git a/qml/harbour-nextcloudnotes.qml b/qml/harbour-nextcloudnotes.qml index 71b821b..7175d6f 100644 --- a/qml/harbour-nextcloudnotes.qml +++ b/qml/harbour-nextcloudnotes.qml @@ -15,7 +15,7 @@ ApplicationWindow property bool initialized: false property var accounts: value("accounts", [], Array) - property int currentAccountIndex: value("currentAccountIndex", -1, Number) + property string currentAccount: value("currentAccount", "", String) property int autoSyncInterval: value("autoSyncInterval", 0, Number) property int previewLineCount: value("previewLineCount", 4, Number) property bool favoritesOnTop: value("favoritesOnTop", true, Boolean) @@ -24,15 +24,8 @@ ApplicationWindow property bool useMonoFont: value("useMonoFont", false, Boolean) property bool useCapitalX: value("useCapitalX", false, Boolean) - onCurrentAccountIndexChanged: { - console.log("Current account index: " + currentAccountIndex) - if (currentAccountIndex >= 0 && currentAccountIndex < accounts.length) { - account = accounts[currentAccountIndex] - console.log("Current account: " + account.username + "@" + account.url) - } - else { - account = null - } + onCurrentAccountChanged: { + console.log("Current account: " + currentAccount) } onSortByChanged: { @@ -45,29 +38,60 @@ ApplicationWindow notesProxyModel.favoritesOnTop = favoritesOnTop } - function createAccount(user, password, url) { - var hash = accountHash.hash(user, url) - console.log("Hash(" + user + "@" + url + ") = " + hash) + function createAccount(username, password, url, name) { + var hash = accountHash.hash(username, url) + var tmpaccounts = accounts + tmpaccounts.push(hash) + accounts = tmpaccounts + + tmpAccount.path = appSettings.path + "/accounts/" + hash + tmpAccount.url = url + tmpAccount.username = username + tmpAccount.passowrd = password + tmpAccount.name = name + + console.log("Hash(" + username + "@" + url + ") = " + hash) return hash } function removeAccount(hash) { - accounts[hash] = null - currentAccount = -1 + notesApi.deleteAppPassword(appSettings.value("accounts/" + hash + "/password"), + appSettings.value("accounts/" + hash + "/username"), + appSettings.value("accounts/" + hash + "/url")) + var tmpaccounts = accounts + tmpaccounts.pop(hash) + accounts = tmpaccounts + + tmpAccount.path = appSettings.path + "/accounts/" + hash + tmpAccount.clear() + currentAccount = accounts[-1] } } - property var account - onAccountChanged: { - if (account) { - notesApi.server = server - notesApi.username = username - notesApi.password = password - } - else { - notesApi.server = "" - notesApi.username = "" - notesApi.password = "" + ConfigurationGroup { + id: account + Connections { + target: appSettings + onCurrentAccountChanged: path = appSettings.path + "/accounts/" + currentAccount } + + property url url: value("url", "", String) + property string username: value("username", "", String) + property string passowrd: value("password", "", String) + property string name: value("name", "", String) + property var update: value("update", new Date(0), Date) + } + + ConfigurationGroup { + id: tmpAccount + property url url + property string username + property string passowrd + property string name + property var update + } + + function clearApp() { + appSettings.clear() } Notification { diff --git a/qml/pages/LoginPage.qml b/qml/pages/LoginPage.qml index 935aff6..8cb4784 100644 --- a/qml/pages/LoginPage.qml +++ b/qml/pages/LoginPage.qml @@ -8,7 +8,8 @@ Dialog { canAccept: false - property int peviousAccountIndex: appSettings.currentAccountIndex + property string account + property string peviousAccount: appSettings.currentAccount property bool legacyLoginPossible: false property bool flowLoginV2Possible: false @@ -17,14 +18,15 @@ Dialog { property bool allowUnecrypted: false Component.onCompleted: { - appSettings.currentAccountIndex = -1 + appSettings.currentAccount = null } onRejected: { - appSettings.currentAccountIndex = peviousAccountIndex + notesApi.abortFlowV2Login() + appSettings.currentAccount = peviousAccount } onAccepted: { - appSettings.createAccount(notesApi.username, notesApi.password, notesApi.server) + appSettings.createAccount(notesApi.username, notesApi.password, notesApi.server, notesApi.statusProductName) } Timer { @@ -93,6 +95,8 @@ Dialog { case NotesApi.LoginSuccess: console.log("LoginSuccess") apiProgressBar.label = qsTr("Login successfull!") + if (legacyLoginPossible || forceLegacyButton.checked) + notesApi.convertToAppPassword(); loginDialog.canAccept = true break; case NotesApi.LoginFailed: @@ -140,7 +144,8 @@ Dialog { label: verifyServerTimer.running ? qsTr("Verifying address") : " " indeterminate: notesApi.loginStatus === NotesApi.LoginFlowV2Initiating || notesApi.loginStatus === NotesApi.LoginFlowV2Polling || - notesApi.ncStatusStatus === notesApi.NextcloudBusy || (verifyServerTimer.running) + notesApi.ncStatusStatus === NotesApi.NextcloudBusy || + verifyServerTimer.running } Row { @@ -199,8 +204,8 @@ Dialog { Behavior on opacity { FadeAnimator {} } Button { anchors.horizontalCenter: parent.horizontalCenter - text: notesApi.loginStatus === notesApi.LoginFlowV2Polling ? qsTr("Abort") : notesApi.loginStatus === notesApi.LoginSuccess ? qsTr("Re-Login") : qsTr("Login") - onClicked: notesApi.loginStatus === notesApi.LoginFlowV2Polling ? notesApi.abortFlowV2Login() : notesApi.initiateFlowV2Login() + text: notesApi.loginStatus === NotesApi.LoginFlowV2Polling ? qsTr("Abort") : notesApi.loginStatus === NotesApi.LoginSuccess ? qsTr("Re-Login") : qsTr("Login") + onClicked: notesApi.loginStatus === NotesApi.LoginFlowV2Polling ? notesApi.abortFlowV2Login() : notesApi.initiateFlowV2Login() } } @@ -239,12 +244,12 @@ Dialog { } EnterKey.enabled: text.length > 0 EnterKey.iconSource: "image://theme/icon-m-enter-accept" - EnterKey.onClicked: notesApi.verifyLogin(usernameField.text, passwordField.text) + EnterKey.onClicked: notesApi.verifyLogin() } Button { anchors.horizontalCenter: parent.horizontalCenter text: qsTr("Test Login") - onClicked: notesApi.verifyLogin(usernameField.text, passwordField.text) + onClicked: notesApi.verifyLogin(passwordField.text, usernameField.text, serverField.text) } } diff --git a/qml/pages/SettingsPage.qml b/qml/pages/SettingsPage.qml index eb31287..e9521be 100644 --- a/qml/pages/SettingsPage.qml +++ b/qml/pages/SettingsPage.qml @@ -52,16 +52,14 @@ Page { ConfigurationGroup { id: account - path: "/apps/harbour-nextcloudnotes/accounts/" + modelData - Component.onCompleted: { - accountTextSwitch.text = account.value("username", qsTr("unknown"), String) + " @ " + value("name", qsTr("Unnamed account"), String) - accountTextSwitch.description =account.value("server", qsTr("unknown"), String) - } + path: appSettings.path + "/accounts/" + modelData } TextSwitch { id: accountTextSwitch automaticCheck: false + text: account.username + description: account.url checked: modelData === appSettings.currentAccount onClicked: { appSettings.currentAccount = modelData @@ -72,14 +70,14 @@ Page { MenuItem { text: qsTr("Edit") onClicked: { - var login = pageStack.push(Qt.resolvedUrl("LoginPage.qml"), { accountId: modelData }) + var login = pageStack.push(Qt.resolvedUrl("LoginPage.qml"), { account: modelData }) } } MenuItem { text: qsTr("Delete") onClicked: { accountListItem.remorseAction(qsTr("Deleting account"), function() { - console.log("Deleting " + modelData) + console.log("Deleting account") appSettings.removeAccount(modelData) }) } @@ -217,12 +215,7 @@ Page { Button { text: qsTr("Reset app settings") anchors.horizontalCenter: parent.horizontalCenter - RemorseItem { id: resetRemorse } - ConfigurationGroup { - id: appConfig - path: "/apps/harbour-nextcloudnotes" - } - onClicked: resetRemorse.execute(this, "Reset app", appConfig.clear()) + onClicked: Remorse.popupAction(page, qsTr("Cleared app data"), function() { clearApp() } ) } LinkedLabel { text: qsTr("Resetting the app wipes all application data from the device! This includes offline synced notes, app settings and accounts.") diff --git a/src/accounthash.h b/src/accounthash.h index 3a19003..79e096d 100644 --- a/src/accounthash.h +++ b/src/accounthash.h @@ -1,5 +1,6 @@ #ifndef ACCOUNTHASH_H #define ACCOUNTHASH_H +#include #include #include @@ -7,7 +8,9 @@ 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); + QByteArray data = QString("%1@%2").arg(username).arg(url).toUtf8(); + QByteArray hash = QCryptographicHash::hash(data, QCryptographicHash::Md5); + return hash.toHex(); } }; diff --git a/src/notesapi.cpp b/src/notesapi.cpp index 0c865a5..60ea3c8 100644 --- a/src/notesapi.cpp +++ b/src/notesapi.cpp @@ -8,8 +8,7 @@ QVersionNumber NotesApi::m_capabilities_implementedApiVersion = QVersionNumber(1, 1); -NotesApi::NotesApi(const QString statusEndpoint, const QString loginEndpoint, const QString ocsEndpoint, const QString notesEndpoint, QObject *parent) - : m_statusEndpoint(statusEndpoint), m_loginEndpoint(loginEndpoint), m_ocsEndpoint(ocsEndpoint), m_notesEndpoint(notesEndpoint) +NotesApi::NotesApi(QObject *parent) { // TODO verify connections (also in destructor) m_loginPollTimer.setInterval(POLL_INTERVALL); @@ -169,7 +168,7 @@ bool NotesApi::busy() const { // Callable functions bool NotesApi::getNcStatus() { - QUrl url = apiEndpointUrl(m_statusEndpoint); + QUrl url = apiEndpointUrl(STATUS_ENDPOINT); qDebug() << "GET" << url.toDisplayString(); if (url.isValid() && !url.scheme().isEmpty() && !url.host().isEmpty()) { setNcStatusStatus(NextcloudStatus::NextcloudBusy); @@ -188,7 +187,7 @@ bool NotesApi::initiateFlowV2Login() { if (m_loginStatus == LoginStatus::LoginFlowV2Initiating || m_loginStatus == LoginStatus::LoginFlowV2Polling) { abortFlowV2Login(); } - QUrl url = apiEndpointUrl(m_loginEndpoint); + QUrl url = apiEndpointUrl(LOGIN_ENDPOINT); qDebug() << "POST" << url.toDisplayString(); if (url.isValid() && !url.scheme().isEmpty() && !url.host().isEmpty()) { setLoginStatus(LoginStatus::LoginFlowV2Initiating); @@ -226,18 +225,57 @@ void NotesApi::pollLoginUrl() { } } -void NotesApi::verifyLogin(QString username, QString password) { - m_ocsRequest = m_authenticatedRequest; - if (username.isEmpty()) - username = this->username(); +void NotesApi::verifyLogin(QString password, QString username, QUrl server) { + QNetworkRequest ocsRequest = m_request; if (password.isEmpty()) password = this->password(); - QUrl url = apiEndpointUrl(m_ocsEndpoint + QString("/users/%1").arg(username)); - m_ocsRequest.setRawHeader("Authorization", "Basic " + QString(username + ":" + password).toLocal8Bit().toBase64()); - if (url.isValid() && !url.scheme().isEmpty() && !url.host().isEmpty()) { - qDebug() << "GET" << url.toDisplayString(); - m_ocsRequest.setUrl(url); - m_ocsReplies << m_manager.get(m_ocsRequest); + if (username.isEmpty()) + username = this->username(); + if (server.isEmpty()) + server = apiEndpointUrl(USERS_ENDPOINT + QString("/%1").arg(username)); + else + server.setPath(USERS_ENDPOINT + QString("/%1").arg(username)); + ocsRequest.setRawHeader("Authorization", "Basic " + QString(username + ":" + password).toLocal8Bit().toBase64()); + if (server.isValid() && !server.scheme().isEmpty() && !server.host().isEmpty()) { + qDebug() << "GET" << server.toDisplayString(); + ocsRequest.setUrl(server); + m_ocsReplies << m_manager.get(ocsRequest); + emit busyChanged(true); + } +} + +void NotesApi::convertToAppPassword(QString password, QString username, QUrl server) { + QNetworkRequest ocsRequest = m_request; + if (password.isEmpty()) + password = this->password(); + if (username.isEmpty()) + username = this->username(); + if (server.isEmpty()) + server = apiEndpointUrl(APPPASSWORD_ENDPOINT + QString("/%1").arg("getapppassword")); + else + server.setPath(APPPASSWORD_ENDPOINT + QString("/%1").arg("getapppassword")); + ocsRequest.setRawHeader("Authorization", "Basic " + QString(this->username() + ":" + password).toLocal8Bit().toBase64()); + if (server.isValid() && !server.scheme().isEmpty() && !server.host().isEmpty()) { + ocsRequest.setUrl(server); + m_ocsReplies << m_manager.get(ocsRequest); + emit busyChanged(true); + } +} + +void NotesApi::deleteAppPassword(QString password, QString username, QUrl server) { + QNetworkRequest ocsRequest = m_request; + if (password.isEmpty()) + password = this->password(); + if (username.isEmpty()) + username = this->username(); + if (server.isEmpty()) + server = apiEndpointUrl(APPPASSWORD_ENDPOINT + QString ("/%1").arg("apppassword")); + else + server.setPath(APPPASSWORD_ENDPOINT + QString ("/%1").arg("apppassword")); + ocsRequest.setRawHeader("Authorization", "Basic " + QString(this->username() + ":" + password).toLocal8Bit().toBase64()); + if (server.isValid() && !server.scheme().isEmpty() && !server.host().isEmpty()) { + ocsRequest.setUrl(server); + m_ocsReplies << m_manager.deleteResource(ocsRequest); emit busyChanged(true); } } @@ -256,7 +294,7 @@ int NotesApi::noteModified(const int id) { bool NotesApi::getAllNotes(const QStringList& exclude) { qDebug() << "Getting all notes"; - QUrl url = apiEndpointUrl(m_notesEndpoint); + QUrl url = apiEndpointUrl(NOTES_ENDPOINT); if (!exclude.isEmpty()) url.setQuery(QString(EXCLUDE_QUERY).append(exclude.join(","))); @@ -273,7 +311,7 @@ bool NotesApi::getAllNotes(const QStringList& exclude) { bool NotesApi::getNote(const int id, const QStringList& exclude) { qDebug() << "Getting note: " << id; - QUrl url = apiEndpointUrl(m_notesEndpoint + QString("/%1").arg(id)); + QUrl url = apiEndpointUrl(NOTES_ENDPOINT + QString("/%1").arg(id)); if (!exclude.isEmpty()) url.setQuery(QString(EXCLUDE_QUERY).append(exclude.join(","))); @@ -289,7 +327,7 @@ bool NotesApi::getNote(const int id, const QStringList& exclude) { bool NotesApi::createNote(const QJsonObject& note) { qDebug() << "Creating note"; - QUrl url = apiEndpointUrl(m_notesEndpoint); + QUrl url = apiEndpointUrl(NOTES_ENDPOINT); if (url.isValid() && !url.scheme().isEmpty() && !url.host().isEmpty()) { qDebug() << "POST" << url.toDisplayString(); m_authenticatedRequest.setUrl(url); @@ -302,7 +340,7 @@ bool NotesApi::createNote(const QJsonObject& note) { bool NotesApi::updateNote(const int id, const QJsonObject& note) { qDebug() << "Updating note: " << id; - QUrl url = apiEndpointUrl(m_notesEndpoint + QString("/%1").arg(id)); + QUrl url = apiEndpointUrl(NOTES_ENDPOINT + QString("/%1").arg(id)); if (id >= 0 && url.isValid() && !url.scheme().isEmpty() && !url.host().isEmpty()) { qDebug() << "PUT" << url.toDisplayString(); m_authenticatedRequest.setUrl(url); @@ -315,7 +353,7 @@ bool NotesApi::updateNote(const int id, const QJsonObject& note) { bool NotesApi::deleteNote(const int id) { qDebug() << "Deleting note: " << id; - QUrl url = apiEndpointUrl(m_notesEndpoint + QString("/%1").arg(id)); + QUrl url = apiEndpointUrl(NOTES_ENDPOINT + QString("/%1").arg(id)); if (url.isValid() && !url.scheme().isEmpty() && !url.host().isEmpty()) { qDebug() << "DELETE" << url.toDisplayString(); m_authenticatedRequest.setUrl(url); @@ -470,7 +508,7 @@ void NotesApi::replyFinished(QNetworkReply *reply) { } else if (m_ocsReplies.contains(reply)) { qDebug() << "OCS reply"; - if (reply->error() == QNetworkReply::NoError && updateCapabilities(json.object())) { + if (reply->error() == QNetworkReply::NoError && updateCore(json.object())) { setLoginStatus(LoginSuccess); qDebug() << "Login Succcessfull!"; } @@ -503,8 +541,8 @@ QUrl NotesApi::apiEndpointUrl(const QString endpoint) const { return url; } -bool NotesApi::updateCapabilities(const QJsonObject &capabilities) { - QJsonValue ocsValue = capabilities.value("ocs"); +bool NotesApi::updateCore(const QJsonObject &ocs) { + QJsonValue ocsValue = ocs.value("ocs"); if (!ocsValue.isUndefined() && ocsValue.isObject()) { QJsonObject ocsObject = ocsValue.toObject(); QJsonValue metaValue = ocsObject.value("meta"); @@ -535,6 +573,10 @@ bool NotesApi::updateCapabilities(const QJsonObject &capabilities) { } } } + QJsonValue appPasswordValue = dataObject.value("apppassword"); + if (!appPasswordValue.isUndefined() && appPasswordValue.isString()) { + setPassword(appPasswordValue.toString()); + } } return true; } diff --git a/src/notesapi.h b/src/notesapi.h index 5a999d1..822abdf 100644 --- a/src/notesapi.h +++ b/src/notesapi.h @@ -13,8 +13,10 @@ const QString STATUS_ENDPOINT("/status.php"); const QString LOGIN_ENDPOINT("/index.php/login/v2"); +const QString USERS_ENDPOINT("/ocs/v1.php/cloud/users"); +const QString CAPABILITIES_ENDPOINT("/ocs/v1.php/cloud/capabilities"); +const QString APPPASSWORD_ENDPOINT("/ocs/v2.php/core/"); const QString NOTES_ENDPOINT("/index.php/apps/notes/api/v0.2/notes"); -const QString OCS_ENDPOINT("/ocs/v1.php/cloud"); const QString EXCLUDE_QUERY("exclude="); const QString PURGE_QUERY("purgeBefore="); const QString ETAG_HEADER("If-None-Match"); @@ -63,11 +65,7 @@ class NotesApi : public QObject Q_PROPERTY(QUrl loginUrl READ loginUrl NOTIFY loginUrlChanged) public: - explicit NotesApi(const QString statusEndpoint = STATUS_ENDPOINT, - const QString loginEndpoint = LOGIN_ENDPOINT, - const QString ocsEndpoint = OCS_ENDPOINT, - const QString notesEndpoint = NOTES_ENDPOINT, - QObject *parent = nullptr); + explicit NotesApi(QObject *parent = nullptr); virtual ~NotesApi(); // Status codes @@ -158,7 +156,9 @@ public: Q_INVOKABLE bool getNcStatus(); Q_INVOKABLE bool initiateFlowV2Login(); Q_INVOKABLE void abortFlowV2Login(); - Q_INVOKABLE void verifyLogin(QString username = QString(), QString password = QString()); + Q_INVOKABLE void verifyLogin(QString password = QString(), QString username = QString(), QUrl server = QUrl()); + Q_INVOKABLE void convertToAppPassword(QString password = QString(), QString username = QString(), QUrl server = QUrl()); + Q_INVOKABLE void deleteAppPassword(QString password = QString(), QString username = QString(), QUrl server = QUrl()); enum ErrorCodes { NoError, @@ -244,10 +244,9 @@ private: QNetworkAccessManager m_manager; QNetworkRequest m_request; QNetworkRequest m_authenticatedRequest; - QNetworkRequest m_ocsRequest; QUrl apiEndpointUrl(const QString endpoint) const; - bool updateCapabilities(const QJsonObject & capabilities); + bool updateCore(const QJsonObject & ocs); CapabilitiesStatus m_capabilitiesStatus; void setCababilitiesStatus(CapabilitiesStatus status, bool *changed = NULL); bool m_capabilities_notesInstalled; @@ -255,7 +254,6 @@ private: QStringList m_capabilities_notesApiVersions; // Nextcloud status.php - const QString m_statusEndpoint; QVector m_statusReplies; void updateNcStatus(const QJsonObject &status); NextcloudStatus m_ncStatusStatus; @@ -270,7 +268,6 @@ private: bool m_status_extendedSupport; // Nextcloud Login Flow v2 - https://docs.nextcloud.com/server/18/developer_manual/client_apis/LoginFlow/index.html#login-flow-v2 - const QString m_loginEndpoint; QVector m_loginReplies; QVector m_pollReplies; bool updateLoginFlow(const QJsonObject &login); @@ -283,11 +280,9 @@ private: QString m_pollToken; // Nextcloud OCS API - https://docs.nextcloud.com/server/18/developer_manual/client_apis/OCS/ocs-api-overview.html - const QString m_ocsEndpoint; QVector m_ocsReplies; // Nextcloud Notes API - https://github.com/nextcloud/notes/wiki/Notes-0.2 - const QString m_notesEndpoint; QVersionNumber m_notesApiVersion; QVector m_getAllNotesReplies; QVector m_getNoteReplies; diff --git a/translations/harbour-nextcloudnotes-de.ts b/translations/harbour-nextcloudnotes-de.ts index 05fd1d4..f968cfa 100644 --- a/translations/harbour-nextcloudnotes-de.ts +++ b/translations/harbour-nextcloudnotes-de.ts @@ -215,6 +215,17 @@ + + LoginWebView + + %1 Login + + + + Nextcloud Login + Nextcloud Login + + MITLicense @@ -400,14 +411,6 @@ No Nextcloud account yet Noch kein Nextcloud Konto eingerichtet - - Unnamed account - Unbenanntes Konto - - - unknown - unbekannt - Edit Bearbeiten @@ -544,6 +547,10 @@ Resetting the app wipes all application data from the device! This includes offline synced notes, app settings and accounts. + + Reset app + + SyntaxPage diff --git a/translations/harbour-nextcloudnotes-sv.ts b/translations/harbour-nextcloudnotes-sv.ts index 3c37cc3..11203df 100644 --- a/translations/harbour-nextcloudnotes-sv.ts +++ b/translations/harbour-nextcloudnotes-sv.ts @@ -215,6 +215,17 @@ + + LoginWebView + + %1 Login + + + + Nextcloud Login + + + MITLicense @@ -408,10 +419,6 @@ Edit Redigera - - Unnamed account - Namnlöst konto - Delete Ta bort @@ -508,10 +515,6 @@ For interoperability with other apps such as Joplin För samverkan med andra program som Joplin - - unknown - okänd - The Answer is 42 Svaret är 42 @@ -544,6 +547,10 @@ Resetting the app wipes all application data from the device! This includes offline synced notes, app settings and accounts. + + Reset app + + SyntaxPage diff --git a/translations/harbour-nextcloudnotes.ts b/translations/harbour-nextcloudnotes.ts index 24eb40a..68e792a 100644 --- a/translations/harbour-nextcloudnotes.ts +++ b/translations/harbour-nextcloudnotes.ts @@ -138,123 +138,123 @@ LoginPage - + Nextcloud Login - + Username - + Password - + Abort - + Follow the instructions in the browser - + Login successfull! - - + + Login failed! - + Enter your credentials - + Nextcloud address - + Verifying address - + Enter Nextcloud address - + Enforce legacy login - + Login - + Re-Login - + Enter Username - + Enter Password - + Test Login - + Note - + The <a href="https://apps.nextcloud.com/apps/notes">Notes</a> app needs to be installed on the Nextcloud server for this app to work. - + Security - + <strong>CAUTION: Your password will be saved without any encryption on the device!</strong><br>Please consider creating a dedicated app password! Open your Nextcloud in a browser and go to <i>Settings</i> → <i>Security</i>. - + Do not check certificates - + Enable this option to allow selfsigned certificates - + Allow unencrypted connections @@ -264,6 +264,19 @@ + + LoginWebView + + + %1 Login + + + + + Nextcloud Login + + + MITLicense @@ -341,32 +354,32 @@ NotesApi - + No error - + No network connection available - + Failed to communicate with the Nextcloud server - + An error occured while establishing an encrypted connection - + Could not authenticate to the Nextcloud instance - + Unknown error @@ -492,183 +505,177 @@ - - Unnamed account - - - - - - unknown - - - - + Edit - + Delete - + Deleting account - + Add account - + Synchronization - + Auto-Sync - + Periodically pull notes from the server - + Disabled - + every - + Minutes - + Seconds - + The Answer is 42 - + Congratulation you found the Answer to the Ultimate Question of Life, The Universe, and Everything! - + Appearance - + No sorting - + Favorites on top - + Show notes marked as favorite above the others - + Reset - + Reset app settings - + + Reset app + + + + Resetting the app wipes all application data from the device! This includes offline synced notes, app settings and accounts. - + Last edited - + Category - + Title alphabetically - + Sort notes by - + This will also change how the notes are grouped - + Show separator - + Show a separator line between the notes - + lines - + Number of lines in the preview - + Editing - + Monospaced font - + Use a monospeced font to edit a note - + Capital 'X' in checkboxes - + For interoperability with other apps such as Joplin @@ -880,27 +887,27 @@ You can also use other markdown syntax inside them. harbour-nextcloudnotes - + Notes - + Offline - + Synced - + API error - + File error