desktop: five sections by job (Release, Store page, Revenue, Players, Health), Game services view, sidebar by attention

- app view: 12 tabs become 5 sections, each with a header (what it is for, the one next step from state
  already read) and its sub views in a segmented control; 1-5 jump, Ctrl+Shift+[ ] cycle, Ctrl+K and the
  context menu list sections and sub views; old tab ids, section and section/sub links all resolve
- Players > Game services (games only): both stores, one row per leaderboard / achievement with its
  languages and each store's state, Game Center locale map, Play API-disabled state with the console plan
- games status --json: items, localeMap, playApiDisabled; inspect summary: games
- sidebar: apps grouped by attention / in progress / live / not on a store, one status line each, studio
  filter, workspace as a footer icon row
- boot check opens places like a person (section, then sub view)
This commit is contained in:
CRTkafa 2026-10-24 13:01:50 +08:00
parent 98cd1d3c92
commit 0ef27cf2e4
22 changed files with 918 additions and 115 deletions

View file

@ -1,16 +1,20 @@
// One app across both stores: header (icon, studio, ids, store state, pairing), then the tabs. A store-only app (no
// One app across both stores: header (icon, studio, ids, store state, pairing), then five sections by job (Release,
// Store page, Revenue, Players, Health; docs IA), each with a header (what it is for, the one next step) and its sub views. A store-only app (no
// app folder) shows its store reads (releases, health, reviews, TestFlight) read-only, with "Link a folder" and
// "Start an app folder here" to make it a full app.
import { useContextMenu, useMenuButton } from '../ui/ContextMenu';
import { appMenu, playUrlFor } from '../ui/app-menu';
import { useShowIgnored } from '../lib/show-ignored';
import { useState } from 'react';
import { GamesTab } from './tabs/Games';
import { SectionHead } from './SectionHead';
import { api, errorText } from '../api';
import { type Key, t } from '../i18n';
import { pairCandidates, type UnifiedApp, withLink, withPair, withStudio } from '../lib/identity';
import { attention, storeNow } from '../lib/model';
import { Hero, nextStep, Tabs } from '../ui/ds';
import { type AppTab, TAB_ICON, TAB_KEY, tabsFor, useNav } from '../nav';
import { isGameSummary } from '../lib/model';
import { type AppSection, type AppTab, SECTION_ICON, SECTION_KEY, SECTION_TABS, sectionOf, sectionsFor, subsOf, useNav } from '../nav';
import { useData } from '../store';
import { Icon } from '../ui/icons';
import { ErrorBoundary } from '../ui/ErrorBoundary';
@ -107,11 +111,15 @@ export function AppView({ app, tab }: { app: UnifiedApp; tab: AppTab }) {
const ios = e?.app?.platforms.ios ?? (app.ios ? { bundleId: app.ios.app.bundleId, ascAppId: app.ios.app.id } : null);
const android = e?.app?.platforms.android ?? (app.android ? { packageName: app.android.app.id } : null);
const name = (app.project && e?.app?.name) || app.name;
const tabs = tabsFor(!!app.project);
const game = isGameSummary(e?.summary);
const sections = sectionsFor(!!app.project, game);
const section = sectionOf(tab);
const ed = useEdits();
useUnsavedGuard();
const studio = d.studios.find(s => s.id === app.studio) ?? null;
const setTab = (x: AppTab) => nav.go({ screen: 'app', appId: app.key, tab: x });
const setSection = (x: AppSection) => setTab(subsOf(x, !!app.project, game)[0] ?? SECTION_TABS[x][0]!);
const editCount = (x: AppTab) => (EDIT_TABS[x] ? ed.count(app.project?.id ?? app.target, EDIT_TABS[x]) : 0);
const all = l?.release.all ?? [];
const iosNow = l ? storeNow(all, 'ios').now : null, androidNow = l ? storeNow(all, 'android').now : null;
const att = attention(app.key, app.name, e, l);
@ -137,19 +145,20 @@ export function AppView({ app, tab }: { app: UnifiedApp; tab: AppTab }) {
{android && <button className="btn btn-sm btn-ghost" onClick={() => open(playUrlFor(d, app))} title={t('app.consoleHint')}><Icon name="external" />Play Console</button>}
</>}
all={all} ios={!!ios} android={!!android} next={l?.release.next?.version ?? null}
compact={tab !== 'releases'}
compact={section !== 'release' || tab !== 'releases'}
cta={nextStep({ project: !!app.project, att, all, next: l?.release.next?.version ?? null })} onCta={setTab} />
<AppControls app={app} />
{!app.project && <StoreOnlyBanner app={app} />}
{app.project?.storeboundPin && <Notice tone="info" icon="info">{t('app.pinNotice', { pin: app.project.storeboundPin, app: d.info?.version ?? '' })}</Notice>}
{app.project?.missing ? <FolderMissing project={app.project} /> : <>
<div className="ds-app-sticky">
<Tabs label={t('app.sections', { name })} value={tab} onChange={setTab} items={tabs.map(x => {
const n = EDIT_TABS[x] ? ed.count(app.project?.id ?? app.target, EDIT_TABS[x]) : 0;
return { id: x, label: t(TAB_KEY[x]), icon: TAB_ICON[x], count: att.filter(a => a.tab === x && a.tone === 'bad').length, dot: n > 0, dotTitle: n ? t('ed.unsaved', { count: n }) : undefined };
<Tabs label={t('app.sections', { name })} value={section} onChange={setSection} items={sections.map((x, i) => {
const n = subsOf(x, !!app.project, game).reduce((a, s2) => a + editCount(s2), 0);
return { id: x, label: t(SECTION_KEY[x]), icon: SECTION_ICON[x], hint: String(i + 1), count: att.filter(a => a.tab && sectionOf(a.tab) === x && a.tone === 'bad').length, dot: n > 0, dotTitle: n ? t('ed.unsaved', { count: n }) : undefined };
})} />
</div>
<div role="tabpanel" id="tabpanel" aria-labelledby={`tab-${tab}`} className="stack" style={{ gap: 16 }}>
<SectionHead app={app} section={section} tab={tab} subs={subsOf(section, !!app.project, game)} att={att} all={all} next={l?.release.next?.version ?? null} unsaved={subsOf(section, !!app.project, game).reduce((a, s2) => a + editCount(s2), 0)} editCount={editCount} onTab={setTab} />
<div role="tabpanel" id="tabpanel" aria-labelledby={`tab-${section}`} className="stack" style={{ gap: 16 }}>
<ErrorBoundary key={tab} where={`tab ${tab}`}>
{tab === 'releases' && <ReleasesTab app={app} />}
{tab === 'catalog' && app.project && <CatalogTab project={app.project} />}
@ -160,6 +169,7 @@ export function AppView({ app, tab }: { app: UnifiedApp; tab: AppTab }) {
{tab === 'art' && !app.project && <LiveArt target={app.target} />}
{tab === 'health' && <HealthTab app={app} />}
{tab === 'reviews' && <ReviewsTab app={app} />}
{tab === 'games' && app.project && <GamesTab project={app.project} />}
{tab === 'testflight' && <TestFlightTab app={app} />}
{tab === 'growth' && <GrowthTab app={app} />}
{tab === 'appinfo' && app.project && <AppInfoTab project={app.project} />}