Files
desktop-planer/desktop@marha.de/store.js
2026-09-18 22:02:53 +02:00

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