feat(transcripts): migration v5 meta — starred, tags, template, language, segments persisted

This commit is contained in:
2026-04-19 16:35:17 +01:00
parent 9378980639
commit e248923f5d
7 changed files with 360 additions and 24 deletions

View File

@@ -94,10 +94,30 @@ import { toasts } from "$lib/stores/toasts.svelte.js";
export const history = $state([]);
/** Shape a TranscriptDto row from `list_transcripts` into the UI entry shape
* that HistoryPage expects. UI-only fields that were never persisted to
* SQLite (segments, starred, manualTags, language, template) are absent. */
* that HistoryPage expects.
*
* Task 2.5 — the meta columns (starred, manualTags, template, language,
* segments_json) are now carried by SQLite. `manualTags` is stored comma-
* joined in a single TEXT column (empty string = no tags); `segmentsJson`
* is a serialised JSON array of segment objects (empty string = no
* segments).
*/
function mapTranscriptRow(row) {
const text = row.text ?? "";
// manual_tags is a single TEXT column. Empty string ⇒ empty array.
const rawTags = row.manualTags ?? "";
const manualTags = rawTags ? rawTags.split(",").filter(Boolean) : [];
// segments_json is a serialised array. Empty string ⇒ empty array.
const rawSegments = row.segmentsJson ?? "";
let segments = [];
if (rawSegments) {
try {
const parsed = JSON.parse(rawSegments);
if (Array.isArray(parsed)) segments = parsed;
} catch (err) {
console.warn("mapTranscriptRow: segmentsJson parse failed", err);
}
}
return {
id: row.id,
text,
@@ -110,6 +130,11 @@ function mapTranscriptRow(row) {
createdAt: row.createdAt ?? null,
date: row.createdAt ? new Date(row.createdAt).toLocaleString("en-GB") : "",
preview: text.slice(0, 120),
starred: !!row.starred,
manualTags,
template: row.template ?? "",
language: row.language ?? "",
segments,
};
}
@@ -138,9 +163,9 @@ if (typeof window !== "undefined" && hasTauriRuntime()) {
// `saveHistory` is retained as a no-op for callers in HistoryPage that use
// it as a mutation hook (tag add/remove, clear-all). SQLite writes happen
// via the specific `add_transcript` / `update_transcript` /
// `delete_transcript` commands in the functions below; ad-hoc UI-only
// mutations (e.g. manualTags) are session-scoped because they are not
// backed by a SQLite column.
// `update_transcript_meta_cmd` / `delete_transcript` commands in the
// functions below. HistoryPage's manualTags mutations will need to call
// `saveTranscriptMeta` directly (Task 2.5) to persist past reload.
export function saveHistory() {}
/**
@@ -211,6 +236,47 @@ export async function renameHistoryEntry(id, updates) {
}
}
/**
* Task 2.5 — persist viewer metadata to SQLite via update_transcript_meta_cmd.
* `patch` may contain any of:
* - `starred: boolean`
* - `manualTags: string[] | string` (array joined on ',' server-side)
* - `template: string`
* - `language: string`
* - `segments: Array<object>` (JSON.stringify'd before the wire hop)
* Omitted fields are preserved by COALESCE server-side. The in-memory
* history row (if present) is refreshed from the returned canonical row
* so the main-window list stays consistent.
*
* Returns nothing — callers needing the new row should read `history`.
*
* @param {string} id
* @param {Record<string, any>} patch
*/
export async function saveTranscriptMeta(id, patch) {
if (!hasTauriRuntime()) return;
try {
const payload = {
starred: patch.starred,
manualTags: patch.manualTags == null
? undefined
: (Array.isArray(patch.manualTags)
? patch.manualTags.join(",")
: String(patch.manualTags)),
template: patch.template,
language: patch.language,
segmentsJson: patch.segments ? JSON.stringify(patch.segments) : undefined,
};
const row = await invoke("update_transcript_meta_cmd", { id: String(id), patch: payload });
const idx = history.findIndex((h) => String(h.id) === String(id));
if (idx !== -1) {
history[idx] = mapTranscriptRow(row);
}
} catch (err) {
toasts.error("Failed to save transcript metadata", String(err));
}
}
export function deleteFromHistory(index) {
const entry = history[index];
history.splice(index, 1);

View File

@@ -4,6 +4,7 @@
import { convertFileSrc, invoke } from "@tauri-apps/api/core";
import { formatTime, formatDuration } from "$lib/utils/time.js";
import { PLAYBACK_SPEEDS } from "$lib/utils/constants.js";
import { saveTranscriptMeta } from "$lib/stores/page.svelte.js";
let item = $state(null);
let audioEl = $state(null);
@@ -184,6 +185,10 @@
// Update full text
item.text = item.segments.map((s) => s.text).join(" ").trim();
saveItemToHistory();
// Task 2.5 — persist segment boundaries (text + starred flags) to
// SQLite via segments_json. update_transcript above only covers text
// and title; the segment array needs the dedicated meta command.
persistSegments();
}
editingIdx = -1;
editingText = "";
@@ -194,20 +199,35 @@
item.segments.splice(idx, 1);
item.text = item.segments.map((s) => s.text).join(" ").trim();
saveItemToHistory();
// Task 2.5 — persist the shrunk segment array.
persistSegments();
if (editingIdx === idx) { editingIdx = -1; }
}
}
function toggleStar(idx) {
if (item?.segments) {
// TODO: Phase 2 remaining Task 1.5 — starred is not a SQLite column
// on the transcripts table yet, so this mutation is in-memory only
// and will not survive a reload. Task 1.5 will extend update_transcript
// (and the transcripts schema) to carry starred segment state.
item.segments[idx].starred = !item.segments[idx].starred;
// Task 2.5 — persist the updated segments array (incl. starred flags)
// via the new update_transcript_meta_cmd. Before this, starring a
// segment was in-memory only and vanished on reload.
persistSegments();
}
}
// Task 2.5 — single entry point for segments_json persistence. Callers
// already guard with `if (item?.segments)` so the snapshot here is safe;
// the @ts-ignore matches the file-wide pattern where `item` inferred as
// `null` from `$state(null)` narrows to `never` after guards. Replacing
// the annotation needs a full viewer re-typing pass — out of scope for
// Task 2.5, tracked by the file's 80-odd pre-existing TS errors.
function persistSegments() {
if (!item) return;
/** @type {any} */
const snap = item;
saveTranscriptMeta(snap.id, { segments: snap.segments });
}
function copySegment(idx) {
if (item?.segments?.[idx]) {
const text = item.segments[idx].text.trim();
@@ -217,13 +237,11 @@
}
}
// Persist edits back to SQLite via update_transcript (the canonical store).
// Only `text` and `title` are accepted by update_transcript today. Segment
// structure, segment-level starred flags, and manualTags are not SQLite
// columns yet — Phase 2 remaining Task 1.5 will extend the command and the
// transcripts schema to cover those. Until then, segment-level edits that
// change `item.text` (the joined full transcript) survive reload; the
// segment array itself does not.
// Persist edits back to SQLite. `update_transcript` covers `text` / `title`;
// segment structure, per-segment starred flags, manualTags, template and
// language go through `update_transcript_meta_cmd` (Task 2.5) via the
// store's `saveTranscriptMeta` helper — callers in this file wire those
// up alongside the plain-text update below.
//
// The cross-window `kon_viewer_item` localStorage payload stays in sync so
// a second viewer window hydrating from it reflects the latest edit.