import { useEffect, useState } from 'react'; import type { VideoSummary } from '../../shared/types.ts'; import { navigate } from '../App.tsx'; import { api } from '../api.ts'; import { Icon } from '../components/Icon.tsx'; import { ConfirmDialog, Modal, type ConfirmRequest } from '../components/Modal.tsx'; import { fmtDate, fmtDuration } from '../lib/format.ts'; export function Library() { const [videos, setVideos] = useState(null); const [error, setError] = useState(null); const [filter, setFilter] = useState(''); const [dragId, setDragId] = useState(null); const [confirm, setConfirm] = useState(null); const [creating, setCreating] = useState(false); const load = () => api .videos() .then(setVideos) .catch((e) => setError(e.message)); useEffect(() => { document.title = 'Takes'; load(); }, []); const q = filter.trim().toLowerCase(); const shown = (videos ?? []).filter((v) => !q || v.title.toLowerCase().includes(q) || v.description.toLowerCase().includes(q)); const onDragOver = (overId: string) => { if (!dragId || dragId === overId || !videos) return; const from = videos.findIndex((v) => v.id === dragId); const to = videos.findIndex((v) => v.id === overId); const next = videos.slice(); const [item] = next.splice(from, 1); next.splice(to, 0, item); setVideos(next); }; const onDragEnd = () => { setDragId(null); if (videos) api.setOrder(videos.map((v) => v.id)).catch((e) => setError(e.message)); }; const askDelete = (v: VideoSummary) => setConfirm({ title: 'Delete video?', message: ( <> “{v.title}” and all of its takes will be moved to the .trash folder in your library directory. ), confirmLabel: 'Delete', danger: true, onConfirm: async () => { try { await api.deleteVideo(v.id); } catch (e) { setError((e as Error).message); } load(); }, }); return (
Takes
setFilter(e.target.value)} />
Settings
{error && (
setError(null)}> {error}
)}
{videos && videos.length === 0 && (

No videos yet

Create a video, then record yourself, your screen, or both.

)}
{shown.map((v) => (
{ e.dataTransfer.effectAllowed = 'move'; setDragId(v.id); }} onDragOver={(e) => { e.preventDefault(); onDragOver(v.id); }} onDragEnd={onDragEnd} onClick={() => navigate(`video/${v.id}`)} >
{v.thumbnailUrl ? :
} {v.duration > 0 && {fmtDuration(v.duration)}} {v.hasCamera && } {v.hasScreen && }
{v.title}
{v.description &&
{v.description}
}
{fmtDate(v.createdAt)} {v.takeCount} take{v.takeCount === 1 ? '' : 's'} {v.exportState === 'current' && exported} {v.exportState === 'stale' && export outdated}
e.stopPropagation()}>
{!q && (
)}
))}
{creating && setCreating(false)} />} {confirm && setConfirm(null)} />}
); } function NewVideoDialog({ onClose }: { onClose: () => void }) { const [title, setTitle] = useState(''); const [description, setDescription] = useState(''); const [notes, setNotes] = useState(''); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const create = async (record: boolean) => { setBusy(true); try { const v = await api.createVideo({ title, description, notes }); navigate(record ? `video/${v.id}/record` : `video/${v.id}`); } catch (e) { setError((e as Error).message); setBusy(false); } }; return ( } >