refactor(frontend): migrate JS modules to TypeScript
Wholesale JS -> TS migration of the frontend — stores, utils, actions,
and all Svelte component scripts adopt type annotations. Compile-time
surfaces (app.d.ts, lib/types/) added for shared DTO types.
Build plumbing:
- package.json: dev:frontend script that runs svelte-kit sync first
- tauri.conf.json: beforeDevCommand points at dev:frontend
- run.sh: dropped the sed-hack that temporarily blanked beforeDevCommand;
now relies on npm run dev:frontend to avoid double-Vite
- jsconfig.json: allowImportingTsExtensions
Preserves all Group 1 behaviour:
- page.svelte.ts keeps loadHistory / loadTasks Tauri-first, no
localStorage; saveTranscriptMeta + mapTranscriptRow + mapTaskRow
intact; update_task_cmd and update_transcript_meta_cmd invocations
carry the correct payload shape.
- Toasts, preferences stores typed without behaviour change.
- Viewer still routes segment edits through saveTranscriptMeta; the
Task 1.5 TODO markers are gone.
taskExtractor.ts is functionally improved during the migration:
- multi-task matches in the same sentence
- list-style shopping-verb expansion (get bread, milk, and cheese)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import type { ViewerSegment } from "$lib/types/app";
|
||||
import { measureTextHeight } from "$lib/utils/textMeasure.js";
|
||||
import { buildCumulativeOffsets, findVisibleRange } from "$lib/utils/virtualList.js";
|
||||
import { getPreferences } from "$lib/stores/preferences.svelte.js";
|
||||
@@ -8,23 +9,39 @@
|
||||
let {
|
||||
segments = [],
|
||||
activeSegmentIdx = -1,
|
||||
matchingSegments = new Set(),
|
||||
matchingSegments = new Set<number>(),
|
||||
showTimestamps = true,
|
||||
editingIdx = -1,
|
||||
editingText = $bindable(""),
|
||||
searchQuery = "",
|
||||
formatTime = (t) => String(t),
|
||||
onSeekToTime = () => {},
|
||||
onStartEditing = () => {},
|
||||
formatTime = (t: number) => String(t),
|
||||
onSeekToTime = (_time: number) => {},
|
||||
onStartEditing = (_idx: number) => {},
|
||||
onFinishEditing = () => {},
|
||||
onToggleStar = () => {},
|
||||
onCopySegment = () => {},
|
||||
onDeleteSegment = () => {},
|
||||
highlightText = (t) => t,
|
||||
} = $props();
|
||||
onToggleStar = (_idx: number) => {},
|
||||
onCopySegment = (_idx: number) => {},
|
||||
onDeleteSegment = (_idx: number) => {},
|
||||
highlightText = (t: string) => t,
|
||||
} = $props<{
|
||||
segments?: ViewerSegment[];
|
||||
activeSegmentIdx?: number;
|
||||
matchingSegments?: Set<number>;
|
||||
showTimestamps?: boolean;
|
||||
editingIdx?: number;
|
||||
editingText?: string;
|
||||
searchQuery?: string;
|
||||
formatTime?: (time: number) => string;
|
||||
onSeekToTime?: (time: number) => void;
|
||||
onStartEditing?: (idx: number) => void;
|
||||
onFinishEditing?: () => void;
|
||||
onToggleStar?: (idx: number) => void;
|
||||
onCopySegment?: (idx: number) => void;
|
||||
onDeleteSegment?: (idx: number) => void;
|
||||
highlightText?: (text: string) => string;
|
||||
}>();
|
||||
|
||||
// Internal state
|
||||
let containerEl = $state(null);
|
||||
let containerEl = $state<HTMLDivElement | null>(null);
|
||||
let containerHeight = $state(0);
|
||||
let containerWidth = $state(0);
|
||||
let scrollTop = $state(0);
|
||||
@@ -46,7 +63,7 @@
|
||||
const textWidth = containerWidth - 26 - timestampWidth - 24;
|
||||
if (textWidth <= 0) return segments.map(() => 40);
|
||||
|
||||
return segments.map(seg => {
|
||||
return segments.map((seg: ViewerSegment) => {
|
||||
const text = seg.text?.trim() || "";
|
||||
if (!text) return SEGMENT_PADDING + textLineHeight;
|
||||
const { height } = measureTextHeight(text, textFont, textWidth, textLineHeight);
|
||||
@@ -75,7 +92,7 @@
|
||||
|
||||
// Visible segment slice with positioning
|
||||
let visibleItems = $derived.by(() => {
|
||||
const items = [];
|
||||
const items: Array<{ seg: ViewerSegment; idx: number; top: number; height: number }> = [];
|
||||
for (let i = visibleRange.start; i < visibleRange.end; i++) {
|
||||
items.push({
|
||||
seg: segments[i],
|
||||
@@ -88,7 +105,7 @@
|
||||
});
|
||||
|
||||
// Scroll to a specific segment index (used for active segment tracking + search)
|
||||
export function scrollToSegment(idx) {
|
||||
export function scrollToSegment(idx: number) {
|
||||
if (!containerEl || idx < 0 || idx >= segments.length) return;
|
||||
const segTop = cumulativeOffsets[idx];
|
||||
const segHeight = segmentHeights[idx];
|
||||
@@ -96,8 +113,8 @@
|
||||
containerEl.scrollTop = segTop - viewCenter + segHeight / 2;
|
||||
}
|
||||
|
||||
function onScroll(e) {
|
||||
scrollTop = e.target.scrollTop;
|
||||
function onScroll(e: Event) {
|
||||
scrollTop = (e.currentTarget as HTMLDivElement).scrollTop;
|
||||
// Track selection state for expanded buffer
|
||||
hasSelection = !document.getSelection()?.isCollapsed;
|
||||
}
|
||||
@@ -105,7 +122,7 @@
|
||||
// ResizeObserver for container dimensions
|
||||
$effect(() => {
|
||||
if (!containerEl) return;
|
||||
const ro = new ResizeObserver(entries => {
|
||||
const ro = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
containerHeight = entry.contentRect.height;
|
||||
containerWidth = entry.contentRect.width;
|
||||
@@ -119,7 +136,7 @@
|
||||
$effect(() => {
|
||||
if (activeSegmentIdx >= 0) {
|
||||
// Find the index in our segments array that matches
|
||||
const arrayIdx = segments.findIndex(s => s._idx === activeSegmentIdx);
|
||||
const arrayIdx = segments.findIndex((s: ViewerSegment) => s._idx === activeSegmentIdx);
|
||||
if (arrayIdx >= 0) {
|
||||
const segTop = cumulativeOffsets[arrayIdx];
|
||||
const segBottom = segTop + segmentHeights[arrayIdx];
|
||||
|
||||
Reference in New Issue
Block a user