// 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 { Empty } from '../../ui/kit'; import { Section } from '../../ui/ds'; import { keyOf } from '../../lib/edits'; type Scene = { id: string; palette?: unknown; frames?: { src: string }[] } & Record; export function ArtEditor({ project, data, onSaved }: { project: Project; data: EditableResult; onSaved: () => void }) { const e = useEdits(); const w = useWrite(project.id); const d = useData(); const langs = data.languages.map(l => l.id); const [frameLang, setFrameLang] = useState(langs[0] ?? ''); const fl = langs.includes(frameLang) ? frameLang : langs[0] ?? ''; const framesDir = ((data.config as unknown as { shots?: { frames?: string } | null }).shots?.frames ?? 'store/shots/frames').replace(/\/+$/, ''); // replace the frame a scene shows (this language, phone) with a file the owner picks: native open dialog, then // art.replace (dry run with the checks, the native dialog, the copy into the app folder); render again afterwards const replaceFrame = async (src: string) => { let from: string | null = null; try { from = await api.pickImage(project.id); } catch (err) { d.toast({ kind: 'bad', title: t('art.replace'), detail: errorText(err) }); return; } if (!from) return; await w.run({ method: 'art.replace', args: { from, to: `${framesDir}/${fl}/phone/${src}` }, title: t('art.replaceTitle', { name: src }), done: t('art.replaced') }); }; const saved = ((data.config as unknown as { shots?: { scenes?: Scene[] } | null }).shots?.scenes ?? []) as Scene[]; const op = { kind: 'config' as const, path: 'shots.scenes' }; 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
{t('ed.artEmptyText')}
; const move = (i: number, d: -1 | 1) => { const next = [...scenes]; const j = i + d; if (j < 0 || j >= next.length) return; [next[i], next[j]] = [next[j]!, next[i]!]; e.set(project.id, { ...op, value: next }, 'art', saved); }; return (
1 ? : undefined}>
    {scenes.map((s, i) => (
  1. {i + 1} {s.id}
    x.id === s.id)?.frames?.[0]?.src ?? ''} label={t('ed.frame')} mono />
    {s.frames?.[0]?.src && }
  2. ))}
{data.shotsCopy && s.id)} />}
{t('act.render')}
{data.play &&
Google Play
} {data.apple &&
App Store
}
); } /** Screenshot captions per language and scene (the shots copy file): eyebrow, the two title lines, the subline. */ function Captions({ project, data, scenes }: { project: Project; data: EditableResult; scenes: string[] }) { const copy = data.shotsCopy ?? {}; const langs = data.languages.map(l => l.id); const [lang, setLang] = useState(langs[0] ?? ''); const L = langs.includes(lang) ? lang : langs[0] ?? ''; return (
{langs.map(x => )}
{scenes.map(id => { const c = copy[L]?.[id] ?? {}; const title = Array.isArray(c.title) ? c.title : []; return (
{id}
); })}
); }