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:
co-authored by
Claude Opus 5
parent
963ce427da
commit
772385713e
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user