diff --git a/src/lib/pages/HistoryPage.svelte b/src/lib/pages/HistoryPage.svelte index 1d65f58..c9a083f 100644 --- a/src/lib/pages/HistoryPage.svelte +++ b/src/lib/pages/HistoryPage.svelte @@ -28,10 +28,12 @@ import LumotiaNotice from "$lib/ui/LumotiaNotice.svelte"; // v0.3 Phase 5f — quietware History layout. import LumotiaPageSkeleton from "$lib/ui/LumotiaPageSkeleton.svelte"; + import LumotiaMenu from "$lib/ui/LumotiaMenu.svelte"; + import LumotiaIconButton from "$lib/ui/LumotiaIconButton.svelte"; import { onMount } from "svelte"; import { formatTime, formatDuration, formatTimestamp } from "$lib/utils/time.js"; import { PLAYBACK_SPEEDS } from "$lib/utils/constants.js"; - import { Search, Clock, Play, Pause, FileText, Mic, ChevronDown, ExternalLink, Star, Tag } from 'lucide-svelte'; + import { Search, Clock, Play, Pause, FileText, Mic, ChevronDown, ExternalLink, Star, Tag, MoreHorizontal, Copy, Trash2, FileDown } from 'lucide-svelte'; const prefs = getPreferences(); const COLLAPSED_ROW_MIN_HEIGHT = 54; @@ -298,6 +300,95 @@ return item.title?.trim() || "Untitled"; } + // v0.3 Phase 5f polish — quietware row helpers. Kept separate from + // compactPreviewText so the v0.2 fallback's row-height measurement + // (which reads compactPreviews) is untouched. + + /** Two-line transcript snippet for the quietware row sub-line. Falls + * through item.text → item.preview → empty string ("No preview…" + * rendering is the template's job, not this helper's). Stripped of + * leading whitespace so previews don't start mid-pause. */ + function quietRowPreview(item) { + const raw = (item?.text || item?.preview || "").replace(/\s+/g, " ").trim(); + return raw; + } + + /** Source label for the quietware row meta-line. "Dictation" for + * mic-captured items, "File" for imports. Mirrors the icon column + * but lives in the meta-line so the row scans without colour cues. */ + function quietRowSourceLabel(item) { + const s = (item?.source || "").toLowerCase(); + return s.includes("file") ? "File" : "Dictation"; + } + + /** Just the HH:MM portion of a saved-at timestamp. The date sits in + * the group header above the row, so repeating it here would waste + * hierarchy. Uses en-GB 24h to match formatTimestamp. */ + function quietRowTime(iso) { + if (!iso) return ""; + const d = new Date(iso); + if (Number.isNaN(d.getTime())) return ""; + return d.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" }); + } + + /** Calendar-day label for date-group headers. Formula: + * same local-calendar day as now → "Today" + * previous local-calendar day → "Yesterday" + * same local-calendar year → "D MMM" (e.g. "12 May") + * older → "D MMM YYYY" (e.g. "12 May 2025") + * Day boundaries are strictly local midnight, not 24-hour windows. */ + function dateGroupLabel(iso) { + if (!iso) return "Undated"; + const d = new Date(iso); + if (Number.isNaN(d.getTime())) return "Undated"; + const now = new Date(); + const sameDay = (a, b) => + a.getFullYear() === b.getFullYear() && + a.getMonth() === b.getMonth() && + a.getDate() === b.getDate(); + if (sameDay(d, now)) return "Today"; + const y = new Date(now); + y.setDate(now.getDate() - 1); + if (sameDay(d, y)) return "Yesterday"; + const fmt = d.toLocaleDateString("en-GB", { + day: "numeric", + month: "short", + year: d.getFullYear() === now.getFullYear() ? undefined : "numeric", + }); + return fmt; + } + + /** Stable bucket key for grouping. Local-calendar YYYY-MM-DD so the + * groups break at midnight, not on UTC date math. Items with no + * savedAt fall into an "undated" bucket that surfaces last. */ + function dateGroupKey(iso) { + if (!iso) return "undated"; + const d = new Date(iso); + if (Number.isNaN(d.getTime())) return "undated"; + const y = d.getFullYear(); + const m = (d.getMonth() + 1).toString().padStart(2, "0"); + const day = d.getDate().toString().padStart(2, "0"); + return `${y}-${m}-${day}`; + } + + /** Walk `filtered` in order, breaking at calendar-day boundaries. + * Preserves the underlying sort so newest-first stays newest-first; + * groups appear in the order their first item appears. */ + let liveGroups = $derived.by(() => { + const out = []; + let current = null; + for (const item of filtered) { + const iso = item.savedAt || item.timestamp; + const key = dateGroupKey(iso); + if (!current || current.key !== key) { + current = { key, label: dateGroupLabel(iso), items: [] }; + out.push(current); + } + current.items.push(item); + } + return out; + }); + let compactPreviews = $derived.by(() => { return filtered.map((item) => { const text = compactPreviewText(item); @@ -721,14 +812,19 @@ +
- {item.title || item.preview?.slice(0, 80) || "Untitled"} -
-{compactPreviewText(item)}
-{formatTimestamp(item.savedAt || item.timestamp)}
-+ {item.title?.trim() || "Untitled"} +
++ {previewText || "No preview available"} +
++ {quietRowTime(item.savedAt || item.timestamp)} + {#if item.duration} + · {formatDuration(item.duration)} + {/if} + · {sourceLabel} +
+- {item.title || item.preview?.slice(0, 80) || "Untitled"} + {item.title?.trim() || "Untitled"} +
++ {previewText || "No preview available"} +
++ Deleted {formatTrashTimestamp(item.deleted_at || item.createdAt)} · Auto-purges after 30 days
-{item.preview || ""}
-Deleted {formatTrashTimestamp(item.deleted_at)}