import * as Linking from 'expo-linking'; import { Link, Stack, useLocalSearchParams } from 'expo-router'; import { useEffect, useMemo, useRef, useState } from 'react'; import { ActivityIndicator, Platform, Pressable, ScrollView, StyleSheet, TextInput, View, } from 'react-native'; import { ThemedText } from '@/components/themed-text'; import { ThemedView } from '@/components/themed-view'; import { MaxContentWidth, Spacing } from '@/constants/theme'; import { useTheme } from '@/hooks/use-theme'; import { getRepo, type Transcript } from '@/lib/db'; import { downloadText } from '@/lib/download'; import { detectCitations, detectEvents, lookupLinksFor, resolveCitation, toIcs, type CandidateEvent, type Citation, type LookupLinks, type RefMetadata, } from '@/lib/enrich'; import { EXPORT_META, formatTranscript, type ExportFormat } from '@/lib/export'; import { formatClock } from '@/lib/format'; import { cardsFromGlossary, glossary, summarize, type GlossaryEntry, type SourcedSentence, } from '@/lib/learn'; import type { Segment } from '@/lib/types'; import { useTranscribe } from '@/stores/transcribeStore'; const ACCENT = '#3c87f7'; export default function TranscriptScreen() { const theme = useTheme(); const { id, t } = useLocalSearchParams<{ id: string; t?: string }>(); // Deep-link target time (seconds) from a search hit, if any. const jumpTo = useMemo(() => { const n = Number(t); return Number.isFinite(n) && n >= 0 ? n : null; }, [t]); const [transcript, setTranscript] = useState(undefined); const [title, setTitle] = useState(''); const [segments, setSegments] = useState([]); const [dirty, setDirty] = useState(false); const [saving, setSaving] = useState(false); const [currentTime, setCurrentTime] = useState(0); // Study aids (deterministic, computed on demand — no storage, no model). const [aids, setAids] = useState<{ summary: SourcedSentence[]; glossary: GlossaryEntry[]; } | null>(null); const [creatingCards, setCreatingCards] = useState(false); const [cardsAdded, setCardsAdded] = useState(null); // Dates & references (Phase 4) — detected on demand, nothing persisted. // `enrichment === null` means "not scanned yet". const [enrichment, setEnrichment] = useState<{ events: CandidateEvent[]; citations: Citation[]; } | null>(null); // Per-citation resolved metadata + in-flight flags, keyed by list index. const [refMeta, setRefMeta] = useState>({}); const [refLoading, setRefLoading] = useState>({}); const scrollRef = useRef(null); // Y offset of each rendered segment row, captured via onLayout, for scroll-to. const segmentYs = useRef>({}); // Ensure we only auto-jump once per (id, t) deep-link. const jumpedRef = useRef(false); // Prefer the just-transcribed in-session audio; otherwise load the persisted // source media so playback/scrub works after a reload (ROADMAP Phase 0). const sessionAudioUrl = useTranscribe((s) => (s.lastTranscriptId === id ? s.audioUrl : undefined)); const [persistedUrl, setPersistedUrl] = useState(undefined); const audioUrl = sessionAudioUrl ?? persistedUrl; const audioRef = useRef(null); useEffect(() => { if (sessionAudioUrl) return; // already have in-session audio let url: string | undefined; let alive = true; void getRepo() .getMediaUrl(id) .then((u) => { if (!alive) return; url = u; setPersistedUrl(u); }); return () => { alive = false; if (url && Platform.OS === 'web' && url.startsWith('blob:')) URL.revokeObjectURL(url); }; }, [id, sessionAudioUrl]); useEffect(() => { let alive = true; void getRepo() .get(id) .then((t) => { if (!alive) return; setTranscript(t ?? null); setTitle(t?.title ?? ''); setSegments(t?.segments ?? []); }); return () => { alive = false; }; }, [id]); // Web-only