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

@ -1,13 +1,15 @@
// Store art editor (Store art tab): the order of the screenshot scenes (the store shows them in this order) and the
// raw frame each scene shows (replace a screenshot by pointing the scene at another frame). Saved into the config;
// then render, and upload to each store with the dry run in the window (uploads replace the store's sets).
import '../../styles/art.css';
import { useState } from 'react';
import { api, errorText, type EditableResult, type Project } from '../../api';
import { useData, useWrite } from '../../store';
import { t } from '../../i18n';
import { ApplyPanel, EditBar, TextField, useEdits } from '../../ui/Editor';
import { Icon, StoreGlyph } from '../../ui/icons';
import { Card, Empty } from '../../ui/kit';
import { Empty } from '../../ui/kit';
import { Section } from '../../ui/ds';
import { keyOf } from '../../lib/edits';
type Scene = { id: string; palette?: unknown; frames?: { src: string }[] } & Record<string, unknown>;
@ -33,7 +35,7 @@ export function ArtEditor({ project, data, onSaved }: { project: Project; data:
const p = e.get(project.id, keyOf(op));
const scenes = (p && p.op.kind === 'config' ? p.op.value : saved) as Scene[];
const unsaved = e.count(project.id, 'art') ? t('ed.saveFirst') : null;
if (!saved.length) return <Card title={t('ed.artTitle')} icon="image"><Empty compact illo="text" title={t('ed.artEmpty')}>{t('ed.artEmptyText')}</Empty></Card>;
if (!saved.length) return <Section className="scr-art" title={t('ed.artTitle')} icon="image"><Empty compact illo="text" title={t('ed.artEmpty')}>{t('ed.artEmptyText')}</Empty></Section>;
const move = (i: number, d: -1 | 1) => {
const next = [...scenes];
const j = i + d;
@ -42,8 +44,8 @@ export function ArtEditor({ project, data, onSaved }: { project: Project; data:
e.set(project.id, { ...op, value: next }, 'art', saved);
};
return (
<div className="stack" id="art-editor" style={{ gap: 16 }}>
<Card title={t('ed.artTitle')} icon="image" sub={t('ed.artSub')} pad={false}
<div className="stack scr-art art-editor" id="art-editor" style={{ gap: 16 }}>
<Section title={t('ed.artTitle')} icon="image" sub={t('ed.artSub')} pad={false}
actions={langs.length > 1 ? <label className="row small" style={{ gap: 6 }}>{t('ed.frameLang')}<select value={fl} onChange={ev => setFrameLang(ev.target.value)} data-frame-lang>{langs.map(l => <option key={l} value={l}>{l}</option>)}</select></label> : undefined}>
<ol className="ed-scenes">
{scenes.map((s, i) => (
@ -61,16 +63,16 @@ export function ArtEditor({ project, data, onSaved }: { project: Project; data:
</li>
))}
</ol>
</Card>
</Section>
{data.shotsCopy && <Captions project={project} data={data} scenes={scenes.map(s => s.id)} />}
<EditBar project={project} area="art" data={data} files={[data.files.config, data.files.shotsCopy ?? null]} onSaved={onSaved} />
<Card title={t('ed.applyTitle')} icon="upload" sub={t('ed.applyArtSub')}>
<Section title={t('ed.applyTitle')} icon="upload" sub={t('ed.applyArtSub')}>
<div className="act-grid">
<div className="act-col"><div className="act-head"><Icon name="wand" />{t('act.render')}</div><ApplyPanel project={project} method="shots.render" label={t('act.renderShots')} done={t('act.rendered')} blocked={unsaved} /></div>
{data.play && <div className="act-col"><div className="act-head"><StoreGlyph store="android" />Google Play</div><ApplyPanel project={project} method="play.images" label={t('act.playImages')} done={t('act.uploaded')} blocked={unsaved} /></div>}
{data.apple && <div className="act-col"><div className="act-head"><StoreGlyph store="ios" />App Store</div><ApplyPanel project={project} method="apple.screenshots" label={t('act.appleShots')} done={t('act.uploaded')} blocked={unsaved} /><ApplyPanel project={project} method="apple.previews" label={t('act.applePreviews')} done={t('act.uploaded')} blocked={unsaved} /></div>}
</div>
</Card>
</Section>
</div>
);
}
@ -82,7 +84,7 @@ function Captions({ project, data, scenes }: { project: Project; data: EditableR
const [lang, setLang] = useState(langs[0] ?? '');
const L = langs.includes(lang) ? lang : langs[0] ?? '';
return (
<Card title={t('ed.captionsTitle')} icon="text" sub={t('ed.captionsSub', { file: data.files.shotsCopy ?? '' })} id="captions-editor">
<Section title={t('ed.captionsTitle')} icon="text" sub={t('ed.captionsSub', { file: data.files.shotsCopy ?? '' })} id="captions-editor">
<div className="row" style={{ gap: 8, flexWrap: 'wrap', marginBottom: 12 }} role="tablist" aria-label={t('col.language')}>
{langs.map(x => <button key={x} role="tab" aria-selected={x === L} className={`btn btn-sm${x === L ? ' btn-primary' : ''}`} onClick={() => setLang(x)}><Icon name="globe" />{x}</button>)}
</div>
@ -103,6 +105,6 @@ function Captions({ project, data, scenes }: { project: Project; data: EditableR
);
})}
</div>
</Card>
</Section>
);
}