storebound/apps/desktop/src/screens/Ignored.tsx
CRT 5236f002f1 desktop: tray menu panels and pinnable context menus
The tray's right-click menu is drawn by the app: one tree from tray.rs
(stable submenu keys), one frameless always-on-top menu-N window per
panel rendered by tray-menu.html with the design system, placed inside
the monitor's work area. A pure panel book keeps transient and pinned
panels apart; pinned panels (pin button, Ctrl+click, middle click) are
draggable, step aside from each other and are saved in state.json
(menuPins) and restored at start. tray_update pushes the tree to the
open panels, which update in place; closed panels are destroyed. The
native menu, rendered from the same tree, stays as the fallback and on
Linux. Only menu-* windows get the menu_* commands (menu.json).

In-app context menus get the same behaviour through a pure menu stack
(ui/menu-stack.ts): pin a menu, tear off a submenu, pinned panels
survive the next menu, are draggable and re-read their entries from
live sources so data changes update them in place.

Also drops a stray diff3 marker line from CHANGELOG.md.
2026-10-24 13:44:08 +08:00

69 lines
4.9 KiB
TypeScript

// 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 { useContextMenu } from '../ui/ContextMenu';
import { appMenu } from '../ui/app-menu';
import { t } from '../i18n';
import { withIgnored, withSideIgnored } from '../lib/identity';
import { useNav } from '../nav';
import { useData } from '../store';
import { Icon, StoreGlyph } from '../ui/icons';
import { AppIcon, StoreOnlyTag, StudioMark } from '../ui/kit';
import { EmptyState, PageHeader, Section } from '../ui/ds';
import '../styles/folders.css';
export function Ignored() {
const d = useData();
const nav = useNav();
const apps = d.apps.filter(a => a.ignored || a.ignoredStores.length > 0).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;
const cm = useContextMenu();
return (
<div className="page scr-ignored">
<PageHeader icon="eyeOff" eyebrow={t('v2cfg.workspace')} title={t('nav.ignored')} subtitle={t('ignored.lead')} actions={!empty ? <><button className="btn" onClick={restoreAll}><Icon name="eye" />{t('ignored.restoreAll')}</button></> : undefined} />
{empty ? <Section><EmptyState icon="shield" tone="ok"><b>{t('ignored.emptyTitle')}</b> {t('ignored.emptyText')}</EmptyState></Section> : (
<>
<Section 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} {...cm(() => appMenu(a, d, nav, { showIgnored: true }), `app:${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>
{a.ignoredStores.map(k => <button key={k} className="btn btn-sm" onClick={() => d.updateOverrides(o => withSideIgnored(o, a, k, false))}><Icon name="eye" />{t('app.unignoreSide', { store: k === 'ios' ? 'App Store' : 'Google Play' })}</button>)}
{a.ignored && <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>}
</Section>
<Section 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} {...cm(() => [{ label: t('ignored.restore'), icon: 'eye', onSelect: () => { void d.updateOverrides(o => withIgnored(o, [`account:${a.id}`], false)); } }, { label: t('cm.openAccounts'), icon: 'key', onSelect: () => nav.go({ screen: 'accounts' }) }])}>
<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>}
</Section>
</>
)}
<p className="small muted">{t('ignored.howTo')} <button className="link" onClick={() => nav.go({ screen: 'settings', section: 'pairing' })}>{t('ignored.toPairing')}</button></p>
</div>
);
}