Desktop: app page no longer overflows (page grid minmax, wrapping tabs); app ignore/unpair actions move to a ... menu; unique keys in App info

This commit is contained in:
CRTkafa 2026-10-23 09:08:23 +08:00
parent dece83c3e8
commit 5e82010ace
9 changed files with 32 additions and 21 deletions

View file

@ -1,13 +1,13 @@
// One app across both stores: header (icon, studio, ids, store state, pairing), then the tabs. 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 } from '../ui/ContextMenu';
import { useContextMenu, useMenuButton } from '../ui/ContextMenu';
import { appMenu } from '../ui/app-menu';
import { useShowIgnored } from '../lib/show-ignored';
import { useState } from 'react';
import { api, errorText } from '../api';
import { type Key, t } from '../i18n';
import { pairCandidates, type UnifiedApp, withIgnored, withLink, withPair, withSideIgnored, withStudio, withUnpair } from '../lib/identity';
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';
@ -43,8 +43,8 @@ function AppControls({ app }: { app: UnifiedApp }) {
const d = useData();
const nav = useNav();
const cands = pairCandidates(app, d.apps);
const canUnpair = !app.project && !!app.ios && !!app.android;
const sides = (['ios', 'android'] as const).filter(k => app[k] || app.ignoredStores.includes(k));
const more = useMenuButton();
const [showIgnored] = useShowIgnored();
return (
<div className="app-controls">
<span className="small muted row" style={{ gap: 6 }}><Icon name="link" className="ic13" />{t(PAIRING[app.pairing])}</span>
@ -65,16 +65,8 @@ function AppControls({ app }: { app: UnifiedApp }) {
{cands.slice(0, 30).map(c => <option key={c.key} value={c.key}>{c.name} · {(c.ios ?? c.android)!.app.bundleId}</option>)}
</select></label>
)}
{canUnpair && <button className="btn btn-sm btn-ghost" onClick={() => d.updateOverrides(o => withUnpair(o, app.ios!.key, app.android!.key)).then(() => nav.go({ screen: 'portfolio' }))}><Icon name="unlink" />{t('app.unpair')}</button>}
{sides.length === 2 && sides.map(k => {
const off = app.ignored || app.ignoredStores.includes(k);
const store = k === 'ios' ? 'App Store' : 'Google Play';
return <button key={k} className="btn btn-sm btn-ghost" onClick={() => d.updateOverrides(o => withSideIgnored(o, app, k, !off))}
title={off ? t('app.unignoreSideHint', { store }) : t('app.ignoreSideHint', { store })}><Icon name={off ? 'eye' : 'eyeOff'} />{off ? t('app.unignoreSide', { store }) : t('app.ignoreSide', { store })}</button>;
})}
{app.ignored
? <button className="btn btn-sm btn-ghost" onClick={() => d.updateOverrides(o => withIgnored(o, app.ignoreKeys, false))} title={t('app.unignoreHint')}><Icon name="eye" />{t('app.unignore')}</button>
: <button className="btn btn-sm btn-ghost" onClick={() => d.updateOverrides(o => withIgnored(o, app.ignoreKeys, true)).then(() => nav.go({ screen: 'portfolio' }))} title={t('app.ignoreHint')}><Icon name="eyeOff" />{t('app.ignore')}</button>}
<span className="grow" />
<button className="btn btn-sm btn-ghost" {...more(() => appMenu(app, d, nav, { showIgnored }))} aria-label={t('cm.more')} title={t('cm.more')}><Icon name="more" />{app.ignored || app.ignoredStores.length ? t('nav.ignored') : null}</button>
</div>
);
}

View file

@ -107,8 +107,8 @@ function DataSafety({ project, d, onDone }: { project: Project; d: DeclarationsR
{ds.types.map(x => (
<tr key={x.id}>
<td><div className="strong">{typeLabel(x.id)}</div><div className="small muted">{x.category}{x.optional ? ` · ${t('ai.optional')}` : ''}{x.ephemeral ? ` · ${t('ai.ephemeral')}` : ''}</div></td>
<td><div className="chips">{x.collect.map(p => <Chip key={p} tone="neutral" dot={false}>{t(PURPOSE[p] ?? 'ds.functionality')}</Chip>)}</div></td>
<td>{x.shared ? <div className="chips">{x.share.map(p => <Chip key={p} tone="warn" dot={false}>{t(PURPOSE[p] ?? 'ds.functionality')}</Chip>)}</div> : <span className="muted small">{t('ai.notShared')}</span>}</td>
<td><div className="chips">{x.collect.map((p, i) => <Chip key={`${p}-${i}`} tone="neutral" dot={false}>{t(PURPOSE[p] ?? 'ds.functionality')}</Chip>)}</div></td>
<td>{x.shared ? <div className="chips">{x.share.map((p, i) => <Chip key={`${p}-${i}`} tone="warn" dot={false}>{t(PURPOSE[p] ?? 'ds.functionality')}</Chip>)}</div> : <span className="muted small">{t('ai.notShared')}</span>}</td>
</tr>
))}
</tbody>
@ -138,7 +138,7 @@ function AppPrivacy({ project, d, appId }: { project: Project; d: DeclarationsRe
{ap.items.map(x => (
<tr key={x.apple}>
<td><div className="strong">{x.apple}</div><div className="small muted">{x.category} · {x.from.map(typeLabel).join(', ')}</div></td>
<td><div className="chips">{x.purposes.map(p => <Chip key={p} tone={/Third-Party/.test(p) ? 'warn' : 'neutral'} dot={false}>{p}</Chip>)}</div></td>
<td><div className="chips">{x.purposes.map((p, i) => <Chip key={`${p}-${i}`} tone={/Third-Party/.test(p) ? 'warn' : 'neutral'} dot={false}>{p}</Chip>)}</div></td>
<td><Chip tone="warn" icon="question">{t('ai.ask')}</Chip></td>
<td><Chip tone={x.thirdParty ? 'warn' : 'outline'} icon="question">{x.thirdParty ? t('ai.askLikely') : t('ai.ask')}</Chip></td>
</tr>