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); } }