Desktop D3 UI: studios and store accounts (health checks with fixes, import apps), one card per app across studios (automatic iOS / Android pairing, manual pair / unpair, links, studio moves), store-only apps read-only, ignore list, portfolio search / studio filter / sort / grouping, collapsible studio sidebar, complete Settings, notifications, fixtures with 3 studios and 5 accounts

This commit is contained in:
CRTkafa 2026-10-11 04:15:30 +08:00
parent e058d09683
commit 7b2cc8da0a
24 changed files with 2225 additions and 312 deletions

View file

@ -0,0 +1,66 @@
// Ignored: apps and whole store accounts left out of the portfolio, the sidebar and store reads. Restore brings them
// back (nothing on the stores changes either way).
import { t } from '../i18n';
import { withIgnored } from '../lib/identity';
import { useNav } from '../nav';
import { useData } from '../store';
import { Icon, StoreGlyph } from '../ui/icons';
import { AppIcon, Card, Empty, StoreOnlyTag, StudioMark } from '../ui/kit';
export function Ignored() {
const d = useData();
const nav = useNav();
const apps = d.apps.filter(a => a.ignored).sort((a, b) => a.name.localeCompare(b.name));
const accounts = d.accounts.filter(a => d.overrides.ignored.includes(`account:${a.id}`));
const restoreAll = () => d.updateOverrides(o => ({ ...o, ignored: [] }));
const empty = !apps.length && !accounts.length;
return (
<div className="page">
<div className="ph">
<div><h1>{t('nav.ignored')}</h1><p>{t('ignored.lead')}</p></div>
{!empty && <div className="actions"><button className="btn" onClick={restoreAll}><Icon name="eye" />{t('ignored.restoreAll')}</button></div>}
</div>
{empty ? <Card pad={false}><Empty compact illo="shield" title={t('ignored.emptyTitle')}>{t('ignored.emptyText')}</Empty></Card> : (
<>
<Card title={t('ignored.apps')} icon="layers" sub={t('ignored.appsSub', { count: apps.length })} pad={false}>
{apps.length ? (
<div className="list">
{apps.map(a => {
const s = d.studios.find(x => x.id === a.studio) ?? null;
return (
<div className="li" key={a.key}>
<AppIcon name={a.name} src={d.iconFor(a)} />
<div className="li-main">
<span className="li-title row" style={{ gap: 8 }}>{a.name}{!a.project && <StoreOnlyTag />}</span>
<span className="li-sub row" style={{ gap: 8 }}>
<StudioMark studio={s} size="sm" />{s?.name ?? t('studio.none')}
{a.ios && <span className="row" style={{ gap: 4 }}><StoreGlyph store="ios" className="ic13" /><span className="mono-id">{a.ios.app.bundleId}</span></span>}
{a.android && <span className="row" style={{ gap: 4 }}><StoreGlyph store="android" className="ic13" /><span className="mono-id">{a.android.app.id}</span></span>}
</span>
</div>
<button className="btn btn-sm" onClick={() => d.updateOverrides(o => withIgnored(o, a.ignoreKeys, false))}><Icon name="eye" />{t('ignored.restore')}</button>
</div>
);
})}
</div>
) : <div className="card-body"><p className="small muted">{t('ignored.noApps')}</p></div>}
</Card>
<Card title={t('ignored.accounts')} icon="key" sub={t('ignored.accountsSub')} pad={false}>
{accounts.length ? (
<div className="list">
{accounts.map(a => (
<div className="li" key={a.id}>
<span className="li-icon tone-bg-neutral"><StoreGlyph store={a.kind === 'play' ? 'android' : 'ios'} /></span>
<div className="li-main"><span className="li-title">{a.label}</span><span className="li-sub">{a.kind === 'play' ? t('acct.kindPlay') : t('acct.kindApple')} · {t('ignored.accountApps', { count: d.storeApps[a.id]?.value.apps.length ?? 0 })}</span></div>
<button className="btn btn-sm" onClick={() => d.updateOverrides(o => withIgnored(o, [`account:${a.id}`], false))}><Icon name="eye" />{t('ignored.restore')}</button>
</div>
))}
</div>
) : <div className="card-body"><p className="small muted">{t('ignored.noAccounts')}</p></div>}
</Card>
</>
)}
<p className="small muted">{t('ignored.howTo')} <button className="link" onClick={() => nav.go({ screen: 'settings', section: 'pairing' })}>{t('ignored.toPairing')}</button></p>
</div>
);
}