// Dependencies: local tools for screenshots, previews, icons and captures. ffmpeg and Chrome for Testing download // into the app data folder in one click (checksum or signature verified, progress, cancel); the rest link out. // Store work needs none of them. Sources and licenses: docs/desktop.md. import { useCallback, useEffect, useState } from 'react'; import { type AndroidSdkLookup, api, type DownloadInfo, errorText, type LocalTools, newJob, onDownload, type ToolStatus } from '../api'; import { t } from '../i18n'; import { bytes } from '../lib/format'; import { shortPath } from '../paths'; import { useData } from '../store'; import { Icon, type IconName } from '../ui/icons'; import { Card, Chip, Notice, Skeleton } from '../ui/kit'; const TOOL_ICON: Record = { ffmpeg: 'image', ffprobe: 'search', chrome: 'globe', xcode: 'terminal', simctl: 'phone', adb: 'phone', git: 'layers' }; /** Status chip of one tool. */ export function toolState(x: ToolStatus): { text: string; tone: 'ok' | 'warn' | 'accent' } { if (!x.found) return { text: t('deps.missing'), tone: 'warn' }; return x.source === 'app' ? { text: t('deps.appCopy'), tone: 'accent' } : { text: t('deps.installed'), tone: 'ok' }; } interface Dl { job: string; stage: string; received: number; total: number | null } export function Dependencies() { const d = useData(); const [tools, setTools] = useState(d.sidecar?.tools?.tools ?? null); const [sdk, setSdk] = useState(d.sidecar?.tools?.androidSdk ?? null); const [info, setInfo] = useState([]); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); const [dl, setDl] = useState>({}); const refresh = useCallback(async () => { setBusy(true); setError(null); try { const [s, i] = await Promise.all([api.call('system.tools'), api.toolsInfo()]); setTools(s.tools); setInfo(i); setSdk(s.androidSdk ?? null); } catch (e) { setError(errorText(e)); } finally { setBusy(false); } }, []); useEffect(() => { refresh(); }, [refresh]); useEffect(() => { const un = onDownload(e => setDl(m => { const k = Object.keys(m).find(x => m[x]!.job === e.job); return k ? { ...m, [k]: { ...m[k]!, stage: e.stage, received: e.received, total: e.total } } : m; })); return () => { un.then(f => f()); }; }, []); const install = async (tool: 'ffmpeg' | 'chrome') => { const job = newJob(`dl-${tool}`); setDl(m => ({ ...m, [tool]: { job, stage: 'resolve', received: 0, total: null } })); const tid = d.toast({ id: job, kind: 'job', title: t('deps.downloading', { tool: tool === 'chrome' ? 'Chrome for Testing' : 'ffmpeg' }), detail: t('dl.resolve'), progress: null, job }); try { const v = await api.installTool(tool, job); d.updateToast(tid, { kind: 'ok', title: t('deps.installedToast', { tool: tool === 'chrome' ? 'Chrome for Testing' : 'ffmpeg' }), detail: v, progress: undefined, job: undefined }); } catch (e) { const msg = errorText(e); d.updateToast(tid, { kind: msg === 'cancelled' ? 'info' : 'bad', title: msg === 'cancelled' ? t('toast.cancelled') : t('deps.failed'), detail: msg === 'cancelled' ? t('deps.cancelledDetail') : msg, progress: undefined, job: undefined }); } finally { setDl(m => { const { [tool]: _gone, ...rest } = m; return rest; }); refresh(); } }; const uninstall = async (tool: 'ffmpeg' | 'chrome') => { try { await api.uninstallTool(tool); d.toast({ kind: 'ok', title: t('deps.removed') }); } catch (e) { setError(errorText(e)); } refresh(); }; const open = (url: string) => api.openExternal(url).catch(e => setError(errorText(e))); const copy = (s: string) => { navigator.clipboard?.writeText(s).then(() => d.toast({ kind: 'ok', title: t('deps.copied') }), () => {}); }; const found = tools?.filter(x => x.found).length ?? 0; const managed = (id: string): DownloadInfo | undefined => info.find(i => i.tool === (id === 'ffprobe' ? 'ffmpeg' : id)); return (

{t('nav.dependencies')}

{t('deps.lead')}

{error && {error}} {!tools ?
: (
{tools.map(x => { const st = toolState(x); const m = x.install.kind === 'download' ? managed(x.id) : undefined; const running = m ? dl[m.tool] : undefined; const pct = running?.total ? running.received / running.total : null; return (
{x.name}{st.text}{x.version && {x.version}} {x.usedFor} {x.path && {shortPath(x.path, d.info?.home)}}
{running ? (
{t(`dl.${running.stage}` as 'dl.download')}{running.total ? `${bytes(running.received)} / ${bytes(running.total)}` : ''}
) : null} {running ? : m && x.id !== 'ffprobe' ? ( m.available ? ( m.installed ? : ) : {t('deps.unavailable')} ) : x.install.kind === 'link' && !x.found ? : null}
); })}
)}
{sdk && ( {t('deps.installed')} : <>{t('deps.missing')}}> {sdk.path ? (
{t('deps.sdkPath')}
{shortPath(sdk.path, d.info?.home)}
{t('deps.sdkFrom')}
{sdk.source}
) : (
{t('deps.sdkNone')}
    {sdk.checked.map((c, i) =>
  • {d.info?.home ? c.split(d.info.home).join("~") : c}
  • )}
)}
)}
{info.map(m => (
{t('deps.source')}
{m.source}
{t('deps.license')}
{m.license}
{t('deps.verify')}
{m.tool === 'chrome' ? t('deps.verifyChrome') : t('deps.verifyFfmpeg')}
))}
{['storebound doctor', 'bun add -d playwright', 'brew install ffmpeg'].map(c => (
{c}
))}
); }