diff --git a/harbour-nextcloudnotes.pro b/harbour-nextcloudnotes.pro index cceaba3..bf302d9 100644 --- a/harbour-nextcloudnotes.pro +++ b/harbour-nextcloudnotes.pro @@ -16,15 +16,19 @@ CONFIG += sailfishapp DEFINES += APP_VERSION=\\\"$$VERSION\\\" -HEADERS += src/note.h \ - src/accounthash.h \ +HEADERS += \ src/nextcloudapi.h \ - src/notesmodel.h + src/accounthash.h \ + src/apps/abstractnextcloudapp.h \ + src/apps/notes/notesapp.h \ + src/apps/notes/notesmodel.h \ + src/apps/notes/note.h SOURCES += src/harbour-nextcloudnotes.cpp \ src/nextcloudapi.cpp \ - src/note.cpp \ - src/notesmodel.cpp + src/apps/notes/notesapp.cpp \ + src/apps/notes/notesmodel.cpp \ + src/apps/notes/note.cpp DISTFILES += qml/harbour-nextcloudnotes.qml \ qml/cover/CoverPage.qml \ diff --git a/src/apps/abstractnextcloudapp.h b/src/apps/abstractnextcloudapp.h new file mode 100644 index 0000000..4aeff82 --- /dev/null +++ b/src/apps/abstractnextcloudapp.h @@ -0,0 +1,70 @@ +#ifndef ABSTRACTNEXTCLOUDAPP_H +#define ABSTRACTNEXTCLOUDAPP_H + +#include +#include +#include "../nextcloudapi.h" + +class AbstractNextcloudApp : public QObject { + Q_OBJECT + + Q_PROPERTY(QString appName READ appName) + Q_PROPERTY(bool installed READ installed NOTIFY installedChanged) + +public: + AbstractNextcloudApp(QObject *parent = nullptr, QString name = QString(), NextcloudApi* api = nullptr) : QObject(parent), m_appName(name), m_api(api) { + connect(this, SIGNAL(capabilitiesChanged), this, SLOT(updateCapabilities)); + connect(m_api, SIGNAL(capabilitiesChanged), this, SLOT(updateApiCapabilities)); + connect(this, SIGNAL(replyReceived), this, SLOT(updateReply)); + connect(m_api, SIGNAL(replyReceived), this, SLOT(updateApiReply)); + } + + virtual ~AbstractNextcloudApp() { + while (!m_replies.empty()) { + QNetworkReply* reply = m_replies.first(); + reply->abort(); + reply->deleteLater(); + m_replies.removeFirst(); + } + } + + const QString appName() const { return m_appName; } + bool installed() const { return !m_capabilities.isEmpty(); } + +public slots: + void updateApiCapabilities(QJsonObject* json) { + QJsonObject capabilities = json->value(appName()).toObject(); + if (m_capabilities != capabilities) { + bool instChanged = m_capabilities.isEmpty() or capabilities.isEmpty(); + m_capabilities = capabilities; + emit capabilitiesChanged(&m_capabilities); + if (instChanged) { + emit installedChanged(!m_capabilities.isEmpty()); + } + } + } + + bool updateApiReply(QNetworkReply* reply) { + if (m_replies.contains(reply)) { + emit replyReceived(reply); + } + return m_replies.removeOne(reply); + } + +protected slots: + virtual void updateCapabilities(QJsonObject* capabilites) = 0; + virtual void updateReply(QNetworkReply* reply) = 0; + +signals: + void installedChanged(bool); + void capabilitiesChanged(QJsonObject* json); + void replyReceived(QNetworkReply* reply); + +protected: + const QString m_appName; + NextcloudApi* m_api; + QJsonObject m_capabilities; + QVector m_replies; +}; + +#endif // ABSTRACTNEXTCLOUDAPP_H diff --git a/src/note.cpp b/src/apps/notes/note.cpp similarity index 100% rename from src/note.cpp rename to src/apps/notes/note.cpp diff --git a/src/note.h b/src/apps/notes/note.h similarity index 100% rename from src/note.h rename to src/apps/notes/note.h diff --git a/src/notesapi.cpp b/src/apps/notes/notesapi.cpp similarity index 100% rename from src/notesapi.cpp rename to src/apps/notes/notesapi.cpp diff --git a/src/notesapi.h b/src/apps/notes/notesapi.h similarity index 99% rename from src/notesapi.h rename to src/apps/notes/notesapi.h index 1e5ce03..60d06f4 100644 --- a/src/notesapi.h +++ b/src/apps/notes/notesapi.h @@ -10,7 +10,7 @@ #include #include #include -#include "nextcloudapi.h" +#include "../../nextcloudapi.h" const QString NOTES_ENDPOINT("/index.php/apps/notes/api/v0.2/notes"); const QString EXCLUDE_QUERY("exclude="); diff --git a/src/apps/notes/notesapp.cpp b/src/apps/notes/notesapp.cpp new file mode 100644 index 0000000..68f8bb0 --- /dev/null +++ b/src/apps/notes/notesapp.cpp @@ -0,0 +1,100 @@ +#include "notesapp.h" + +NotesApp::NotesApp(QObject *parent, QString name, NextcloudApi* api) + : AbstractNextcloudApp(parent, name, api) { + m_notesProxy.setSourceModel(&m_notesModel); +} + + +QVersionNumber NotesApp::serverVersion() const { + return QVersionNumber::fromString(m_capabilities.value("version").toString()); +} + +QList NotesApp::apiVersions() const { + QJsonArray jsonVersions = m_capabilities.value("api_version").toArray(); + QList versions; + QJsonArray::const_iterator i; + for (i = jsonVersions.begin(); i != jsonVersions.end(); ++i) { + versions << QVersionNumber::fromString(i->toString()); + } + return versions; +} + +bool NotesApp::getAllNotes(const QStringList& exclude) { + qDebug() << "Getting all notes"; + QUrlQuery query; + if (!exclude.isEmpty()) + query.addQueryItem(EXCLUDE_QUERY, exclude.join(",")); + return m_api->get(NOTES_APP_ENDPOINT, query); +} + +bool NotesApp::getNote(const int id) { + qDebug() << "Getting note: " << id; + return m_api->get(NOTES_APP_ENDPOINT + QString("/%1").arg(id)); +} + +bool NotesApp::createNote(const QJsonObject& note, bool local) { + qDebug() << "Creating note"; + QJsonValue value = QJsonValue(note); + if (!m_notes.contains(value)) { + m_notes.append(value); + } + if (!local) { + return m_api->post(NOTES_APP_ENDPOINT, QJsonDocument(note).toJson()); + } + return true; +} + +bool NotesApp::updateNote(const int id, const QJsonObject& note, bool local) { + qDebug() << "Updating note: " << id; + bool done = true; + if (!m_notes.contains(QJsonValue(note))) { + done = false; + QJsonArray::iterator i; + for (i = m_notes.begin(); i != m_notes.end() && !done; ++i) { + QJsonObject localNote = i->toObject(); + int localId = localNote.value("id").toInt(-1); + if (localId > 0) { + if (localId == id) { + *i = QJsonValue(note); + done = true; + } + } + else { + if (localNote.value("content") == note.value("content")) { + *i = QJsonValue(note); + done = true; + } + } + } + } + if (!local) { + return m_api->put(NOTES_APP_ENDPOINT + QString("/%1").arg(id), QJsonDocument(note).toJson()); + } + return done; +} + +bool NotesApp::deleteNote(const int id, bool local) { + qDebug() << "Deleting note: " << id; + bool done = false; + QJsonArray::iterator i; + for (i = m_notes.begin(); i != m_notes.end() && !done; ++i) { + QJsonObject localNote = i->toObject(); + if (localNote.value("id").toInt() == id) { + m_notes.erase(i); + done = true; + } + } + if (!local) { + return m_api->del(NOTES_APP_ENDPOINT + QString("/%1").arg(id)); + } + return done; +} + +void NotesApp::updateReply(QNetworkReply* reply) { + if (reply->error() != QNetworkReply::NoError) + qDebug() << reply->error() << reply->errorString(); + + QByteArray data = reply->readAll(); + QJsonDocument json = QJsonDocument::fromJson(data); +} diff --git a/src/apps/notes/notesapp.h b/src/apps/notes/notesapp.h new file mode 100644 index 0000000..166c84b --- /dev/null +++ b/src/apps/notes/notesapp.h @@ -0,0 +1,56 @@ +#ifndef NOTESAPP_H +#define NOTESAPP_H + +#include +#include +#include +#include "../abstractnextcloudapp.h" +#include "notesmodel.h" + +const int NOTES_API_VERSION(1); +const QString NOTES_APP_ENDPOINT(QString("/index.php/apps/notes/api/v%1").arg(NOTES_API_VERSION)); +const QString CAtERGORY_QUERY("category"); +const QString EXCLUDE_QUERY("exclude"); +const QString PURGE_QUERY("purgeBefore"); +const QString ETAG_HEADER("If-None-Match"); + +class NotesApp : public AbstractNextcloudApp { + Q_OBJECT + + Q_PROPERTY(QVersionNumber serverVersion READ serverVersion NOTIFY capabilitiesChanged) + Q_PROPERTY(QList apiVersions READ apiVersions NOTIFY capabilitiesChanged) + +public: + NotesApp(QObject *parent = nullptr, QString name = QString(), NextcloudApi* api = nullptr); + + virtual ~NotesApp() {} + + const QSortFilterProxyModel* model() { return &m_notesProxy; } + + Q_INVOKABLE QVersionNumber serverVersion() const; + Q_INVOKABLE QList apiVersions() const; + +public slots: + Q_INVOKABLE bool getAllNotes(const QStringList& exclude = QStringList()); + Q_INVOKABLE bool getNote(const int id); + Q_INVOKABLE bool createNote(const QJsonObject& note, bool local = false); + Q_INVOKABLE bool updateNote(const int id, const QJsonObject& note, bool local = false); + Q_INVOKABLE bool deleteNote(const int id, bool local = false); + Q_INVOKABLE bool getSettings(); + Q_INVOKABLE bool changeSettings(const QJsonObject& settings); + +protected slots: + virtual void updateReply(QNetworkReply* reply); + +signals: + void capabilitiesChanged(QJsonObject* json); + +private: + NotesModel m_notesModel; + NotesProxyModel m_notesProxy; + + QJsonArray m_notes; + QJsonObject m_settings; +}; + +#endif // NOTESAPP_H diff --git a/src/notesmodel.cpp b/src/apps/notes/notesmodel.cpp similarity index 100% rename from src/notesmodel.cpp rename to src/apps/notes/notesmodel.cpp diff --git a/src/notesmodel.h b/src/apps/notes/notesmodel.h similarity index 98% rename from src/notesmodel.h rename to src/apps/notes/notesmodel.h index b9aee8f..c2c3a5a 100644 --- a/src/notesmodel.h +++ b/src/apps/notes/notesmodel.h @@ -9,7 +9,7 @@ #include #include #include "note.h" -#include "notesapi.h" +//#include "notesapi.h" class NotesProxyModel : public QSortFilterProxyModel { Q_OBJECT @@ -106,7 +106,7 @@ private: QDir m_fileDir; const static QString m_fileSuffix; - NotesApi* mp_notesApi; + //NotesApi* mp_notesApi; }; #endif // NOTESMODEL_H diff --git a/src/harbour-nextcloudnotes.cpp b/src/harbour-nextcloudnotes.cpp index aa372f0..5cfce99 100644 --- a/src/harbour-nextcloudnotes.cpp +++ b/src/harbour-nextcloudnotes.cpp @@ -4,7 +4,7 @@ #include #include "accounthash.h" #include "nextcloudapi.h" -#include "notesmodel.h" +#include "apps/notes/notesmodel.h" int main(int argc, char *argv[]) { diff --git a/src/nextcloudapi.cpp b/src/nextcloudapi.cpp index e03cc93..5f3b6dc 100644 --- a/src/nextcloudapi.cpp +++ b/src/nextcloudapi.cpp @@ -29,7 +29,8 @@ NextcloudApi::NextcloudApi(QObject *parent) : QObject(parent) 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("Accept", "application/json"); + m_request.setRawHeader("Accept-Language", QLocale::system().bcp47Name().toUtf8()); + //m_request.setRawHeader("Accept", "application/json"); m_authenticatedRequest = m_request; m_authenticatedRequest.setHeader(QNetworkRequest::ContentTypeHeader, QString("application/json").toUtf8()); } @@ -178,74 +179,56 @@ const QString NextcloudApi::errorMessage(int error) const { return message; } -bool NextcloudApi::get(const QString& endpoint, bool authenticated) { - QUrl url = server(); - url.setPath(url.path() + endpoint); - qDebug() << "GET" << url.toDisplayString(); +const QNetworkRequest NextcloudApi::prepareRequest(QUrl url, int format, bool authenticated) const { + QNetworkRequest request; if (url.isValid() && !url.scheme().isEmpty() && !url.host().isEmpty()) { - QNetworkRequest request = authenticated ? m_authenticatedRequest : m_request; + request = authenticated ? m_authenticatedRequest : m_request; request.setUrl(url); - m_replies << m_manager.get(request); - return true; + switch (format) { + case ReplyJSON: + request.setRawHeader("Accept", "application/json"); + break; + case ReplyXML: + request.setRawHeader("Accept", "application/json"); + break; + } } - else { - qDebug() << "GET URL not valid" << url.toDisplayString(); - } - return false; + return request; } -bool NextcloudApi::put(const QString& endpoint, const QByteArray& data, bool authenticated) { +QNetworkReply* NextcloudApi::get(const QString& endpoint, const QUrlQuery& query, int format, bool authenticated) { + QUrl url = server(); + url.setPath(url.path() + endpoint); + url.setQuery(query); + qDebug() << "GET" << url.toDisplayString(); + return m_manager.get(prepareRequest(url, format, authenticated)); +} + +QNetworkReply* NextcloudApi::put(const QString& endpoint, const QByteArray& data, int format, bool authenticated) { QUrl url = server(); url.setPath(url.path() + endpoint); qDebug() << "PUT" << url.toDisplayString(); qDebug() << data; - if (url.isValid() && !url.scheme().isEmpty() && !url.host().isEmpty()) { - QNetworkRequest request = authenticated ? m_authenticatedRequest : m_request; - request.setUrl(url); - m_replies << m_manager.put(request, data); - return true; - } - else { - qDebug() << "PUT URL not valid" << url.toDisplayString(); - } - return false; + return m_manager.put(prepareRequest(url, format, authenticated), data); } -bool NextcloudApi::post(const QString& endpoint, const QByteArray& data, bool authenticated) { +QNetworkReply* NextcloudApi::post(const QString& endpoint, const QByteArray& data, int format, bool authenticated) { QUrl url = server(); url.setPath(url.path() + endpoint); qDebug() << "POST" << url.toDisplayString(); qDebug() << data; - if (url.isValid() && !url.scheme().isEmpty() && !url.host().isEmpty()) { - QNetworkRequest request = authenticated ? m_authenticatedRequest : m_request; - request.setUrl(url); - m_replies << m_manager.post(request, data); - return true; - } - else { - qDebug() << "POST URL not valid" << url.toDisplayString(); - } - return false; + return m_manager.post(prepareRequest(url, format, authenticated), data); } -bool NextcloudApi::del(const QString& endpoint, bool authenticated) { +QNetworkReply* NextcloudApi::del(const QString& endpoint, bool authenticated) { QUrl url = server(); url.setPath(url.path() + endpoint); qDebug() << "DEL" << url.toDisplayString(); - if (url.isValid() && !url.scheme().isEmpty() && !url.host().isEmpty()) { - QNetworkRequest request = authenticated ? m_authenticatedRequest : m_request; - request.setUrl(url); - m_replies << m_manager.deleteResource(request); - return true; - } - else { - qDebug() << "DEL URL not valid" << url.toDisplayString(); - } - return false; + return m_manager.deleteResource(prepareRequest(url, authenticated)); } bool NextcloudApi::getStatus() { - if (get(STATUS_ENDPOINT, false)) { + if (get(STATUS_ENDPOINT, QUrlQuery(), ReplyJSON, false)) { setStatusStatus(ApiCallStatus::ApiBusy); return true; } @@ -385,7 +368,6 @@ void NextcloudApi::replyFinished(QNetworkReply* reply) { } else { qDebug() << "GET reply received"; - emit getFinished(reply); break; } m_replies.removeOne(reply); @@ -397,7 +379,6 @@ void NextcloudApi::replyFinished(QNetworkReply* reply) { } else { qDebug() << "PUT reply received"; - emit putFinished(reply); break; } m_replies.removeOne(reply); @@ -414,7 +395,6 @@ void NextcloudApi::replyFinished(QNetworkReply* reply) { } else { qDebug() << "POST reply received"; - emit postFinished(reply); break; } m_replies.removeOne(reply); @@ -426,7 +406,6 @@ void NextcloudApi::replyFinished(QNetworkReply* reply) { } else { qDebug() << "DELETE reply received"; - emit delFinished(reply); break; } m_replies.removeOne(reply); @@ -438,6 +417,7 @@ void NextcloudApi::replyFinished(QNetworkReply* reply) { reply->deleteLater(); break; } + emit apiFinished(reply); break; case QNetworkReply::AuthenticationRequiredError: qDebug() << reply->errorString(); diff --git a/src/nextcloudapi.h b/src/nextcloudapi.h index e5a5075..93e4795 100644 --- a/src/nextcloudapi.h +++ b/src/nextcloudapi.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -89,6 +90,11 @@ public: explicit NextcloudApi(QObject *parent = nullptr); virtual ~NextcloudApi(); + enum ReplyFormat { + ReplyJSON, // The reply should be in JSON format + ReplyXML // The reply should be in XML format + }; + // Status codes enum ApiCallStatus { ApiUnknown, // Initial unknown state @@ -181,10 +187,10 @@ public: public slots: // API helper functions - Q_INVOKABLE bool get(const QString& endpoint, bool authenticated = true); - Q_INVOKABLE bool put(const QString& endpoint, const QByteArray& data, bool authenticated = true); - Q_INVOKABLE bool post(const QString& endpoint, const QByteArray& data, bool authenticated = true); - Q_INVOKABLE bool del(const QString& endpoint, bool authenticated = true); + Q_INVOKABLE QNetworkReply* get(const QString& endpoint, const QUrlQuery& query = QUrlQuery(), int format = ReplyJSON, bool authenticated = true); + Q_INVOKABLE QNetworkReply* put(const QString& endpoint, const QByteArray& data, int format = ReplyJSON, bool authenticated = true); + Q_INVOKABLE QNetworkReply* post(const QString& endpoint, const QByteArray& data, int format = ReplyJSON, bool authenticated = true); + Q_INVOKABLE QNetworkReply* del(const QString& endpoint, bool authenticated = true); // Callable functions Q_INVOKABLE bool getStatus(); @@ -228,10 +234,7 @@ signals: void capabilitiesChanged(QJsonObject* json); // API helper updates - void getFinished(QNetworkReply* reply); - void postFinished(QNetworkReply* reply); - void putFinished(QNetworkReply* reply); - void delFinished(QNetworkReply* reply); + void apiFinished(QNetworkReply* reply); void apiError(ErrorCodes error); private slots: @@ -248,6 +251,7 @@ private: QVector m_replies; QNetworkRequest m_request; QNetworkRequest m_authenticatedRequest; + const QNetworkRequest prepareRequest(QUrl url, int format = ReplyJSON, bool authenticated = true) const; // Nextcloud status.php bool updateStatus(const QJsonObject &json);