214 lines
6.7 KiB
JavaScript
214 lines
6.7 KiB
JavaScript
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
|
|
|
|
const decoder = new TextDecoder();
|
|
|
|
function readJson(path) {
|
|
try {
|
|
const [ok, bytes] = GLib.file_get_contents(path);
|
|
return ok ? JSON.parse(decoder.decode(bytes)) : [];
|
|
} catch {
|
|
// Datei gibt es beim ersten Start noch nicht
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function writeJson(path, data) {
|
|
GLib.file_set_contents(path, JSON.stringify(data, null, 2));
|
|
}
|
|
|
|
function compareArrays(a, b) {
|
|
for (let i = 0; i < a.length; i++) {
|
|
if (a[i] < b[i])
|
|
return -1;
|
|
if (a[i] > b[i])
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function listFolder(path) {
|
|
const node = {name: GLib.path_get_basename(path), path, folders: [], notes: []};
|
|
const dir = Gio.File.new_for_path(path);
|
|
const children = dir.enumerate_children('standard::name,standard::type', Gio.FileQueryInfoFlags.NONE, null);
|
|
let info;
|
|
while ((info = children.next_file(null))) {
|
|
const name = info.get_name();
|
|
if (name.startsWith('.'))
|
|
continue;
|
|
const child = GLib.build_filenamev([path, name]);
|
|
if (info.get_file_type() === Gio.FileType.DIRECTORY)
|
|
node.folders.push(listFolder(child));
|
|
else if (name.endsWith('.md'))
|
|
node.notes.push({name: name.slice(0, -3), path: child});
|
|
}
|
|
children.close(null);
|
|
node.folders.sort((a, b) => a.name.localeCompare(b.name));
|
|
node.notes.sort((a, b) => a.name.localeCompare(b.name));
|
|
return node;
|
|
}
|
|
|
|
function checkName(name) {
|
|
if (!name)
|
|
throw new Error('Name fehlt');
|
|
if (name.includes('/') || name.startsWith('.'))
|
|
throw new Error('Name darf kein "/" enthalten und nicht mit "." beginnen');
|
|
}
|
|
|
|
export class Store {
|
|
constructor() {
|
|
this.dir = GLib.build_filenamev([GLib.get_user_data_dir(), 'desktop-app']);
|
|
this.notesDir = GLib.build_filenamev([this.dir, 'notes']);
|
|
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.events = readJson(this._eventsPath);
|
|
this.todos = readJson(this._todosPath);
|
|
this._listeners = [];
|
|
}
|
|
|
|
onChange(callback) {
|
|
this._listeners.push(callback);
|
|
}
|
|
|
|
_changed() {
|
|
for (const callback of this._listeners)
|
|
callback();
|
|
}
|
|
|
|
// ---- Termine ----
|
|
|
|
// 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))
|
|
.sort((a, b) => compareArrays([a.start ?? '', a.title], [b.start ?? '', b.title]));
|
|
}
|
|
|
|
saveEvent(event) {
|
|
const now = new Date().toISOString();
|
|
if (!event.id) {
|
|
this.events.push({...event, id: GLib.uuid_string_random(), created: now, updated: now});
|
|
} else {
|
|
const index = this.events.findIndex(e => e.id === event.id);
|
|
this.events[index] = {...event, updated: now};
|
|
}
|
|
writeJson(this._eventsPath, this.events);
|
|
this._changed();
|
|
}
|
|
|
|
removeEvent(id) {
|
|
this.events = this.events.filter(e => e.id !== id);
|
|
writeJson(this._eventsPath, this.events);
|
|
this._changed();
|
|
}
|
|
|
|
// ---- Todos ----
|
|
|
|
todosOn(date) {
|
|
return this.todos
|
|
.filter(t => 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)));
|
|
}
|
|
|
|
saveTodo(todo) {
|
|
const now = new Date().toISOString();
|
|
if (!todo.id) {
|
|
this.todos.push({...todo, done: false, id: GLib.uuid_string_random(), created: now, updated: now});
|
|
} else {
|
|
const index = this.todos.findIndex(t => t.id === todo.id);
|
|
this.todos[index] = {...todo, updated: now};
|
|
}
|
|
writeJson(this._todosPath, this.todos);
|
|
this._changed();
|
|
}
|
|
|
|
toggleTodo(id) {
|
|
const todo = this.todos.find(t => t.id === id);
|
|
this.saveTodo({...todo, done: !todo.done});
|
|
}
|
|
|
|
removeTodo(id) {
|
|
this.todos = this.todos.filter(t => t.id !== id);
|
|
writeJson(this._todosPath, this.todos);
|
|
this._changed();
|
|
}
|
|
|
|
// ---- Notizen ----
|
|
|
|
tree() {
|
|
return listFolder(this.notesDir);
|
|
}
|
|
|
|
readNote(path) {
|
|
const [, bytes] = GLib.file_get_contents(path);
|
|
return decoder.decode(bytes);
|
|
}
|
|
|
|
writeNote(path, text) {
|
|
GLib.file_set_contents(path, text);
|
|
}
|
|
|
|
createNote(folder, name) {
|
|
checkName(name);
|
|
const path = GLib.build_filenamev([folder, `${name}.md`]);
|
|
if (GLib.file_test(path, GLib.FileTest.EXISTS))
|
|
throw new Error('Diese Notiz gibt es schon');
|
|
GLib.file_set_contents(path, '');
|
|
return path;
|
|
}
|
|
|
|
createFolder(parent, name) {
|
|
checkName(name);
|
|
const path = GLib.build_filenamev([parent, name]);
|
|
if (GLib.file_test(path, GLib.FileTest.EXISTS))
|
|
throw new Error('Diesen Ordner gibt es schon');
|
|
Gio.File.new_for_path(path).make_directory(null);
|
|
return path;
|
|
}
|
|
|
|
renameNote(path, name) {
|
|
checkName(name);
|
|
const target = GLib.build_filenamev([GLib.path_get_dirname(path), `${name}.md`]);
|
|
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);
|
|
return target;
|
|
}
|
|
|
|
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');
|
|
Gio.File.new_for_path(path).move(Gio.File.new_for_path(target), Gio.FileCopyFlags.NONE, null, null);
|
|
return target;
|
|
}
|
|
|
|
deleteNote(path) {
|
|
Gio.File.new_for_path(path).delete(null);
|
|
}
|
|
|
|
folderEmpty(path) {
|
|
const node = listFolder(path);
|
|
return !node.folders.length && !node.notes.length;
|
|
}
|
|
|
|
deleteFolder(path) {
|
|
if (!this.folderEmpty(path))
|
|
throw new Error('Der Ordner ist nicht leer');
|
|
Gio.File.new_for_path(path).delete(null);
|
|
}
|
|
}
|