Desktop: per-store ignore (hide only the App Store or Google Play side), N ignored show toggle in sidebar and portfolio, Unignore in the app controls; ignore flags sync with the shared app list
This commit is contained in:
parent
13b91dd423
commit
9610c9477e
14 changed files with 347 additions and 17 deletions
|
|
@ -4,7 +4,7 @@
|
|||
import { useState } from 'react';
|
||||
import { api, errorText } from '../api';
|
||||
import { type Key, t } from '../i18n';
|
||||
import { pairCandidates, type UnifiedApp, withIgnored, withLink, withPair, withStudio, withUnpair } from '../lib/identity';
|
||||
import { pairCandidates, type UnifiedApp, withIgnored, withLink, withPair, withSideIgnored, withStudio, withUnpair } 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';
|
||||
|
|
@ -41,6 +41,7 @@ function AppControls({ app }: { app: UnifiedApp }) {
|
|||
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));
|
||||
return (
|
||||
<div className="app-controls">
|
||||
<span className="small muted row" style={{ gap: 6 }}><Icon name="link" className="ic13" />{t(PAIRING[app.pairing])}</span>
|
||||
|
|
@ -62,7 +63,15 @@ function AppControls({ app }: { app: UnifiedApp }) {
|
|||
</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>}
|
||||
<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>
|
||||
{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>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// 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 { withIgnored, withSideIgnored } from '../lib/identity';
|
||||
import { useNav } from '../nav';
|
||||
import { useData } from '../store';
|
||||
import { Icon, StoreGlyph } from '../ui/icons';
|
||||
|
|
@ -12,7 +12,7 @@ import '../styles/folders.css';
|
|||
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 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;
|
||||
|
|
@ -37,7 +37,8 @@ export function Ignored() {
|
|||
{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>
|
||||
{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>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ import { type ReactNode, useEffect, useMemo, useState } from 'react';
|
|||
import { api, type AppStatus, newJob, type StoreRelease, type ToolResult } from '../api';
|
||||
import { type Key, t } from '../i18n';
|
||||
import { ago, pct, share } from '../lib/format';
|
||||
import { filterSort, groupByStudio, type ListRow, type SortBy, studioCounts, type UnifiedApp } from '../lib/identity';
|
||||
import { useShowIgnored } from '../lib/show-ignored';
|
||||
import { filterSort, groupByStudio, type ListRow, type SortBy, studioCounts, type UnifiedApp, withIgnored } from '../lib/identity';
|
||||
import { type Attention, attention, healthSeries, type HealthSnapshot, isError, laneOf, PLAY_THRESHOLDS, STAGES, storeNow, type Tone } from '../lib/model';
|
||||
import { type AppTab, useNav } from '../nav';
|
||||
import type { IconName } from '../ui/icons';
|
||||
|
|
@ -282,6 +283,8 @@ export function Portfolio() {
|
|||
const [query, setQuery] = useState('');
|
||||
const setView = (v: Partial<View>) => setViewState(x => { const n = { ...x, ...v }; try { localStorage.setItem(VIEW_KEY, JSON.stringify(n)); } catch { /* private mode */ } return n; });
|
||||
const visible = useMemo(() => d.apps.filter(a => !a.ignored), [d.apps]);
|
||||
const ignoredApps = useMemo(() => d.apps.filter(a => a.ignored), [d.apps]);
|
||||
const [showIgnored, setShowIgnored] = useShowIgnored();
|
||||
const [round, setRound] = useState(0);
|
||||
const [rechecking, setRechecking] = useState(false);
|
||||
const health = useCachedHealth(visible.map(a => a.target), round);
|
||||
|
|
@ -427,6 +430,18 @@ export function Portfolio() {
|
|||
<Seg value={view.layout} onChange={layout => setView({ layout })} label={t('pf.layout')} options={[['cards', t('pf.cards'), 'grid'], ['table', t('pf.table'), 'layers']]} />
|
||||
</div>
|
||||
|
||||
{ignoredApps.length > 0 && (
|
||||
<div className="row small" style={{ gap: 8, flexWrap: 'wrap' }} data-testid="ignored-strip">
|
||||
<button className="link" aria-pressed={showIgnored} onClick={() => setShowIgnored(!showIgnored)}>{showIgnored ? t('ignored.hide', { count: ignoredApps.length }) : t('ignored.show', { count: ignoredApps.length })}</button>
|
||||
{showIgnored && ignoredApps.map(a => (
|
||||
<span key={a.key} className="row muted" style={{ gap: 4 }}>
|
||||
<button className="link" onClick={() => open(a)}>{a.name}</button>
|
||||
<button className="btn btn-sm btn-ghost btn-icon" onClick={() => d.updateOverrides(o => withIgnored(o, a.ignoreKeys, false))} aria-label={t('ignored.restore')} title={t('ignored.restore')}><Icon name="eye" /></button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{view.layout === 'cards' && (rows.length ? grouped.map(g => {
|
||||
const s = g.studio === undefined ? undefined : d.studios.find(x => x.id === g.studio) ?? null;
|
||||
return (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue