Desktop v2 screens: releases (store cards, stage stepper, rollout ring), submit checklist, TestFlight cards; listings as store pages with completeness, framed screenshots with issue badges, product cards, price matrix with gap shading; health tiles and issue cards, store-style review feed with rating distribution, growth funnel, readiness score with fix-first, tools grid, store-info sheet; settings, accounts, folders, dependencies and onboarding in the v2 language

This commit is contained in:
CRTkafa 2026-10-22 23:31:22 +08:00
parent ce939ee063
commit f131de9df2
47 changed files with 2309 additions and 927 deletions

View file

@ -8,7 +8,9 @@ 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';
import { Chip, Notice, Skeleton } from '../ui/kit';
import { PageHeader, Section, StatTile, StatusPill } from '../ui/ds';
import '../styles/deps.css';
const TOOL_ICON: Record<string, IconName> = { ffmpeg: 'image', ffprobe: 'search', chrome: 'globe', xcode: 'terminal', simctl: 'phone', adb: 'phone', git: 'layers' };
@ -72,29 +74,32 @@ export function Dependencies() {
const managed = (id: string): DownloadInfo | undefined => info.find(i => i.tool === (id === 'ffprobe' ? 'ffmpeg' : id));
return (
<div className="page">
<div className="ph">
<div><h1>{t('nav.dependencies')}</h1><p>{t('deps.lead')}</p></div>
<div className="actions"><button className="btn" onClick={refresh} disabled={busy}><Icon name="refresh" className={busy ? 'spin' : undefined} />{t('deps.checkAgain')}</button></div>
</div>
<div className="page scr-deps">
<PageHeader icon="package" eyebrow={t('v2cfg.workspace')} title={t('nav.dependencies')} subtitle={t('deps.lead')} actions={<><button className="btn" onClick={refresh} disabled={busy}><Icon name="refresh" className={busy ? 'spin' : undefined} />{t('deps.checkAgain')}</button></>} />
{error && <Notice tone="bad">{error}</Notice>}
<Card title={t('deps.tools')} icon="package" sub={tools ? t('deps.found', { found, total: tools.length }) : t('common.loading')} pad={false}>
{tools && <div className="ds-grid deps-kpis">
<StatTile icon="check" tone={found === tools.length ? 'ok' : undefined} label={t('v2cfg.depsFound')} value={`${found} / ${tools.length}`} />
<StatTile icon="alert" tone={tools.some(x => !x.found || x.status === 'outdated') ? 'warn' : undefined} label={t('v2cfg.depsNeeds')} value={tools.filter(x => !x.found || x.status === 'outdated').length} />
<StatTile icon="download" label={t('v2cfg.depsInApp')} value={info.filter(i => i.installed).length} sub={t('v2cfg.depsInAppSub')} />
</div>}
<Section title={t('deps.tools')} icon="package" sub={tools ? t('deps.found', { found, total: tools.length }) : t('common.loading')} pad={false}>
{!tools ? <div className="card-body"><Skeleton lines={6} /></div> : (
<div className="list">
<div className="list deps-grid">
{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 (
<div className="li tool-row" key={x.id} style={{ alignItems: 'center' }}>
<div className={`li tool-row tool-${st.tone}`} key={x.id}>
<span className="ti"><Icon name={TOOL_ICON[x.id] ?? 'package'} /></span>
<div className="li-main">
<span className="row" style={{ gap: 8 }}><span className="li-title">{x.name}</span><Chip tone={st.tone}>{st.text}</Chip>{x.version && <span className="small muted num">{x.version}</span>}</span>
<span className="row" style={{ gap: 8 }}><span className="li-title">{x.name}</span></span>
<span className="row tool-chips" style={{ gap: 6 }}><StatusPill tone={st.tone === 'accent' ? 'info' : st.tone}>{st.text}</StatusPill>{x.version && <Chip tone="outline">{x.version}</Chip>}{x.required && <Chip tone="neutral">{t('v2cfg.needs', { version: x.required })}</Chip>}</span>
<span className="li-sub">{x.usedFor}</span>
{x.path && <span className="li-sub mono-id selectable" title={x.path}>{shortPath(x.path, d.info?.home)}</span>}
</div>
<div className="actions" style={{ flexWrap: 'nowrap' }}>
<div className="actions tool-acts">
{running ? (
<div className="prog" aria-live="polite">
<div className={`bar${pct === null ? ' indet' : ''}`}><span style={{ width: `${(pct ?? 0.35) * 100}%` }} /></div>
@ -114,9 +119,9 @@ export function Dependencies() {
})}
</div>
)}
</Card>
</Section>
{sdk && (
<Card title={t('deps.sdkTitle')} icon="phone" sub={t('deps.sdkSub')} id="android-sdk"
<Section title={t('deps.sdkTitle')} icon="phone" sub={t('deps.sdkSub')} id="android-sdk"
actions={sdk.path ? <Chip tone="ok">{t('deps.installed')}</Chip> : <><Chip tone="warn">{t('deps.missing')}</Chip><button className="btn btn-sm" onClick={() => open('https://developer.android.com/studio')}><Icon name="external" />{t('deps.getIt')}</button></>}>
{sdk.path ? (
<dl className="kv">
@ -129,27 +134,27 @@ export function Dependencies() {
<ul className="note-list small">{sdk.checked.map((c, i) => <li key={i} className="selectable">{d.info?.home ? c.split(d.info.home).join("~") : c}</li>)}</ul>
</div>
)}
</Card>
</Section>
)}
<div className="grid g2">
<div className="grid g2 deps-src">
{info.map(m => (
<Card key={m.tool} title={m.tool === 'chrome' ? 'Chrome for Testing' : `ffmpeg ${m.version ?? ''}`} icon={m.tool === 'chrome' ? 'globe' : 'image'} sub={m.installed ? t('deps.inAppData', { folder: m.folder }) : t('deps.notDownloaded')}>
<Section key={m.tool} title={m.tool === 'chrome' ? 'Chrome for Testing' : `ffmpeg ${m.version ?? ''}`} icon={m.tool === 'chrome' ? 'globe' : 'image'} sub={m.installed ? t('deps.inAppData', { folder: m.folder }) : t('deps.notDownloaded')}>
<dl className="kv">
<dt>{t('deps.source')}</dt><dd className="selectable small">{m.source}</dd>
<dt>{t('deps.license')}</dt><dd className="small">{m.license}</dd>
<dt>{t('deps.verify')}</dt><dd className="small">{m.tool === 'chrome' ? t('deps.verifyChrome') : t('deps.verifyFfmpeg')}</dd>
</dl>
<div className="actions" style={{ marginTop: 12 }}><button className="btn btn-sm btn-ghost" onClick={() => open(m.licenseUrl)}><Icon name="external" />{t('deps.licenseLink')}</button></div>
</Card>
</Section>
))}
</div>
<Card title={t('deps.terminal')} icon="terminal" sub={t('deps.terminalSub')}>
<Section title={t('deps.terminal')} icon="terminal" sub={t('deps.terminalSub')}>
<div className="stack">
{['storebound doctor', 'bun add -d playwright', 'brew install ffmpeg'].map(c => (
<div className="cmd" key={c}><code className="selectable">{c}</code><button className="btn btn-sm btn-ghost btn-icon" onClick={() => copy(c)} aria-label={t('deps.copy', { cmd: c })}><Icon name="copy" /></button></div>
))}
</div>
</Card>
</Section>
</div>
);
}