Implemented API in C++. Next: I discovered QSortFilterProxyModel :)

This commit is contained in:
Scharel Clemens 2019-11-10 23:04:42 +01:00
parent a026c2ee8d
commit 3f128bcd63
16 changed files with 334 additions and 660 deletions

View file

@ -18,13 +18,11 @@ DEFINES += APP_VERSION=\\\"$$VERSION\\\"
HEADERS += \
src/notesapi.h \
src/sslconfiguration.h \
src/notesmodel.h \
src/note.h
SOURCES += src/harbour-nextcloudnotes.cpp \
src/notesapi.cpp \
src/sslconfiguration.cpp \
src/notesmodel.cpp \
src/note.cpp
@ -46,8 +44,7 @@ DISTFILES += qml/harbour-nextcloudnotes.qml \
qml/pages/NotesApi.qml \
qml/pages/MITLicense.qml \
qml/pages/GPLLicense.qml \
qml/pages/SyntaxPage.qml \
qml/components/NotesApi.qml
qml/pages/SyntaxPage.qml
SAILFISHAPP_ICONS = 86x86 108x108 128x128 172x172

View file

@ -1,175 +0,0 @@
import QtQuick 2.5
import Sailfish.Silica 1.0
import Nemo.Configuration 1.0
Item {
property string response
property var categories: [ ]
property string file: StandardPaths.data + "/" + appSettings.currentAccount + ".json"
property bool saveFile: false
property bool busy: jobsRunning > 0
property int jobsRunning: 0
property int status: 0 //204
property string statusText: "No Content"
onStatusChanged: {
//console.log("Network response: " + statusText + " (" + status + ")")
}
/*function getNote(id) {
var dict
if (id) {
for (var i = 0; i < model.count; i++) {
dict = model.get(i)
if (dict.id === id) {
return dict
}
}
}
}*/
function apiCall(method, data) {
jobsRunning++
var endpoint = account.server + "/index.php/apps/notes/api/" + account.version + "/notes"
if (data) {
if (method === "POST" || method === "PUT") {
console.log("Adding note...")
}
else if (data.id && method === "DELETE") {
console.log("Deleting note...")
}
if (method === "GET" || method === "PUT" || method === "DELETE") {
if (data.id) {
endpoint = endpoint + "/" + data.id
}
}
}
console.log("Calling " + endpoint)
var apiReq = new XMLHttpRequest
apiReq.open(method, endpoint, true)
apiReq.setRequestHeader('User-Agent', 'SailfishOS/harbour-nextcloudnotes')
apiReq.setRequestHeader('OCS-APIRequest', 'true')
apiReq.setRequestHeader("Content-Type", "application/json")
apiReq.setRequestHeader("Authorization", "Basic " + Qt.btoa(account.username + ":" + account.password))
apiReq.withCredentials = true
//apiReq.timeout = 5000
apiReq.onreadystatechange = function() {
if (apiReq.readyState === XMLHttpRequest.DONE) {
statusText = apiReq.statusText
status = apiReq.status
if (apiReq.status === 200) {
response = apiReq.responseText
//console.log(response)
console.log("Network response: " + statusText + " (" + status + ")")
}
else if(apiReq.status === 0) {
statusText = qsTr("Unable to connect")
}
/*
else if (apiReq.status === 304) {
console.log("ETag does not differ!")
}
else if (apiReq.status === 401) {
console.log("Unauthorized!")
}
else if (apiReq.status === 404) {
console.log("Note does not exist!")
}*/
else {
//console.log("Network error: " + apiReq.statusText + " (" + apiReq.status + ")")
}
jobsRunning--
}
}
if (method === "GET") {
apiReq.send()
}
else if (method === "POST" || method === "PUT" || method === "DELETE") {
apiReq.send(JSON.stringify(data))
}
else {
console.log("Unsupported method: " + method)
apiReq.abort()
}
}
function getNotesFromApi() {
apiCall("GET")
}
function getNoteFromApi(id) {
if (id) {
apiCall("GET", { 'id': id } )
}
}
function createNote(data) {
if (data)
apiCall("POST", data)
}
function updateNote(id, data) {
if (id && data) {
data.id = id
apiCall("PUT", data)
}
}
function deleteNote(id) {
if (id)
apiCall("DELETE", { 'id': id } )
}
// source: https://stackoverflow.com/a/14339782
/*function getPrettyDate(date) {
var today = new Date()
today.setHours(0)
today.setMinutes(0)
today.setSeconds(0)
today.setMilliseconds(0)
var compDate = new Date(date*1000)
compDate.setHours(0)
compDate.setMinutes(0)
compDate.setSeconds(0)
compDate.setMilliseconds(0)
if (compDate.getTime() === today.getTime()) {
return qsTr("Today")
} else if ((today.getTime() - compDate.getTime()) === (24 * 60 * 60 * 1000)) {
return qsTr("Yesterday")
} else if ((today.getTime() - compDate.getTime()) <= (7 * 24 * 60 * 60 * 1000)) {
return compDate.toLocaleDateString(Qt.locale(), "dddd")
} else if (today.getFullYear() === compDate.getFullYear()) {
return compDate.toLocaleDateString(Qt.locale(), "MMMM")
} else {
return compDate.toLocaleDateString(Qt.locale(), "MMMM yyyy")
}
}*/
/*Component.onCompleted: {
if (saveFile) {
if (account.name === "") {
saveFile = false
}
else {
busy = true
var fileReq = new XMLHttpRequest
fileReq.open("GET", file)
fileReq.onreadystatechange = function() {
if (fileReq.readyState === XMLHttpRequest.DONE) {
if (fileReq.responseText === "") {
update()
}
else {
console.log("Loaded " + account.name + " from local JSON file")
json = fileReq.responseText
busy = false
}
}
}
fileReq.send()
}
}
}*/
}

View file

@ -2,10 +2,7 @@ import QtQuick 2.0
import Sailfish.Silica 1.0
import Nemo.Configuration 1.0
import harbour.nextcloudnotes.notesapi 1.0
import harbour.nextcloudnotes.notesmodel 1.0
import harbour.nextcloudnotes.sslconfiguration 1.0
import "pages"
//import "components"
ApplicationWindow
{
@ -26,8 +23,8 @@ ApplicationWindow
property string version: value("version", "v0.2", String)
property string username: value("username", "", String)
property string password: account.value("password", "", String)
property bool unsecureConnection: account.value("unsecureConnection", false, Boolean)
property bool unencryptedConnection: account.value("unencryptedConnection", false, Boolean)
property bool doNotVerifySsl: account.value("doNotVerifySsl", false, Boolean)
property bool allowUnecrypted: account.value("allowUnecrypted", false, Boolean)
property date update: value("update", "", Date)
onValuesChanged: console.log("A property of the current account has changed")
onNameChanged: console.log("Account: " + name)
@ -37,6 +34,7 @@ ApplicationWindow
id: appSettings
path: "/apps/harbour-nextcloudnotes/settings"
property bool initialized: false
property string currentAccount: value("currentAccount", "", String)
property var accountIDs: value("accountIDs", [ ], Array)
property int autoSyncInterval: value("autoSyncInterval", 0, Number)
@ -48,10 +46,12 @@ ApplicationWindow
property bool useCapitalX: value("useCapitalX", false, Boolean)
onCurrentAccountChanged: {
account.path = "/apps/harbour-nextcloudnotes/accounts/" + currentAccount
//noteListModel.clear()
//api.getNotesFromApi()
api.getAllNotes();
account.sync()
if (initialized)
notesApi.getAllNotes();
autoSyncTimer.restart()
}
Component.onCompleted: initialized = true
function addAccount() {
var uuid = uuidv4()
@ -88,26 +88,18 @@ ApplicationWindow
}
}
/*SslConfiguration {
id: ssl
checkCert: !account.unsecureConnection
}*/
Timer {
id: autoSyncTimer
interval: appSettings.autoSyncInterval * 1000
repeat: true
running: interval > 0 && appWindow.visible
triggeredOnStart: true
triggeredOnStart: false
onTriggered: {
if (!api.busy) {
//api.getNotesFromApi()
api.getAllNotes();
if (!notesApi.busy) {
notesApi.getAllNotes();
}
else {
triggeredOnStart = false
restart()
triggeredOnStart = true
}
}
onIntervalChanged: {
@ -118,33 +110,16 @@ ApplicationWindow
}
NotesApi {
id: api
/*scheme: "https"
id: notesApi
scheme: account.allowUnecrypted ? "http" : "https"
host: account.server
path: "/index.php/apps/notes/api/" + account.version
username: account.username
password: account.password*/
password: account.password
sslVerify: !account.doNotVerifySsl
Component.onCompleted: getAllNotes()
}
Component.onCompleted: {
api.scheme = "https"
api.host = account.server
api.path = "/index.php/apps/notes/api/" + account.version
api.username = account.username
api.password = account.password
}
/*NotesApi {
id: api
onResponseChanged: noteListModel.applyJSON(response)
}
/*NotesModel {
id: noteListModel
sortBy: appSettisignangs.sortBy
favoritesOnTop: appSettings.favoritesOnTop
}*/
initialPage: Component { NotesPage { } }
cover: Qt.resolvedUrl("cover/CoverPage.qml")
allowedOrientations: defaultAllowedOrientations

View file

@ -1,12 +1,12 @@
import QtQuick 2.0
import Sailfish.Silica 1.0
import harbour.nextcloudnotes.note 1.0
import "../components"
import harbour.nextcloudnotes.notesmodel 1.0
Page {
id: page
property string searchText: ""
property NotesModel notesModel: notesApi.model()
onStatusChanged: {
if (status === PageStatus.Active) {
@ -25,7 +25,7 @@ Page {
spacing: Theme.paddingLarge
PullDownMenu {
busy: api.busy
busy: notesApi.busy
MenuItem {
text: qsTr("Settings")
@ -34,12 +34,12 @@ Page {
MenuItem {
text: qsTr("Add note")
enabled: appSettings.currentAccount.length > 0
onClicked: api.createNote( { 'content': "" } )
onClicked: notesApi.createNote( { 'content': "" } )
}
MenuItem {
text: enabled ? qsTr("Reload") : qsTr("Updating...")
enabled: appSettings.currentAccount.length > 0 && !api.busy
onClicked: api.getNotesFromApi()
enabled: appSettings.currentAccount.length > 0 && !notesApi.busy
onClicked: notesApi.getAllNotes()
}
MenuLabel {
visible: appSettings.currentAccount.length > 0
@ -59,7 +59,7 @@ Page {
placeholderText: account.name.length > 0 ? account.name : qsTr("Nextcloud Notes")
EnterKey.iconSource: "image://theme/icon-m-enter-close"
EnterKey.onClicked: focus = false
onTextChanged: noteListModel.searchText = text
onTextChanged: notesModel.searchText = text
}
Label {
id: description
@ -70,24 +70,25 @@ Page {
anchors.bottomMargin: Theme.paddingMedium
color: Theme.secondaryHighlightColor
font.pixelSize: Theme.fontSizeSmall
text: account.username + "@" + account.server.toString().split("://")[1]
text: account.username + "@" + account.server
}
BusyIndicator {
anchors.verticalCenter: searchField.verticalCenter
anchors.right: parent.right
anchors.rightMargin: Theme.horizontalPageMargin
size: BusyIndicatorSize.Medium
running: api.busy && !busyIndicator.running
running: notesApi.busy && !busyIndicator.running
}
}
currentIndex: -1
model: api.model()
model: notesModel
delegate: BackgroundItem {
id: note
visible: inSearch
contentHeight: titleLabel.height + previewLabel.height + 2*Theme.paddingSmall
height: contentHeight + menu.height
width: parent.width
@ -103,7 +104,7 @@ Page {
}
onClicked: pageStack.push(Qt.resolvedUrl("../pages/NotePage.qml"),
{ note: noteListModel.get(index),
{ //note: noteListModel.get(index),
id: id,
modified: modified,
title: title,
@ -112,8 +113,8 @@ Page {
favorite: favorite,
etag: etag,
error: error,
errorMessage: errorMessage,
date: date
errorMessage: errorMessage
//date: date
})
onPressAndHold: menu.open(note)
@ -131,7 +132,7 @@ Page {
icon.source: (favorite ? "image://theme/icon-m-favorite-selected?" : "image://theme/icon-m-favorite?") +
(note.highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor)
onClicked: {
api.updateNote(id, {'favorite': !favorite} )
notesApi.updateNote(id, {'favorite': !favorite} )
}
}
@ -201,7 +202,7 @@ Page {
text: qsTr("Delete")
onClicked: {
remorse.execute(note, qsTr("Deleting note"), function() {
api.deleteNote(id)
notesApi.deleteNote(id)
})
}
}
@ -219,7 +220,7 @@ Page {
id: busyIndicator
anchors.centerIn: parent
size: BusyIndicatorSize.Large
running: notesList.count === 0 && api.busy
running: notesList.count === 0 && notesApi.busy
}
Label {
id: busyLabel
@ -232,7 +233,7 @@ Page {
horizontalAlignment: Qt.AlignHCenter
text: qsTr("Loading notes...")
}
/*
ViewPlaceholder {
id: noLoginPlaceholder
enabled: appSettings.accountIDs.length <= 0
@ -242,14 +243,14 @@ Page {
ViewPlaceholder {
id: noNotesPlaceholder
enabled: api.status === 204 && !busyIndicator.running && !noLoginPlaceholder.enabled
enabled: notesApi.status === 204 && !busyIndicator.running && !noLoginPlaceholder.enabled
text: qsTr("No notes yet")
hintText: qsTr("Pull down to add a note")
}
ViewPlaceholder {
id: noSearchPlaceholder
enabled: notesList.count === 0 && noteListModel.searchText !== ""
enabled: notesList.count === 0 && notesModel.searchText !== ""
text: qsTr("No result")
hintText: qsTr("Try another query")
}
@ -258,9 +259,9 @@ Page {
id: errorPlaceholder
enabled: notesList.count === 0 && !busyIndicator.running && !noSearchPlaceholder.enabled && !noNotesPlaceholder.enabled && !noLoginPlaceholder.enabled
text: qsTr("An error occurred")
hintText: api.statusText
hintText: notesApi.statusText
}
*/
TouchInteractionHint {
id: addAccountHint
interactionMode: TouchInteraction.Pull

View file

@ -5,7 +5,6 @@
#include "notesapi.h"
#include "note.h"
#include "notesmodel.h"
#include "sslconfiguration.h"
int main(int argc, char *argv[])
{
@ -20,7 +19,6 @@ int main(int argc, char *argv[])
qmlRegisterType<NotesApi>("harbour.nextcloudnotes.notesapi", 1, 0, "NotesApi");
qmlRegisterType<Note>("harbour.nextcloudnotes.note", 1, 0, "Note");
qmlRegisterType<NotesModel>("harbour.nextcloudnotes.notesmodel", 1, 0, "NotesModel");
qmlRegisterType<SslConfiguration>("harbour.nextcloudnotes.sslconfiguration", 1, 0, "SslConfiguration");
QQuickView* view = SailfishApp::createView();

View file

@ -42,7 +42,7 @@ Note& Note::operator=(const Note& note) {
}
bool Note::operator==(const Note& note) const {
return same(note);
return equal(note);
}
bool Note::same(const Note& note) const {
@ -118,3 +118,37 @@ bool Note::searchInNote(const QString &query, const Note &note, SearchAttributes
}
return queryFound;
}
bool Note::lessThanByDate(const Note &n1, const Note &n2) {
return n1.modified() > n2.modified();
}
bool Note::lessThanByCategory(const Note &n1, const Note &n2) {
return n1.category() > n2.category();
}
bool Note::lessThanByTitle(const Note &n1, const Note &n2) {
return n1.title() > n2.title();
}
bool Note::lessThanByDateFavOnTop(const Note &n1, const Note &n2) {
if (n1.favorite() != n2.favorite())
return n1.favorite();
else
return n1.modified() > n2.modified();
}
bool Note::lessThanByCategoryFavOnTop(const Note &n1, const Note &n2) {
if (n1.favorite() != n2.favorite())
return n1.favorite();
else
return n1.category() > n2.category();
}
bool Note::lessThanByTitleFavOnTop(const Note &n1, const Note &n2) {
if (n1.favorite() != n2.favorite())
return n1.favorite();
else
return n1.title() > n2.title();
}

View file

@ -64,6 +64,12 @@ public:
static Note fromjson(const QJsonObject& jobj);
static bool searchInNote(const QString &query, const Note &note, SearchAttributes criteria = QFlag(SearchAll), Qt::CaseSensitivity cs = Qt::CaseInsensitive);
static bool lessThanByDate(const Note &n1, const Note &n2);
static bool lessThanByCategory(const Note &n1, const Note &n2);
static bool lessThanByTitle(const Note &n1, const Note &n2);
static bool lessThanByDateFavOnTop(const Note &n1, const Note &n2);
static bool lessThanByCategoryFavOnTop(const Note &n1, const Note &n2);
static bool lessThanByTitleFavOnTop(const Note &n1, const Note &n2);
signals:
void idChanged(int id);

View file

@ -147,24 +147,24 @@ void NotesApi::getNote(int noteId, QStringList excludeFields) {
}
}
void NotesApi::createNote(QVariantHash fields) {
void NotesApi::createNote(QVariantMap fields) {
QUrl url = m_url;
url.setPath(url.path() + "/notes");
if (url.isValid()) {
qDebug() << "POST" << url.toDisplayString();
m_request.setUrl(url);
m_replies << m_manager.post(m_request, QJsonDocument(QJsonObject::fromVariantHash(fields)).toJson());
m_replies << m_manager.post(m_request, QJsonDocument(QJsonObject::fromVariantMap(fields)).toJson());
emit busyChanged(busy());
}
}
void NotesApi::updateNote(int noteId, QVariantHash fields) {
void NotesApi::updateNote(int noteId, QVariantMap fields) {
QUrl url = m_url;
url.setPath(url.path() + QString("/notes/%1").arg(noteId));
if (url.isValid()) {
qDebug() << "PUT" << url.toDisplayString();
m_request.setUrl(url);
m_replies << m_manager.put(m_request, QJsonDocument(QJsonObject::fromVariantHash(fields)).toJson());
m_replies << m_manager.put(m_request, QJsonDocument(QJsonObject::fromVariantMap(fields)).toJson());
emit busyChanged(busy());
}
}
@ -197,7 +197,7 @@ void NotesApi::replyFinished(QNetworkReply *reply) {
//qDebug() << json;
}
else {
qDebug() << reply->errorString();
qDebug() << reply->error() << reply->errorString();
}
m_replies.removeAll(reply);
reply->deleteLater();

View file

@ -5,6 +5,7 @@
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QSortFilterProxyModel>
#include <QDebug>
#include "notesmodel.h"
@ -58,10 +59,10 @@ public:
Q_INVOKABLE void getAllNotes(QStringList excludeFields = QStringList());
Q_INVOKABLE void getNote(int noteId, QStringList excludeFields = QStringList());
Q_INVOKABLE void createNote(QVariantHash fields = QVariantHash());
Q_INVOKABLE void updateNote(int noteId, QVariantHash fields = QVariantHash());
Q_INVOKABLE void createNote(QVariantMap fields = QVariantMap());
Q_INVOKABLE void updateNote(int noteId, QVariantMap fields = QVariantMap());
Q_INVOKABLE void deleteNote(int noteId);
Q_INVOKABLE NotesModel& model() const { return *mp_model; }
Q_INVOKABLE NotesModel* model() const { return mp_model; }
signals:
void sslVerifyChanged(bool verify);
@ -91,6 +92,7 @@ private:
QNetworkRequest m_request;
QVector<QNetworkReply*> m_replies;
NotesModel* mp_model;
QSortFilterProxyModel* mp_modelProxy; // TODO: use!
};
#endif // NOTESAPI_H

View file

@ -1,4 +1,5 @@
#include "notesmodel.h"
#include <algorithm> // std::sort
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
@ -8,11 +9,11 @@
NotesModel::NotesModel(QObject *parent) : QAbstractListModel(parent)
{
m_sortBy = noSorting;
m_favoritesOnTop = false;
m_favoritesOnTop = true;
}
NotesModel::~NotesModel() {
clear();
//clear();
}
void NotesModel::setSortBy(QString sortBy) {
@ -37,15 +38,24 @@ void NotesModel::setSearchText(QString searchText) {
qDebug() << "Searching by:" << searchText;
if (searchText != m_searchText) {
m_searchText = searchText;
for (int i = 0; i < m_notes.size(); i++) {
emit searchTextChanged(m_searchText);
if (m_searchText.isEmpty()) {
m_notes[i].param = true;
m_invisibleIds.clear();
emit dataChanged(this->index(0), this->index(m_notes.size()));
}
else {
m_notes[i].param = Note::searchInNote(m_searchText, m_notes[i].note);
for (int i = 0; i < m_notes.size(); i++) {
if (Note::searchInNote(m_searchText, m_notes[i])) {
//qDebug() << "Note" << m_notes[i].title() << "in search";
m_invisibleIds.removeAll(m_notes[i].id());
}
else {
//qDebug() << "Note" << m_notes[i].title() << "not in search";
m_invisibleIds.append(m_notes[i].id());
}
emit dataChanged(this->index(i), this->index(i));
}
}
emit searchTextChanged(m_searchText);
}
}
@ -57,54 +67,46 @@ void NotesModel::clearSearch() {
search();
}
bool NotesModel::applyJSONobject(const QJsonObject &jobj) {
if (!jobj.isEmpty()) {
Note note = Note::fromjson(jobj); // TODO connect signals
if (!note.error()) {
int position = indexOf(note.id());
Note oldNote = get(position);
if (position >= 0 && note.etag() != oldNote.etag()) {
qDebug() << "-- Existing note " << note.title() << "changed, updating the model.";
replaceNote(note);
}
else if (position < 0) {
qDebug() << "-- New note" << note.title() << ", adding it to the model.";
insertNote(note);
}
else {
qDebug() << "-- Existing note " << note.title() << "unchanged, nothing to do.";
}
}
else {
qDebug() << "Note contains an error:" << note.errorMessage();
}
}
else {
qDebug() << "Unknown JSON object. This message should never occure!";
return false;
}
return true;
}
bool NotesModel::applyJSON(const QJsonDocument &jdoc) {
qDebug() << "Applying new JSON input";// << json;
if (!jdoc.isNull()) {
if (jdoc.isArray()) {
qDebug() << "- It's an array...";
QVector<Note> newNotes;
QJsonArray jarr = jdoc.array();
while (!jarr.empty()) {
//qDebug() << jarr.count() << "JSON Objects to handle...";
QJsonValue jval = jarr.first();
if (jval.isObject()) {
//qDebug() << "It's an object, all fine...";
applyJSONobject(jval.toObject());
QJsonObject jobj = jval.toObject();
if (!jobj.isEmpty()) {
newNotes.append(Note::fromjson(jobj));
}
}
jarr.pop_front();
}
for (int i = 0; i < m_notes.size(); ++i) {
bool noteToBeRemoved = true;
for (int j = 0; j < newNotes.size(); ++j) {
if (m_notes[i].id() == newNotes[j].id())
noteToBeRemoved = false;
}
if (noteToBeRemoved) {
qDebug() << "-- Removing note " << m_notes[i].title();
removeNote(m_notes[i]);
}
}
while (!newNotes.empty()) {
insertNote(newNotes.first());
newNotes.pop_front();
}
return true;
}
else if (jdoc.isObject()) {
qDebug() << "- It's a single object...";
return applyJSONobject(jdoc.object());
insertNote(Note::fromjson(jdoc.object()));
return true;
}
else {
qDebug() << "Unknown JSON document. This message should never occure!";
@ -127,22 +129,29 @@ bool NotesModel::applyJSON(const QString &json) {
}
int NotesModel::insertNote(const Note &note) {
int position = insertPosition(note);
ModelNote<Note, bool> modelNote;
modelNote.note = note;
modelNote.param = true;
int position = indexOf(note.id());
if (position >= 0) {
if (note.etag() != m_notes[position].etag()) {
qDebug() << "-- Existing note " << note.title() << "changed, updating the model.";
m_notes.replace(position, note);
emit dataChanged(this->index(position), this->index(position));
}
else {
qDebug() << "-- Existing note " << note.title() << "unchanged, nothing to do.";
}
}
else {
qDebug() << "-- New note" << note.title() << ", adding it to the model.";
position = insertPosition(note);
beginInsertRows(QModelIndex(), position, position);
m_notes.insert(position, modelNote);
m_notes.insert(position, note);
endInsertRows();
}
return position;
}
bool NotesModel::removeNote(const Note &note) {
return removeNote(note.id());
}
bool NotesModel::removeNote(int id) {
int position = indexOf(id);
int position = m_notes.indexOf(note);
if (position >= 0 && position < m_notes.size()) {
beginRemoveRows(QModelIndex(), position, position);
m_notes.removeAt(position);
@ -152,61 +161,19 @@ bool NotesModel::removeNote(int id) {
return false;
}
bool NotesModel::replaceNote(const Note &note) {
int position = indexOf(note.id());
if (position >= 0 && position < m_notes.size()) {
ModelNote<Note, bool> modelNote;
modelNote.note = note;
modelNote.param = m_notes[position].param;
m_notes.replace(position, modelNote);
QVector<int> roles;
roles << ModifiedRole << TitleRole << CategoryRole << ContentRole << FavoriteRole << EtagRole;
emit dataChanged(this->index(position), this->index(position), roles);
return true;
bool NotesModel::removeNote(int id) {
bool retval = false;
for (int i = 0; i < m_notes.size(); ++i) {
if (m_notes[i].id() == id) {
retval |= removeNote(m_notes[i]);
if (i > 0) i--;
}
return false;
}
void NotesModel::clear() {
m_searchText.clear();
beginRemoveRows(QModelIndex(), 0, rowCount());
m_notes.clear();
endRemoveRows();
}
int NotesModel::indexOf(int id) const {
for (int i = 0; i < m_notes.size(); i++) {
if (m_notes[i].note.id() == id)
return i;
}
return -1;
return retval;
}
Note NotesModel::get(int index) const {
Note note;
if (index >= 0 && index < m_notes.size()) {
note = m_notes[index].note;
}
return note;
}
/*
bool NotesModel::addNote(Note &note) {
m_notes.append(note);
return false;
}
bool NotesModel::addNotes(QList<Note> &notes) {
for (int i = 0; i < notes.length(); i++) {
addNote(notes[i]);
}
return false;
}
*/
QHash<int, QByteArray> NotesModel::roleNames() const {
return QHash<int, QByteArray> {
{NotesModel::VisibleRole, "visible"},
{NotesModel::IdRole, "id"},
{NotesModel::ModifiedRole, "modified"},
{NotesModel::TitleRole, "title"},
@ -216,17 +183,17 @@ QHash<int, QByteArray> NotesModel::roleNames() const {
{NotesModel::EtagRole, "etag"},
{NotesModel::ErrorRole, "error"},
{NotesModel::ErrorMessageRole, "errorMessage"},
{NotesModel::DateStringRole, "date"}
{NotesModel::InSearchRole, "inSearch"}
};
}
QHash<int, QByteArray> NotesModel::sortingNames() const {
QHash<int, QByteArray> criteria;
criteria[sortByDate] = "date";
criteria[sortByCategory] = "category";
criteria[sortByTitle] = "title";
criteria[noSorting] = "none";
return criteria;
return QHash<int, QByteArray> {
{NotesModel::sortByDate, "date"},
{NotesModel::sortByCategory, "category"},
{NotesModel::sortByTitle, "title"},
{NotesModel::noSorting, "none"}
};
}
QStringList NotesModel::sortingCriteria() const {
@ -257,17 +224,19 @@ int NotesModel::rowCount(const QModelIndex &parent) const {
QVariant NotesModel::data(const QModelIndex &index, int role) const {
if (!index.isValid()) return QVariant();
else if (role == VisibleRole) return m_notes[index.row()].param;
else if (role == IdRole) return m_notes[index.row()].note.id();
else if (role == ModifiedRole) return m_notes[index.row()].note.modified();
else if (role == TitleRole) return m_notes[index.row()].note.title();
else if (role == CategoryRole) return m_notes[index.row()].note.category();
else if (role == ContentRole) return m_notes[index.row()].note.content();
else if (role == FavoriteRole) return m_notes[index.row()].note.favorite();
else if (role == EtagRole) return m_notes[index.row()].note.etag();
else if (role == ErrorRole) return m_notes[index.row()].note.error();
else if (role == ErrorMessageRole) return m_notes[index.row()].note.errorMessage();
else if (role == DateStringRole) return m_notes[index.row()].note.dateString();
else if (role == IdRole) return m_notes[index.row()].id();
else if (role == ModifiedRole) return m_notes[index.row()].modified();
else if (role == TitleRole) return m_notes[index.row()].title();
else if (role == CategoryRole) return m_notes[index.row()].category();
else if (role == ContentRole) return m_notes[index.row()].content();
else if (role == FavoriteRole) return m_notes[index.row()].favorite();
else if (role == EtagRole) return m_notes[index.row()].etag();
else if (role == ErrorRole) return m_notes[index.row()].error();
else if (role == ErrorMessageRole) return m_notes[index.row()].errorMessage();
else if (role == InSearchRole) {
qDebug() << "Invisible:" << m_invisibleIds.contains(m_notes[index.row()].id());
return !m_invisibleIds.contains(m_notes[index.row()].id());
}
return QVariant();
}
@ -275,99 +244,47 @@ QMap<int, QVariant> NotesModel::itemData(const QModelIndex &index) const {
QMap<int, QVariant> map;
if (!index.isValid()) return map;
else {
for (int role = VisibleRole; role <= ErrorMessageRole; role++) {
for (int role = Qt::UserRole; role < Qt::UserRole + 10; ++role) {
map.insert(role, data(index, role));
}
}
return map;
}
bool NotesModel::setData(const QModelIndex &index, const QVariant &value, int role) {
if (!index.isValid()) return false;
else if (role == ModifiedRole && m_notes[index.row()].note.modified() != value.toUInt()) {
m_notes[index.row()].note.setModified(value.toInt());
emit dataChanged(this->index(index.row()), this->index(index.row()), QVector<int> { 1, role } ); // TODO remove when signals from Note are connected
emit dataChanged(this->index(index.row()), this->index(index.row()), QVector<int> { 1, DateStringRole} ); // TODO remove when signals from Note are connected
sort();
return true;
}
else if (role == CategoryRole && m_notes[index.row()].note.category() != value.toString()) {
m_notes[index.row()].note.setCategory(value.toString());
emit dataChanged(this->index(index.row()), this->index(index.row()), QVector<int> { 1, role } ); // TODO remove when signals from Note are connected
sort();
return true;
}
else if (role == ContentRole && m_notes[index.row()].note.content() != value.toString()) {
m_notes[index.row()].note.setContent(value.toString());
emit dataChanged(this->index(index.row()), this->index(index.row()), QVector<int> { 1, role } ); // TODO remove when signals from Note are connected
sort();
return true;
}
else if (role == FavoriteRole && m_notes[index.row()].note.favorite() != value.toBool()) {
m_notes[index.row()].note.setFavorite(value.toBool());
emit dataChanged(this->index(index.row()), this->index(index.row()), QVector<int> { 1, role } ); // TODO remove when signals from Note are connected
sort();
return true;
}
return false;
}
bool NotesModel::setItemData(const QModelIndex &index, const QMap<int, QVariant> &roles) {
if (!index.isValid()) return false;
else if (roles.contains(ModifiedRole) || roles.contains(CategoryRole) || roles.contains(ContentRole) || roles.contains(FavoriteRole)) {
QMap<int, QVariant>::const_iterator i = roles.constBegin();
while (i != roles.constEnd()) {
setData(index, i.value(), i.key());
i++;
}
}
return false;
}
void NotesModel::sort() {
qDebug() << "Sorting notes in the model";
QList<ModelNote<Note, bool> > notes;
QMap<QString, ModelNote<Note, bool> > map;
QMap<QString, ModelNote<Note, bool> > favorites;
if (m_sortBy == sortingNames()[sortByDate]) {
emit layoutAboutToBeChanged(QList<QPersistentModelIndex> (), VerticalSortHint);
for (int i = 0; i < m_notes.size(); i++) {
if (m_favoritesOnTop && m_notes[i].note.favorite())
favorites.insert(QString::number(std::numeric_limits<uint>::max() - m_notes[i].note.modified()), m_notes[i]);
else
map.insert(QString::number(std::numeric_limits<uint>::max() - m_notes[i].note.modified()), m_notes[i]);
}
notes = favorites.values();
notes.append(map.values());
m_notes = notes;
emit layoutChanged(QList<QPersistentModelIndex> (), VerticalSortHint);
if (m_favoritesOnTop) {
if (m_sortBy == sortingNames()[sortByDate]) {
std::sort(m_notes.begin(), m_notes.end(), Note::lessThanByDateFavOnTop);
}
else if (m_sortBy == sortingNames()[sortByCategory]) {
emit layoutAboutToBeChanged(QList<QPersistentModelIndex> (), VerticalSortHint);
for (int i = 0; i < m_notes.size(); i++) {
if (m_favoritesOnTop && m_notes[i].note.favorite())
favorites.insert(m_notes[i].note.category(), m_notes[i]);
else
map.insert(m_notes[i].note.category(), m_notes[i]);
}
notes = favorites.values();
notes.append(map.values());
m_notes = notes;
emit layoutChanged(QList<QPersistentModelIndex> (), VerticalSortHint);
std::sort(m_notes.begin(), m_notes.end(), Note::lessThanByCategoryFavOnTop);
}
else if (m_sortBy == sortingNames()[sortByTitle]) {
emit layoutAboutToBeChanged(QList<QPersistentModelIndex> (), VerticalSortHint);
for (int i = 0; i < m_notes.size(); i++) {
if (m_favoritesOnTop && m_notes[i].note.favorite())
favorites.insert(m_notes[i].note.title(), m_notes[i]);
else
map.insert(m_notes[i].note.title(), m_notes[i]);
std::sort(m_notes.begin(), m_notes.end(), Note::lessThanByTitleFavOnTop);
}
}
else {
if (m_sortBy == sortingNames()[sortByDate]) {
std::sort(m_notes.begin(), m_notes.end(), Note::lessThanByDate);
}
else if (m_sortBy == sortingNames()[sortByCategory]) {
std::sort(m_notes.begin(), m_notes.end(), Note::lessThanByCategory);
}
else if (m_sortBy == sortingNames()[sortByTitle]) {
std::sort(m_notes.begin(), m_notes.end(), Note::lessThanByTitle);
}
}
notes = favorites.values();
notes.append(map.values());
m_notes = notes;
emit layoutChanged(QList<QPersistentModelIndex> (), VerticalSortHint);
}
int NotesModel::indexOf(int id) const {
for (int i = 0; i < m_notes.size(); i++) {
if (m_notes[i].id() == id)
return i;
}
return -1;
}
int NotesModel::insertPosition(const Note &n) const {
@ -375,7 +292,7 @@ int NotesModel::insertPosition(const Note &n) const {
int upper = m_notes.size();
while (lower < upper) {
int middle = qFloor(lower + (upper-lower) / 2);
bool result = noteLessThan(n, m_notes[middle].note);
bool result = noteLessThan(n, m_notes[middle]);
if (result)
upper = middle;
else
@ -386,22 +303,13 @@ int NotesModel::insertPosition(const Note &n) const {
bool NotesModel::noteLessThan(const Note &n1, const Note &n2) const {
if (m_sortBy == sortingNames()[sortByDate]) {
if (m_favoritesOnTop && n1.favorite() != n2.favorite())
return n1.favorite();
else
return n1.modified() > n2.modified();
return m_favoritesOnTop ? Note::lessThanByDateFavOnTop(n1, n2) : Note::lessThanByDate(n1, n2);
}
else if (m_sortBy == sortingNames()[sortByCategory]) {
if (m_favoritesOnTop && n1.favorite() != n2.favorite())
return n1.favorite();
else
return n1.category() < n2.category();
return m_favoritesOnTop ? Note::lessThanByCategoryFavOnTop(n1, n2) : Note::lessThanByCategory(n1, n2);
}
else if (m_sortBy == sortingNames()[sortByTitle]) {
if (m_favoritesOnTop && n1.favorite() != n2.favorite())
return n1.favorite();
else
return n1.title() < n2.title();
return m_favoritesOnTop ? Note::lessThanByTitleFavOnTop(n1, n2) : Note::lessThanByTitle(n1, n2);
}
else {
if (m_favoritesOnTop && n1.favorite() != n2.favorite())
@ -409,73 +317,3 @@ bool NotesModel::noteLessThan(const Note &n1, const Note &n2) const {
}
return true;
}
/*bool NotesModel::noteLessThanByDate(const Note &n1, const Note &n2) {
if (m_favoritesOnTop && n1.favorite != n2.favorite)
return n1.favorite;
else
return n1.modified > n2.modified;
}
bool NotesModel::noteLessThanByCategory(const Note &n1, const Note &n2) {
if (m_favoritesOnTop && n1.favorite != n2.favorite)
return n1.favorite;
else
return n1.category < n2.category;
}
bool NotesModel::noteLessThanByTitle(const Note &n1, const Note &n2) {
if (m_favoritesOnTop && n1.favorite != n2.favorite)
return n1.favorite;
else
return n1.title < n2.title;
}*/
/*
bool NotesModel::insertRow(int row, const QModelIndex &parent) {
beginInsertRows(parent, row, row);
m_notes.insert(row, Note());
endInsertRows();
return true;
}
bool NotesModel::insertRows(int row, int count, const QModelIndex &parent) {
if (count > 0) {
beginInsertRows(parent, row, row+count);
for (int i = 0; i < count; i++) {
m_notes.insert(row + i, Note());
}
endInsertRows();
return true;
}
else {
return false;
}
}
bool NotesModel::removeRow(int row, const QModelIndex &parent) {
if (row >= 0 && row < m_notes.size()) {
beginRemoveRows(parent, row, row);
m_notes.removeAt(row);
endRemoveRows();
return true;
}
else {
return false;
}
}
bool NotesModel::removeRows(int row, int count, const QModelIndex &parent) {
if (row >= 0 && row < m_notes.size()) {
beginRemoveRows(parent, row, count);
for (int i = 0; i < count && row + i < m_notes.size(); i++) {
m_notes.removeAt(row);
}
endRemoveRows();
return true;
}
else {
return false;
}
}
*/

View file

@ -5,12 +5,6 @@
#include <QDateTime>
#include "note.h"
template <typename N, typename P>
struct ModelNote {
N note;
P param;
};
class NotesModel : public QAbstractListModel {
Q_OBJECT
public:
@ -32,29 +26,20 @@ public:
Q_INVOKABLE void search(QString searchText = QString());
Q_INVOKABLE void clearSearch();
Q_INVOKABLE bool applyJSON(const QJsonDocument &jdoc);
Q_INVOKABLE bool applyJSON(const QString &json);
Q_INVOKABLE int insertNote(const Note &note);
Q_INVOKABLE bool removeNote(const Note &note);
Q_INVOKABLE bool removeNote(int position);
Q_INVOKABLE bool replaceNote(const Note &note);
Q_INVOKABLE void clear();
Q_INVOKABLE int indexOf(int id) const;
Q_INVOKABLE Note get(int index) const;
bool applyJSON(const QJsonDocument &jdoc);
bool applyJSON(const QString &json);
enum NoteRoles {
VisibleRole = Qt::UserRole,
IdRole = Qt::UserRole + 1,
ModifiedRole = Qt::UserRole + 2,
TitleRole = Qt::UserRole + 3,
CategoryRole = Qt::UserRole + 4,
ContentRole = Qt::UserRole + 5,
FavoriteRole = Qt::UserRole + 6,
EtagRole = Qt::UserRole + 7,
ErrorRole = Qt::UserRole + 8,
ErrorMessageRole = Qt::UserRole + 9,
DateStringRole = Qt::UserRole + 10
IdRole = Qt::UserRole,
ModifiedRole = Qt::UserRole + 1,
TitleRole = Qt::UserRole + 2,
CategoryRole = Qt::UserRole + 3,
ContentRole = Qt::UserRole + 4,
FavoriteRole = Qt::UserRole + 5,
EtagRole = Qt::UserRole + 6,
ErrorRole = Qt::UserRole + 7,
ErrorMessageRole = Qt::UserRole + 8,
InSearchRole = Qt::UserRole + 9
};
QHash<int, QByteArray> roleNames() const;
@ -71,14 +56,6 @@ public:
virtual int rowCount(const QModelIndex &parent = QModelIndex()) const;
virtual QVariant data(const QModelIndex &index, int role) const;
QMap<int, QVariant> itemData(const QModelIndex &index) const;
//virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const;
virtual bool setData(const QModelIndex &index, const QVariant &value, int role);
virtual bool setItemData(const QModelIndex &index, const QMap<int, QVariant> &roles);
//bool insertRow(int row, const QModelIndex &parent);
//bool insertRows(int row, int count, const QModelIndex &parent);
//bool removeRow(int row, const QModelIndex &parent);
//bool removeRows(int row, int count, const QModelIndex &parent);
protected:
@ -89,19 +66,21 @@ signals:
void searchTextChanged(QString searchText);
private:
QList<ModelNote<Note, bool> > m_notes;
QVector<Note> m_notes;
QVector<int> m_invisibleIds;
QString m_sortBy;
bool m_favoritesOnTop;
QString m_searchText;
void sort();
//void update();
bool applyJSONobject(const QJsonObject &jobj);
int insertNote(const Note &note);
bool replaceNote(const Note &note);
bool removeNote(const Note &note);
bool removeNote(int id);
int indexOf(int id) const;
int insertPosition(const Note &n) const;
bool noteLessThan(const Note &n1, const Note &n2) const;
/*static bool noteLessThanByDate(const Note &n1, const Note &n2);
static bool noteLessThanByCategory(const Note &n1, const Note &n2);
static bool noteLessThanByTitle(const Note &n1, const Note &n2);*/
};
#endif // NOTESMODEL_H

View file

@ -1,22 +0,0 @@
#include "sslconfiguration.h"
#include <QDebug>
SslConfiguration::SslConfiguration(QObject *parent) : QObject(parent), _checkCert(true) {
checkCertConfig = noCheckConfig = QSslConfiguration::defaultConfiguration();
noCheckConfig.setPeerVerifyMode(QSslSocket::VerifyNone);
QSslConfiguration::setDefaultConfiguration(checkCertConfig);
}
bool SslConfiguration::checkCert() {
return _checkCert;
}
void SslConfiguration::setCheckCert(bool check) {
if (_checkCert != check) {
qDebug() << "Changing SSL Cert check to" << check;
_checkCert = check;
QSslConfiguration::setDefaultConfiguration(_checkCert ? checkCertConfig : noCheckConfig);
emit checkCertChanged(_checkCert);
}
}

View file

@ -1,28 +0,0 @@
#ifndef SSLCONFIGURATION_H
#define SSLCONFIGURATION_H
#include <QObject>
#include <QSslConfiguration>
#include <QSslSocket>
class SslConfiguration : public QObject
{
Q_OBJECT
Q_PROPERTY(bool checkCert READ checkCert WRITE setCheckCert NOTIFY checkCertChanged)
public:
explicit SslConfiguration(QObject *parent = nullptr);
public slots:
bool checkCert();
void setCheckCert(bool check);
signals:
void checkCertChanged(bool check);
private:
bool _checkCert;
QSslConfiguration checkCertConfig;
QSslConfiguration noCheckConfig;
};
#endif // SSLCONFIGURATION_H

View file

@ -213,13 +213,6 @@
<translation>Geändert</translation>
</message>
</context>
<context>
<name>NotesApi</name>
<message>
<source>Unable to connect</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>NotesPage</name>
<message>
@ -270,6 +263,34 @@
<source>Open the settings to configure your Nextcloud accounts</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No account yet</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Got to the settings to add an account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No notes yet</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pull down to add a note</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No result</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Try another query</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>An error occurred</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SettingsPage</name>

View file

@ -213,13 +213,6 @@
<translation>Ingen kategori</translation>
</message>
</context>
<context>
<name>NotesApi</name>
<message>
<source>Unable to connect</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>NotesPage</name>
<message>
@ -270,6 +263,34 @@
<source>Open the settings to configure your Nextcloud accounts</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No account yet</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Got to the settings to add an account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No notes yet</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pull down to add a note</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No result</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Try another query</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>An error occurred</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SettingsPage</name>

View file

@ -259,73 +259,100 @@
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>NotesApi</name>
<message>
<location filename="../qml/components/NotesApi.qml" line="68"/>
<source>Unable to connect</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>NotesPage</name>
<message>
<location filename="../qml/pages/NotesPage.qml" line="31"/>
<location filename="../qml/pages/NotesPage.qml" line="38"/>
<source>Settings</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/NotesPage.qml" line="35"/>
<location filename="../qml/pages/NotesPage.qml" line="42"/>
<source>Add note</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/NotesPage.qml" line="40"/>
<location filename="../qml/pages/NotesPage.qml" line="47"/>
<source>Reload</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/NotesPage.qml" line="40"/>
<location filename="../qml/pages/NotesPage.qml" line="47"/>
<source>Updating...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/NotesPage.qml" line="46"/>
<location filename="../qml/pages/NotesPage.qml" line="53"/>
<source>Last update</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/NotesPage.qml" line="49"/>
<location filename="../qml/pages/NotesPage.qml" line="56"/>
<source>never</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/NotesPage.qml" line="59"/>
<location filename="../qml/pages/NotesPage.qml" line="66"/>
<source>Nextcloud Notes</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/NotesPage.qml" line="198"/>
<location filename="../qml/pages/NotesPage.qml" line="206"/>
<source>Modified</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/NotesPage.qml" line="201"/>
<location filename="../qml/pages/NotesPage.qml" line="209"/>
<source>Delete</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/NotesPage.qml" line="203"/>
<location filename="../qml/pages/NotesPage.qml" line="211"/>
<source>Deleting note</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/NotesPage.qml" line="233"/>
<location filename="../qml/pages/NotesPage.qml" line="241"/>
<source>Loading notes...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/NotesPage.qml" line="271"/>
<location filename="../qml/pages/NotesPage.qml" line="247"/>
<source>No account yet</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/NotesPage.qml" line="248"/>
<source>Got to the settings to add an account</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/NotesPage.qml" line="254"/>
<source>No notes yet</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/NotesPage.qml" line="255"/>
<source>Pull down to add a note</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/NotesPage.qml" line="261"/>
<source>No result</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/NotesPage.qml" line="262"/>
<source>Try another query</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/NotesPage.qml" line="268"/>
<source>An error occurred</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/pages/NotesPage.qml" line="279"/>
<source>Open the settings to configure your Nextcloud accounts</source>
<translation type="unfinished"></translation>
</message>