feat(gui): Projektansicht mit Ordnern, Unterreitern und angehefteten Aufnahmen

Neuer Reiter neben "Aufnahmen": Projektordner bündeln Aufnahmen zweistufig
(Ordner + Unterreiter), auf beiden Ebenen anheftbar. Anheften geht aus dem
Projekt heraus (Dropdown mit Filter) oder über den 📌-Knopf in der
Aufnahmenliste, der Ordner und Ebene abfragt, neue Ordner anlegt und
bestehende Anheftungen zum Lösen zeigt. Klick auf eine angeheftete Aufnahme
springt in die Aufnahmen-Ansicht.

Ein Pin ist eine Referenz, kein Besitz: die Aufnahme bleibt unverändert in
der Liste und darf in mehreren Ordnern hängen.

project_pins.tab_id ist INT NOT NULL DEFAULT 0 (0 = Ordner-Ebene) statt NULL,
weil MariaDB NULL von UNIQUE-Indizes ausschließt — sonst ließe sich dieselbe
Aufnahme mehrfach auf dieselbe Ebene anheften. So macht INSERT IGNORE das
Anheften idempotent.

purgeRecording räumt project_pins mit ab; getProject blendet Aufnahmen im
Papierkorb aus, ohne deren Pin zu löschen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
schakal00783
2026-08-02 23:05:07 +02:00
co-authored by Claude Opus 5
parent 963ce427da
commit 772385713e
7 changed files with 688 additions and 9 deletions
+57
View File
@@ -449,6 +449,63 @@ app.delete('/api/recordings/:id', wrap(async (req, res) => {
await db.deleteRecording(+req.params.id);
res.json({ ok: true, trashed: true, days: db.TRASH_DAYS });
}));
// --- Projektansicht -----------------------------------------------------------
// Ordner > Unterreiter > angepinnte Aufnahmen. Rein organisatorisch: ein Pin
// referenziert eine Aufnahme, er verschiebt oder kopiert nichts. Dieselbe
// Aufnahme darf in mehreren Projekten und Reitern hängen.
app.get('/api/projects', wrap(async (req, res) => res.json(await db.listProjects())));
app.post('/api/projects', wrap(async (req, res) => {
const name = String((req.body && req.body.name) || '').trim();
if (!name) return res.status(400).json({ error: 'name fehlt' });
res.json(await db.createProject(name));
}));
app.get('/api/projects/:id', wrap(async (req, res) => {
const p = await db.getProject(+req.params.id);
p ? res.json(p) : res.status(404).json({ error: 'not found' });
}));
app.patch('/api/projects/:id', wrap(async (req, res) => {
const name = String((req.body && req.body.name) || '').trim();
if (!name) return res.status(400).json({ error: 'name fehlt' });
await db.renameProject(+req.params.id, name);
res.json({ ok: true, id: +req.params.id, name });
}));
app.delete('/api/projects/:id', wrap(async (req, res) => {
await db.deleteProject(+req.params.id);
res.json({ ok: true });
}));
app.post('/api/projects/:id/tabs', wrap(async (req, res) => {
const name = String((req.body && req.body.name) || '').trim();
if (!name) return res.status(400).json({ error: 'name fehlt' });
res.json(await db.createProjectTab(+req.params.id, name));
}));
app.patch('/api/projects/:id/tabs/:tid', wrap(async (req, res) => {
const name = String((req.body && req.body.name) || '').trim();
if (!name) return res.status(400).json({ error: 'name fehlt' });
await db.renameProjectTab(+req.params.tid, name);
res.json({ ok: true, id: +req.params.tid, name });
}));
app.delete('/api/projects/:id/tabs/:tid', wrap(async (req, res) => {
await db.deleteProjectTab(+req.params.tid);
res.json({ ok: true });
}));
// tab_id fehlt / 0 = direkt auf der Ordner-Ebene anpinnen.
app.post('/api/projects/:id/pins', wrap(async (req, res) => {
const recId = +((req.body && req.body.recording_id) || 0);
if (!recId) return res.status(400).json({ error: 'recording_id fehlt' });
await db.pinRecording(+req.params.id, +((req.body && req.body.tab_id) || 0), recId);
res.json({ ok: true });
}));
app.delete('/api/projects/:id/pins/:pinId', wrap(async (req, res) => {
await db.unpinRecording(+req.params.pinId);
res.json({ ok: true });
}));
// Für den 📌-Button in der Aufnahmenliste: Zuordnungen einer Aufnahme bzw.
// die Pin-Zahl aller Aufnahmen auf einmal.
app.get('/api/recordings/:id/pins', wrap(async (req, res) => res.json(await db.listPinsForRecording(+req.params.id))));
app.get('/api/pins/counts', wrap(async (req, res) => res.json(await db.pinCounts())));
app.get('/api/trash', wrap(async (req, res) => res.json({ days: db.TRASH_DAYS, items: await db.listTrash() })));
app.post('/api/trash/:id/restore', wrap(async (req, res) => {
await db.restoreRecording(+req.params.id);