update
This commit is contained in:
@@ -2,26 +2,44 @@ import Gio from 'gi://Gio';
|
||||
import GLib from 'gi://GLib';
|
||||
|
||||
// Ablage: ~/.local/share/desktop-app/
|
||||
// events.json Termine
|
||||
// todos.json Todos
|
||||
// notes/ Notizen als .md-Dateien, Ordner beliebig verschachtelt
|
||||
// events.json Termine
|
||||
// todos.json Todos
|
||||
// notes/ Notizen als .md-Dateien, Ordner beliebig verschachtelt
|
||||
// deleted-notes.json gelöschte Notizen mit Zeitpunkt, damit der Sync sie auch woanders löscht
|
||||
// sync.json Server-Zugang (url, token), siehe sync.js
|
||||
// Gelöschte Termine und Todos bleiben mit deleted: true in der Liste (Tombstone) und werden nur ausgeblendet.
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
function readJson(path) {
|
||||
export function readJson(path, fallback = []) {
|
||||
try {
|
||||
const [ok, bytes] = GLib.file_get_contents(path);
|
||||
return ok ? JSON.parse(decoder.decode(bytes)) : [];
|
||||
return ok ? JSON.parse(decoder.decode(bytes)) : fallback;
|
||||
} catch {
|
||||
// Datei gibt es beim ersten Start noch nicht
|
||||
return [];
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function writeJson(path, data) {
|
||||
export function writeJson(path, data) {
|
||||
GLib.file_set_contents(path, JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
// Änderungszeit einer Datei als ISO-String mit Millisekunden.
|
||||
function modifiedIso(file) {
|
||||
const info = file.query_info('time::modified,time::modified-usec', Gio.FileQueryInfoFlags.NONE, null);
|
||||
const usec = info.get_attribute_uint32('time::modified-usec');
|
||||
return new Date(Number(info.get_attribute_uint64('time::modified')) * 1000 + Math.floor(usec / 1000)).toISOString();
|
||||
}
|
||||
|
||||
function setModified(file, iso) {
|
||||
const ms = new Date(iso).getTime();
|
||||
const info = new Gio.FileInfo();
|
||||
info.set_attribute_uint64('time::modified', Math.floor(ms / 1000));
|
||||
info.set_attribute_uint32('time::modified-usec', (ms % 1000) * 1000);
|
||||
file.set_attributes_from_info(info, Gio.FileQueryInfoFlags.NONE, null);
|
||||
}
|
||||
|
||||
function compareArrays(a, b) {
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] < b[i])
|
||||
@@ -67,9 +85,13 @@ export class Store {
|
||||
GLib.mkdir_with_parents(this.notesDir, 0o755);
|
||||
this._eventsPath = GLib.build_filenamev([this.dir, 'events.json']);
|
||||
this._todosPath = GLib.build_filenamev([this.dir, 'todos.json']);
|
||||
this._deletedNotesPath = GLib.build_filenamev([this.dir, 'deleted-notes.json']);
|
||||
this.events = readJson(this._eventsPath);
|
||||
this.todos = readJson(this._todosPath);
|
||||
this.deletedNotes = readJson(this._deletedNotesPath, {});
|
||||
this._listeners = [];
|
||||
// Notiz, die gerade im Editor offen ist. Der Sync fasst sie nicht an.
|
||||
this.lockedNote = null;
|
||||
}
|
||||
|
||||
onChange(callback) {
|
||||
@@ -86,7 +108,7 @@ export class Store {
|
||||
// Termine, die den Tag berühren. Mehrtägige laufen von date bis endDate.
|
||||
eventsOn(date) {
|
||||
return this.events
|
||||
.filter(e => e.date <= date && date <= (e.endDate ?? e.date))
|
||||
.filter(e => !e.deleted && e.date <= date && date <= (e.endDate ?? e.date))
|
||||
.sort((a, b) => compareArrays([a.start ?? '', a.title], [b.start ?? '', b.title]));
|
||||
}
|
||||
|
||||
@@ -103,23 +125,45 @@ export class Store {
|
||||
}
|
||||
|
||||
removeEvent(id) {
|
||||
this.events = this.events.filter(e => e.id !== id);
|
||||
writeJson(this._eventsPath, this.events);
|
||||
this._changed();
|
||||
const event = this.events.find(e => e.id === id);
|
||||
this.saveEvent({...event, deleted: true});
|
||||
}
|
||||
|
||||
// ---- Sync: Änderungen von außen übernehmen, neuester Stand gewinnt ----
|
||||
|
||||
// Gibt true zurück, wenn sich lokal etwas geändert hat.
|
||||
applyRemote(collection, items) {
|
||||
const list = this[collection];
|
||||
let changed = false;
|
||||
for (const item of items) {
|
||||
const index = list.findIndex(e => e.id === item.id);
|
||||
if (index >= 0 && list[index].updated >= item.updated)
|
||||
continue;
|
||||
if (index >= 0)
|
||||
list[index] = item;
|
||||
else
|
||||
list.push(item);
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
writeJson(collection === 'events' ? this._eventsPath : this._todosPath, list);
|
||||
this._changed();
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
// ---- Todos ----
|
||||
|
||||
todosOn(date) {
|
||||
return this.todos
|
||||
.filter(t => t.date === date)
|
||||
.filter(t => !t.deleted && t.date === date)
|
||||
.sort((a, b) => compareArrays([a.time ?? '', a.title], [b.time ?? '', b.title]));
|
||||
}
|
||||
|
||||
// Offene zuerst (terminierte nach Datum, dann allgemeine), erledigte zuletzt.
|
||||
sortedTodos() {
|
||||
const key = t => [t.done ? 1 : 0, t.date ? 0 : 1, t.date ?? '', t.time ?? '', t.created ?? ''];
|
||||
return [...this.todos].sort((a, b) => compareArrays(key(a), key(b)));
|
||||
return this.todos.filter(t => !t.deleted).sort((a, b) => compareArrays(key(a), key(b)));
|
||||
}
|
||||
|
||||
saveTodo(todo) {
|
||||
@@ -140,9 +184,8 @@ export class Store {
|
||||
}
|
||||
|
||||
removeTodo(id) {
|
||||
this.todos = this.todos.filter(t => t.id !== id);
|
||||
writeJson(this._todosPath, this.todos);
|
||||
this._changed();
|
||||
const todo = this.todos.find(t => t.id === id);
|
||||
this.saveTodo({...todo, deleted: true});
|
||||
}
|
||||
|
||||
// ---- Notizen ----
|
||||
@@ -151,6 +194,50 @@ export class Store {
|
||||
return listFolder(this.notesDir);
|
||||
}
|
||||
|
||||
// Pfad relativ zum Notizordner, dient dem Sync als id.
|
||||
relative(path) {
|
||||
return path.slice(this.notesDir.length + 1);
|
||||
}
|
||||
|
||||
absolute(relative) {
|
||||
return GLib.build_filenamev([this.notesDir, relative]);
|
||||
}
|
||||
|
||||
// Alle Notizen mit Änderungszeit, für den Sync.
|
||||
listNotes() {
|
||||
const out = [];
|
||||
const walk = folder => {
|
||||
folder.folders.forEach(walk);
|
||||
for (const note of folder.notes)
|
||||
out.push({id: this.relative(note.path), updated: modifiedIso(Gio.File.new_for_path(note.path))});
|
||||
};
|
||||
walk(this.tree());
|
||||
return out;
|
||||
}
|
||||
|
||||
// Notiz vom Server übernehmen. Die Änderungszeit wird übernommen, damit sie nicht als neu gilt.
|
||||
writeRemoteNote(relative, content, updated) {
|
||||
const path = this.absolute(relative);
|
||||
GLib.mkdir_with_parents(GLib.path_get_dirname(path), 0o755);
|
||||
GLib.file_set_contents(path, content);
|
||||
setModified(Gio.File.new_for_path(path), updated);
|
||||
delete this.deletedNotes[relative];
|
||||
writeJson(this._deletedNotesPath, this.deletedNotes);
|
||||
this._changed();
|
||||
}
|
||||
|
||||
deleteRemoteNote(relative) {
|
||||
const path = this.absolute(relative);
|
||||
if (GLib.file_test(path, GLib.FileTest.EXISTS))
|
||||
Gio.File.new_for_path(path).delete(null);
|
||||
this._changed();
|
||||
}
|
||||
|
||||
_rememberDeleted(path) {
|
||||
this.deletedNotes[this.relative(path)] = new Date().toISOString();
|
||||
writeJson(this._deletedNotesPath, this.deletedNotes);
|
||||
}
|
||||
|
||||
readNote(path) {
|
||||
const [, bytes] = GLib.file_get_contents(path);
|
||||
return decoder.decode(bytes);
|
||||
@@ -166,6 +253,8 @@ export class Store {
|
||||
if (GLib.file_test(path, GLib.FileTest.EXISTS))
|
||||
throw new Error('Diese Notiz gibt es schon');
|
||||
GLib.file_set_contents(path, '');
|
||||
delete this.deletedNotes[this.relative(path)];
|
||||
writeJson(this._deletedNotesPath, this.deletedNotes);
|
||||
return path;
|
||||
}
|
||||
|
||||
@@ -184,20 +273,38 @@ export class Store {
|
||||
if (GLib.file_test(target, GLib.FileTest.EXISTS))
|
||||
throw new Error('Diese Notiz gibt es schon');
|
||||
Gio.File.new_for_path(path).move(Gio.File.new_for_path(target), Gio.FileCopyFlags.NONE, null, null);
|
||||
// Für den Sync ist das löschen plus neu anlegen
|
||||
this._rememberDeleted(path);
|
||||
this._touch(target);
|
||||
return target;
|
||||
}
|
||||
|
||||
_touch(path) {
|
||||
setModified(Gio.File.new_for_path(path), new Date().toISOString());
|
||||
}
|
||||
|
||||
renameFolder(path, name) {
|
||||
checkName(name);
|
||||
const target = GLib.build_filenamev([GLib.path_get_dirname(path), name]);
|
||||
if (GLib.file_test(target, GLib.FileTest.EXISTS))
|
||||
throw new Error('Diesen Ordner gibt es schon');
|
||||
const notes = [];
|
||||
const collect = folder => {
|
||||
folder.folders.forEach(collect);
|
||||
notes.push(...folder.notes.map(n => n.path));
|
||||
};
|
||||
collect(listFolder(path));
|
||||
Gio.File.new_for_path(path).move(Gio.File.new_for_path(target), Gio.FileCopyFlags.NONE, null, null);
|
||||
for (const note of notes) {
|
||||
this._rememberDeleted(note);
|
||||
this._touch(target + note.slice(path.length));
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
deleteNote(path) {
|
||||
Gio.File.new_for_path(path).delete(null);
|
||||
this._rememberDeleted(path);
|
||||
}
|
||||
|
||||
folderEmpty(path) {
|
||||
|
||||
Reference in New Issue
Block a user