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:
parent
ce939ee063
commit
f131de9df2
47 changed files with 2309 additions and 927 deletions
|
|
@ -1,7 +1,7 @@
|
|||
// Workspace: app folders (one storebound.config.ts per app), which store accounts each app uses, and starting a new
|
||||
// folder from the templates (`storebound init` with a profile, confirmed in a native dialog).
|
||||
import { failureText } from '../lib/results';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { type ReactNode, useEffect, useRef, useState } from 'react';
|
||||
import { api, errorText, newJob, onProgress, type Project, type ToolResult, type WriteOutcome } from '../api';
|
||||
import { devSwitch } from '../dev/switches';
|
||||
import { t } from '../i18n';
|
||||
|
|
@ -10,12 +10,20 @@ import { useNav } from '../nav';
|
|||
import { useData } from '../store';
|
||||
import { Icon, StoreGlyph } from '../ui/icons';
|
||||
import { useFolderActions } from '../ui/FolderMissing';
|
||||
import { AppIcon, Card, Chip, Empty, Notice, Seg, StoreBadge } from '../ui/kit';
|
||||
import { AppIcon, Empty, Notice, Seg, StoreBadge } from '../ui/kit';
|
||||
import { PageHeader, Section, StatusPill, tintStyle, useTint } from '../ui/ds';
|
||||
import '../styles/folders.css';
|
||||
import { checkState } from './Portfolio';
|
||||
|
||||
type Profile = 'app' | 'game' | 'subscription-app';
|
||||
interface Run { title: string; project: string; job: string; lines: string[]; result?: ToolResult; outcome?: WriteOutcome; error?: string }
|
||||
|
||||
/** Folder card wrapper carrying the app's tint (hooks cannot run inside the list map). */
|
||||
function Tinted({ src, name, children }: { src: string | null | undefined; name: string; children: ReactNode }) {
|
||||
const hue = useTint(src, name);
|
||||
return <div className="li fold-card" style={tintStyle(hue)}>{children}</div>;
|
||||
}
|
||||
|
||||
function LocateButton({ project }: { project: Project }) {
|
||||
const { busy, locate } = useFolderActions(project);
|
||||
return <button className="btn btn-sm btn-primary" onClick={locate} disabled={busy} title={t('folder.locateHint')}><Icon name="search" />{t('folder.locate')}</button>;
|
||||
|
|
@ -73,41 +81,36 @@ export function AppFolders() {
|
|||
: run.result.ok ? t('run.done') : failureText(run.result);
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<div className="ph">
|
||||
<div><h1>{t('nav.apps')}</h1><p>{t('folders.lead')}</p></div>
|
||||
<div className="actions">
|
||||
<button className="btn" onClick={fastlane} disabled={!!busy} title={t('folders.fastlaneHint')}><Icon name="download" />{t('folders.fastlane')}</button>
|
||||
<div className="page scr-folders">
|
||||
<PageHeader icon="folder" eyebrow={t('v2cfg.workspace')} title={t('nav.apps')} subtitle={t('folders.lead')} actions={<><button className="btn" onClick={fastlane} disabled={!!busy} title={t('folders.fastlaneHint')}><Icon name="download" />{t('folders.fastlane')}</button>
|
||||
<button className="btn" onClick={init} disabled={!!busy}><Icon name="sparkle" />{t('folders.startShort')}</button>
|
||||
<button className="btn btn-primary" onClick={add} disabled={!!busy}><Icon name="folderPlus" />{t('action.addFolder')}</button>
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn btn-primary" onClick={add} disabled={!!busy}><Icon name="folderPlus" />{t('action.addFolder')}</button></>} />
|
||||
{error && <Notice tone="bad">{error}</Notice>}
|
||||
|
||||
{!d.projects.length ? (
|
||||
<Card pad={false}><Empty illo="apps" title={t('folders.emptyTitle')} actions={<>
|
||||
<Section pad={false}><Empty illo="apps" title={t('folders.emptyTitle')} actions={<>
|
||||
<button className="btn" onClick={fastlane} disabled={!!busy}><Icon name="download" />{t('folders.fastlane')}</button>
|
||||
<button className="btn" onClick={init} disabled={!!busy}><Icon name="sparkle" />{t('folders.startShort')}</button>
|
||||
<button className="btn btn-primary" onClick={add} disabled={!!busy}><Icon name="folderPlus" />{t('action.addFolder')}</button>
|
||||
</>}>{t('folders.emptyText')}</Empty></Card>
|
||||
</>}>{t('folders.emptyText')}</Empty></Section>
|
||||
) : (
|
||||
<Card title={t('folders.list')} icon="folder" sub={t('folders.listSub', { count: d.projects.length })} pad={false}>
|
||||
<Section title={t('folders.list')} icon="folder" sub={t('folders.listSub', { count: d.projects.length })} pad={false}>
|
||||
<div className="list">
|
||||
{d.projects.map(p => {
|
||||
const e = d.statusOf(p);
|
||||
const st = p.missing ? { text: t('folder.missingShort'), tone: 'bad' as const } : checkState(e);
|
||||
return (
|
||||
<div className="li" key={p.id} style={{ alignItems: 'center' }}>
|
||||
<AppIcon name={p.name} src={d.icons[p.id]} />
|
||||
<Tinted key={p.id} src={d.icons[p.id]} name={e?.app?.name ?? p.name}>
|
||||
<AppIcon name={p.name} src={d.icons[p.id]} className="fold-icon" />
|
||||
<div className="li-main">
|
||||
<span className="row" style={{ gap: 8 }}><span className="li-title">{e?.app?.name ?? p.name}</span><Chip tone={st.tone}>{st.text}</Chip></span>
|
||||
<span className="row" style={{ gap: 8 }}><span className="li-title">{e?.app?.name ?? p.name}</span><StatusPill tone={st.tone}>{st.text}</StatusPill></span>
|
||||
<span className="li-sub mono-id selectable" title={p.path}>{shortPath(p.path, d.info?.home)}</span>
|
||||
<span className="row" style={{ gap: 6, marginTop: 6 }}>
|
||||
<StoreBadge store="ios" id={e?.app?.platforms.ios?.bundleId} off={!!e && !e.app?.platforms.ios} />
|
||||
<StoreBadge store="android" id={e?.app?.platforms.android?.packageName} off={!!e && !e.app?.platforms.android} />
|
||||
</span>
|
||||
</div>
|
||||
<div className="stack" style={{ gap: 6, width: 270 }}>
|
||||
<div className="stack fold-accs" style={{ gap: 6 }}>
|
||||
<label className="acc-pick" title="Google Play"><StoreGlyph store="android" />
|
||||
<select className="input" aria-label={t('folders.playFor', { name: p.name })} value={p.playAccount ?? ''} onChange={ev => setAcc(p, ev.target.value || null, p.appleAccount)} disabled={!!busy}>
|
||||
<option value="">{t('folders.noPlay')}</option>
|
||||
|
|
@ -119,7 +122,7 @@ export function AppFolders() {
|
|||
{apple.map(a => <option key={a.id} value={a.id}>{a.label}</option>)}
|
||||
</select></label>
|
||||
</div>
|
||||
<div className="actions" style={{ flexWrap: 'nowrap' }}>
|
||||
<div className="actions fold-acts">
|
||||
{p.missing ? <LocateButton project={p} /> : <>
|
||||
<button className="btn btn-sm" onClick={() => nav.go({ screen: 'app', appId: p.id, tab: 'releases' })} title={t('folders.openApp')}><Icon name="arrowRight" />{t('folders.open')}</button>
|
||||
<button className="btn btn-sm btn-ghost btn-icon" onClick={() => api.reveal(p.id).catch(e => setError(errorText(e)))} aria-label={t('folders.reveal')} title={t('folders.reveal')}><Icon name="eye" /></button>
|
||||
|
|
@ -128,14 +131,14 @@ export function AppFolders() {
|
|||
</>}
|
||||
<button className="btn btn-sm btn-ghost btn-icon" onClick={() => remove(p)} disabled={!!busy} aria-label={t('folders.remove', { name: p.name })} title={t('folders.remove', { name: p.name })}><Icon name="trash" /></button>
|
||||
</div>
|
||||
</div>
|
||||
</Tinted>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
<Card title={t('folders.newTitle')} icon="sparkle" sub={t('folders.newSub')}>
|
||||
<Section title={t('folders.newTitle')} icon="sparkle" sub={t('folders.newSub')}>
|
||||
<div className="row between" style={{ gap: 16, flexWrap: 'wrap' }}>
|
||||
<div className="stack" style={{ gap: 8 }}>
|
||||
<Seg value={profile} onChange={setProfile} label={t('folders.profile')} options={[['app', t('profile.app')], ['game', t('profile.game')], ['subscription-app', t('profile.subscription-app')]]} />
|
||||
|
|
@ -147,16 +150,16 @@ export function AppFolders() {
|
|||
</div>
|
||||
</div>
|
||||
<p className="small muted act-note"><Icon name="info" />{t('folders.newNote')}</p>
|
||||
</Card>
|
||||
</Section>
|
||||
|
||||
{run && (
|
||||
<Card title={`${run.title} · ${run.project}`} icon="terminal" sub={<>{status}{run.result && <> · <code className="selectable">{run.result.command}</code></>}</>}
|
||||
<Section title={`${run.title} · ${run.project}`} icon="terminal" sub={<>{status}{run.result && <> · <code className="selectable">{run.result.command}</code></>}</>}
|
||||
actions={<button className="btn btn-sm btn-ghost btn-icon" onClick={() => setRun(null)} aria-label={t('common.close')}><Icon name="x" /></button>}>
|
||||
<div className="stack">
|
||||
{run.error && <Notice tone="bad">{run.error}</Notice>}
|
||||
<pre className="log selectable" ref={logRef} aria-live="polite">{tildeText(run.result?.output ?? run.lines.join('\n'), d.info?.home) || ' '}</pre>
|
||||
</div>
|
||||
</Card>
|
||||
</Section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue