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:
parent
98cd1d3c92
commit
0ef27cf2e4
22 changed files with 918 additions and 115 deletions
|
|
@ -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} />}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue