import { Link, useFocusEffect, useRouter } from 'expo-router'; import { useCallback, useState } from 'react'; import { ActivityIndicator, Pressable, ScrollView, StyleSheet, TextInput, View, } from 'react-native'; import { PreCaptureSheet, type PreCaptureResult } from '@/components/PreCaptureSheet'; 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 type { TranscriptMeta } from '@/lib/db'; import { formatClock } from '@/lib/format'; import { pickAudio, type PickedAudio } from '@/lib/pickAudio'; import { useCourses } from '@/stores/coursesStore'; import { useTranscribe } from '@/stores/transcribeStore'; import { useTranscripts } from '@/stores/transcriptsStore'; export default function LibraryScreen() { const theme = useTheme(); const router = useRouter(); const { items, loading, query, setQuery, refresh, remove, courseFilter, setCourseFilter } = useTranscripts(); const courses = useCourses((s) => s.items); const refreshCourses = useCourses((s) => s.refresh); const job = useTranscribe(); const [picked, setPicked] = useState(null); useFocusEffect( useCallback(() => { void refresh(); void refreshCourses(); }, [refresh, refreshCourses]), ); const busy = job.status === 'loading' || job.status === 'transcribing'; const onNew = useCallback(async () => { if (busy) return; const p = await pickAudio(); if (p) setPicked(p); }, [busy]); const onStart = useCallback( async (r: PreCaptureResult) => { const p = picked; setPicked(null); if (!p) return; const id = await useTranscribe.getState().start(p, r); if (id) router.push({ pathname: '/transcript/[id]', params: { id } }); }, [picked, router], ); const courseName = (cid?: string | null) => cid ? courses.find((c) => c.id === cid)?.name ?? 'Course' : null; return ( On your device — nothing uploaded. Courses › [ styles.captureBtn, { backgroundColor: '#e5484d', opacity: busy ? 0.5 : pressed ? 0.85 : 1 }, ]}> 🎙 Record [ styles.captureBtn, { backgroundColor: '#3c87f7', opacity: busy ? 0.5 : pressed ? 0.85 : 1 }, ]}> + Import {busy && } {job.status === 'error' && ( Transcription failed {job.error} )} {courses.length > 0 && ( void setCourseFilter('all')} /> void setCourseFilter(null)} /> {courses.map((c) => ( void setCourseFilter(c.id)} /> ))} )} void setQuery(t)} placeholder="Filter by title…" placeholderTextColor={theme.textSecondary} style={[styles.search, { color: theme.text, backgroundColor: theme.backgroundElement }]} /> {loading && items.length === 0 ? ( ) : items.length === 0 ? ( {query ? 'No matches.' : 'No lectures yet. Record one or import an audio/video file to begin.'} ) : ( items.map((t) => ( router.push({ pathname: '/transcript/[id]', params: { id: t.id } })} onDelete={() => void remove(t.id)} /> )) )} void onStart(r)} onCancel={() => setPicked(null)} /> ); } function FilterChip({ label, active, onPress }: { label: string; active: boolean; onPress: () => void }) { const theme = useTheme(); return ( {label} ); } function ActiveJob() { const { stage, progress, partial, chunkIndex, chunkCount, cancel } = useTranscribe(); const pct = Math.round(progress * 100); const transcribing = stage === 'transcribing'; // While transcribing, show "chunk i/N" so a long file's progress is legible // and it's obvious work is happening between the (discrete) per-chunk updates. const label = stage === 'loading' ? `Loading model… ${pct}%` : `Transcribing… ${pct}%`; const sub = transcribing && chunkCount ? `part ${Math.min((chunkIndex ?? 0) + (pct < 100 ? 1 : 0), chunkCount)} of ${chunkCount}` : undefined; return ( {label} Cancel {sub && ( {sub} )} {partial.length > 0 && ( {partial.map((s) => s.text).join(' ')} )} ); } function ProgressBar({ value }: { value: number }) { return ( ); } function TranscriptRow({ item, courseName, onOpen, onDelete, }: { item: TranscriptMeta; courseName: string | null; onOpen: () => void; onDelete: () => void; }) { const date = new Date(item.lectureDate ?? item.createdAt).toLocaleDateString(); return ( [pressed && styles.pressed]}> {item.title} {courseName ? `${courseName} · ` : ''} {date} · {formatClock(item.durationSec)} · {item.segmentCount} segments ); } const styles = StyleSheet.create({ fill: { flex: 1 }, content: { padding: Spacing.three, gap: Spacing.three, maxWidth: MaxContentWidth, width: '100%', alignSelf: 'center', paddingBottom: Spacing.six, }, flex: { flex: 1 }, subHeader: { flexDirection: 'row', alignItems: 'center', gap: Spacing.two }, captureRow: { flexDirection: 'row', gap: Spacing.two }, captureBtn: { flex: 1, paddingVertical: Spacing.three, borderRadius: Spacing.three, alignItems: 'center' }, captureText: { color: '#fff', fontWeight: '700', fontSize: 16 }, card: { padding: Spacing.three, borderRadius: Spacing.three, gap: Spacing.two }, rowBetween: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: Spacing.two }, jobTitleRow: { flexDirection: 'row', alignItems: 'center', gap: Spacing.two, flex: 1 }, filterBar: { gap: Spacing.two, paddingVertical: Spacing.one, paddingRight: Spacing.three }, filterChip: { paddingHorizontal: Spacing.three, paddingVertical: Spacing.two, borderRadius: 999 }, chipActive: { color: '#fff', fontWeight: '700' }, search: { borderRadius: Spacing.two, paddingHorizontal: Spacing.three, paddingVertical: Spacing.three, fontSize: 15 }, track: { height: 6, borderRadius: 3, backgroundColor: '#88888833', overflow: 'hidden' }, bar: { height: 6, borderRadius: 3, backgroundColor: '#3c87f7' }, pad: { paddingVertical: Spacing.four, textAlign: 'center' }, pressed: { opacity: 0.7 }, });