diff --git a/harbour-nextcloudnotes.pro b/harbour-nextcloudnotes.pro index a5e79be..7de329e 100644 --- a/harbour-nextcloudnotes.pro +++ b/harbour-nextcloudnotes.pro @@ -17,6 +17,7 @@ CONFIG += sailfishapp DEFINES += APP_VERSION=\\\"$$VERSION\\\" HEADERS += src/note.h \ + src/accounthash.h \ src/notesapi.h \ src/notesmodel.h diff --git a/qml/cover/CoverPage.qml b/qml/cover/CoverPage.qml index e4d0bc3..cf2fb8c 100644 --- a/qml/cover/CoverPage.qml +++ b/qml/cover/CoverPage.qml @@ -12,7 +12,7 @@ CoverBackground { CoverActionList { id: coverAction - enabled: appSettings.currentAccount.length > 0 + enabled: account != null CoverAction { iconSource: "image://theme/icon-cover-new" diff --git a/qml/harbour-nextcloudnotes.qml b/qml/harbour-nextcloudnotes.qml index a670724..0ad005e 100644 --- a/qml/harbour-nextcloudnotes.qml +++ b/qml/harbour-nextcloudnotes.qml @@ -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 diff --git a/qml/pages/LoginPage.qml b/qml/pages/LoginPage.qml index 65ef97b..61e92c6 100644 --- a/qml/pages/LoginPage.qml +++ b/qml/pages/LoginPage.qml @@ -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 }) } } diff --git a/qml/pages/NotesPage.qml b/qml/pages/NotesPage.qml index 8ecda99..d8fcb5f 100644 --- a/qml/pages/NotesPage.qml +++ b/qml/pages/NotesPage.qml @@ -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") } diff --git a/qml/pages/SettingsPage.qml b/qml/pages/SettingsPage.qml index 32de3f7..d9481a8 100644 --- a/qml/pages/SettingsPage.qml +++ b/qml/pages/SettingsPage.qml @@ -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: "" }) } } diff --git a/qml/pages/UnencryptedDialog.qml b/qml/pages/UnencryptedDialog.qml index e21a7cc..20ec87b 100644 --- a/qml/pages/UnencryptedDialog.qml +++ b/qml/pages/UnencryptedDialog.qml @@ -17,7 +17,7 @@ Dialog { DialogHeader { } - Label { + LinkedLabel { x: Theme.horizontalPageMargin width: parent.width - 2*x wrapMode: Text.Wrap diff --git a/src/accounthash.h b/src/accounthash.h new file mode 100644 index 0000000..3a19003 --- /dev/null +++ b/src/accounthash.h @@ -0,0 +1,14 @@ +#ifndef ACCOUNTHASH_H +#define ACCOUNTHASH_H +#include +#include + +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 diff --git a/src/harbour-nextcloudnotes.cpp b/src/harbour-nextcloudnotes.cpp index 00550a0..21499ee 100644 --- a/src/harbour-nextcloudnotes.cpp +++ b/src/harbour-nextcloudnotes.cpp @@ -2,6 +2,7 @@ #include #include #include +#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(); 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("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); diff --git a/src/notesapi.cpp b/src/notesapi.cpp index 1460320..670ce26 100644 --- a/src/notesapi.cpp +++ b/src/notesapi.cpp @@ -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)), this, SLOT(sslError(QNetworkReply*,QList))); } -const QList 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 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,25 +438,27 @@ 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(); } - m_pollReplies.removeOne(reply); - abortFlowV2Login(); } else if (m_statusReplies.contains(reply)) { qDebug() << "Status reply"; @@ -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); } diff --git a/src/notesapi.h b/src/notesapi.h index 1e1d0ab..5a999d1 100644 --- a/src/notesapi.h +++ b/src/notesapi.h @@ -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); diff --git a/src/notesmodel.cpp b/src/notesmodel.cpp index 67bb765..70b97b2 100644 --- a/src/notesmodel.cpp +++ b/src/notesmodel.cpp @@ -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))); diff --git a/translations/harbour-nextcloudnotes-de.ts b/translations/harbour-nextcloudnotes-de.ts index 4618006..97f83c0 100644 --- a/translations/harbour-nextcloudnotes-de.ts +++ b/translations/harbour-nextcloudnotes-de.ts @@ -144,7 +144,7 @@ Enable this option to allow selfsigned certificates - Auswählen im selbst signierte Zertifikate zu erlauben + Auswählen um selbst signierte Zertifikate zu erlauben Allow unencrypted connections diff --git a/translations/harbour-nextcloudnotes.ts b/translations/harbour-nextcloudnotes.ts index c3d22d7..8cf0af8 100644 --- a/translations/harbour-nextcloudnotes.ts +++ b/translations/harbour-nextcloudnotes.ts @@ -138,103 +138,103 @@ LoginPage - + Nextcloud Login - + Nextcloud server - + Username - + Password - + Abort - + Follow the instructions in the browser - + Login successfull! - - + + Login failed! - + Enter your credentials - + Enforce legacy login - + Login - + Re-Login - + 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 @@ -321,32 +321,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 @@ -503,152 +503,152 @@ - + 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 - + 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 @@ -860,27 +860,27 @@ You can also use other markdown syntax inside them. harbour-nextcloudnotes - + Notes - + Offline - + Synced - + API error - + File error