Desktop: sidebar reader as short rows with a progress bar; one Refresh (top bar) instead of two; rolling-out tile lists versions on their own lines; cards without screenshots show live versions per store
This commit is contained in:
parent
c36ad87658
commit
83b8ce8869
6 changed files with 67 additions and 17 deletions
|
|
@ -152,7 +152,17 @@ function AppCard({ r, icon, health, studio }: { r: CardRow; icon: string | null;
|
|||
return (
|
||||
<article className="pf-card" style={style} aria-label={name} {...menu(() => appMenu(app, d, nav, { showIgnored }))}>
|
||||
<div className={`pf-shots${shots.length ? '' : ' empty'}`} aria-hidden="true">
|
||||
{shots.length ? shots.map((s, i) => <img key={i} src={s} alt="" draggable={false} loading="lazy" />) : <AppIcon name={app.name} src={icon} className="pf-hero-icon" />}
|
||||
{shots.length ? shots.map((s, i) => <img key={i} src={s} alt="" draggable={false} loading="lazy" />) : <>
|
||||
<AppIcon name={app.name} src={icon} className="pf-hero-icon" />
|
||||
<div className="pf-live">
|
||||
{(['ios', 'android'] as const).filter(k => (k === 'ios' ? ios : android)).map(k => {
|
||||
const now = storeNow(all, k).now;
|
||||
return <div key={k} className="pf-live-row"><StoreGlyph store={k} /><span className="pf-live-s">{k === 'ios' ? 'App Store' : 'Google Play'}</span>
|
||||
<span className="num strong">{now?.version ?? '–'}</span>{now && <PhaseChip r={now} />}</div>;
|
||||
})}
|
||||
<span className="pf-live-note">{t(projectId ? 'pf.noShots' : 'pf.noShotsStore')}</span>
|
||||
</div>
|
||||
</>}
|
||||
</div>
|
||||
<div className="pf-body">
|
||||
<div className="pf-head">
|
||||
|
|
@ -402,21 +412,18 @@ export function Portfolio() {
|
|||
</div>
|
||||
<div className="actions">
|
||||
{neverImported === 0 && <button className="btn" onClick={() => d.importApps()} disabled={d.importBusy || !d.accounts.length} title={t('action.importAppsHint')}><Icon name="download" className={d.importBusy ? 'spin' : undefined} />{t('action.importApps')}</button>}
|
||||
<button className="btn btn-primary" onClick={() => d.refreshPortfolio(true)} disabled={!!d.portfolioBusy} title={t('action.readStoresHint')}>
|
||||
<Icon name="refresh" className={d.portfolioBusy === 'live' ? 'spin' : undefined} />{d.portfolioBusy === 'live' ? t('action.reading') : t('action.readStores')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{d.portfolioError && <Notice tone="bad">{d.portfolioError}</Notice>}
|
||||
<BlockedProjectsNotice blocked={blocked} onRetry={recheck} busy={rechecking} />
|
||||
{neverImported > 0 && <Notice tone="info" icon="download" action={<button className="btn btn-sm" onClick={() => d.importApps()} disabled={d.importBusy}>{t('action.importApps')}</button>}>{t('portfolio.notImported', { count: neverImported })}</Notice>}
|
||||
{!known && d.ready && d.portfolioBusy !== 'live' && <Notice tone="info" action={<button className="btn btn-sm" onClick={() => d.refreshPortfolio(true)}>{t('action.readStores')}</button>}>{t('portfolio.notReadNotice')}</Notice>}
|
||||
{!known && d.ready && d.portfolioBusy !== 'live' && <Notice tone="info" action={<button className="btn btn-sm btn-primary" onClick={() => { d.readAll(true).catch(() => {}); }}>{t('action.readStores')}</button>}>{t('portfolio.notReadNotice')}</Notice>}
|
||||
|
||||
<div className="kpis">
|
||||
<Kpi icon="layers" label={t('kpi.apps')} value={rows.length} sub={t('kpi.appsSubStore', { ios: iosApps, android: androidApps, store: storeOnly })} />
|
||||
<Kpi icon="check" label={t('kpi.live')} value={known ? liveCount : '–'} sub={known ? t('kpi.liveSub', { count: stores }) : notRead} />
|
||||
<Kpi icon="rocket" label={t('kpi.rolling')} value={known ? rolling.length : '–'} sub={!known ? notRead : rolling.length ? [...new Set(rolling.map(r => r.version))].map(v => `${v}: ${rolling.filter(r => r.version === v).map(r => `${r.store === 'ios' ? 'iOS' : 'Android'}${r.fraction !== null ? ` ${share(r.fraction)}` : ''}`).join(', ')}`).join(' · ') : t('kpi.none')} />
|
||||
<Kpi icon="rocket" label={t('kpi.rolling')} value={known ? rolling.length : '–'} sub={!known ? notRead : rolling.length ? <RollingLines rolling={rolling} /> : t('kpi.none')} />
|
||||
<Kpi icon="clock" label={t('kpi.review')} value={known ? review.length : '–'} sub={!known ? notRead : review.length ? review.map(r => `${r.store === 'ios' ? 'iOS' : 'Android'} ${r.version}`).join(' · ') : t('kpi.none')} tone={review.length ? 'warn' : undefined} />
|
||||
<Kpi icon="alert" label={t('kpi.attention')} value={att.length} sub={blockers ? t('kpi.blockers', { count: blockers }) : t('kpi.noBlockers')} tone={blockers ? 'bad' : att.length ? 'warn' : undefined} />
|
||||
</div>
|
||||
|
|
@ -505,3 +512,17 @@ export function Portfolio() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** The rolling-out tile: the first two versions on their own lines ("2.3.0 iOS 20% · Android 20%"), then "+N more"; all of
|
||||
them in the tooltip. Never one long ellipsized line. */
|
||||
function RollingLines({ rolling }: { rolling: StoreRelease[] }) {
|
||||
const versions = [...new Set(rolling.map(r => r.version ?? '?'))];
|
||||
const line = (v: string) => rolling.filter(r => (r.version ?? '?') === v).map(r => `${r.store === 'ios' ? 'iOS' : 'Android'}${r.fraction !== null ? ` ${share(r.fraction)}` : ''}`).join(' · ');
|
||||
const all = versions.map(v => `${v} ${line(v)}`).join(' · ');
|
||||
return (
|
||||
<span className="k-lines" title={all}>
|
||||
{versions.slice(0, 2).map(v => <span key={v} className="k-line"><b className="num">{v}</b> {line(v)}</span>)}
|
||||
{versions.length > 2 && <span className="k-line muted">{t('kpi.more', { count: versions.length - 2 })}</span>}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue