diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx
index 242211c..30789c0 100644
--- a/apps/desktop/src/App.tsx
+++ b/apps/desktop/src/App.tsx
@@ -286,17 +286,22 @@ export function App({ onTheme }: { onTheme: () => void }) {
);
}
-/** The background reader in one line: "Reading 12 of 48 · App Store limit 3 412/3 600 left this hour". */
+/** The background reader in the sidebar foot: progress (count + thin bar) and the App Store hourly limit on their own
+ * short rows, so nothing is cut off; the long wording stays in the tooltip. */
function ReaderLine() {
const d = useData();
const r = d.reader;
const q = Object.values(r.quota).sort((a, b) => a.remaining - b.remaining)[0];
- const parts: string[] = [];
- if (r.total > 0 && (r.running || r.queued)) parts.push(t('reader.progress', { done: r.done, total: r.total }));
- else if (!d.reader.autoRead) parts.push(t('reader.off'));
- else if (d.ready && d.apps.length) parts.push(t('reader.upToDate'));
- if (Object.keys(r.paused).length) parts.push(t('reader.backoff'));
- if (q) parts.push(t('reader.appleQuota', { left: num(q.remaining), limit: num(q.limit) }));
- if (!parts.length) return null;
- return
{parts.join(' · ')}
;
+ const busy = r.total > 0 && (r.running || r.queued);
+ const head = busy ? t('reader.progress', { done: r.done, total: r.total }) : !d.reader.autoRead ? t('reader.off') : d.ready && d.apps.length ? t('reader.upToDate') : '';
+ const paused = Object.keys(r.paused).length > 0;
+ if (!head && !q && !paused) return null;
+ return (
+
+ {head &&
{head}
}
+ {busy &&
}
+ {paused &&
{t('reader.backoffShort')}
}
+ {q &&
{t('reader.appleQuotaShort', { left: num(q.remaining), limit: num(q.limit) })}
}
+
+ );
}
diff --git a/apps/desktop/src/i18n/en.ts b/apps/desktop/src/i18n/en.ts
index 0e2d6c0..075a4a0 100644
--- a/apps/desktop/src/i18n/en.ts
+++ b/apps/desktop/src/i18n/en.ts
@@ -64,6 +64,11 @@ export const en = {
'palette.prevApp': 'Previous app',
'settings.scApps': 'Previous / next app',
'settings.scMenu': 'Context menu of the focused item',
+ 'reader.backoffShort': "Slowed down by the store",
+ 'reader.appleQuotaShort': "App Store {left} / {limit} per hour",
+ 'kpi.more': "+{count} more",
+ 'pf.noShots': "No screenshots in the app folder yet",
+ 'pf.noShotsStore': "Store only: no app folder with screenshots",
'palette.gActions': 'Actions',
'palette.openApp': 'Open',
diff --git a/apps/desktop/src/i18n/tr.ts b/apps/desktop/src/i18n/tr.ts
index ce7af40..0b94e93 100644
--- a/apps/desktop/src/i18n/tr.ts
+++ b/apps/desktop/src/i18n/tr.ts
@@ -66,6 +66,11 @@ export const tr: Record = {
'palette.prevApp': 'Önceki uygulama',
'settings.scApps': 'Önceki / sonraki uygulama',
'settings.scMenu': 'Odaktaki öğenin bağlam menüsü',
+ 'reader.backoffShort': "Mağaza yavaşlattı",
+ 'reader.appleQuotaShort': "App Store saatte {left} / {limit}",
+ 'kpi.more': "+{count} tane daha",
+ 'pf.noShots': "Uygulama klasöründe henüz ekran görüntüsü yok",
+ 'pf.noShotsStore': "Yalnız mağazada: ekran görüntülü uygulama klasörü yok",
'palette.gActions': 'İşlemler',
'palette.openApp': 'Aç',
diff --git a/apps/desktop/src/screens/Portfolio.tsx b/apps/desktop/src/screens/Portfolio.tsx
index 31cdfbb..9f8db34 100644
--- a/apps/desktop/src/screens/Portfolio.tsx
+++ b/apps/desktop/src/screens/Portfolio.tsx
@@ -152,7 +152,17 @@ function AppCard({ r, icon, health, studio }: { r: CardRow; icon: string | null;
return (
appMenu(app, d, nav, { showIgnored }))}>
- {shots.length ? shots.map((s, i) =>
) :
}
+ {shots.length ? shots.map((s, i) =>
) : <>
+
+
+ {(['ios', 'android'] as const).filter(k => (k === 'ios' ? ios : android)).map(k => {
+ const now = storeNow(all, k).now;
+ return
{k === 'ios' ? 'App Store' : 'Google Play'}
+
{now?.version ?? '–'} {now &&
}
;
+ })}
+
{t(projectId ? 'pf.noShots' : 'pf.noShotsStore')}
+
+ >}
@@ -402,21 +412,18 @@ export function Portfolio() {
{neverImported === 0 && d.importApps()} disabled={d.importBusy || !d.accounts.length} title={t('action.importAppsHint')}> {t('action.importApps')} }
- d.refreshPortfolio(true)} disabled={!!d.portfolioBusy} title={t('action.readStoresHint')}>
- {d.portfolioBusy === 'live' ? t('action.reading') : t('action.readStores')}
-
{d.portfolioError && {d.portfolioError} }
{neverImported > 0 && d.importApps()} disabled={d.importBusy}>{t('action.importApps')}}>{t('portfolio.notImported', { count: neverImported })} }
- {!known && d.ready && d.portfolioBusy !== 'live' && d.refreshPortfolio(true)}>{t('action.readStores')}}>{t('portfolio.notReadNotice')} }
+ {!known && d.ready && d.portfolioBusy !== 'live' && { d.readAll(true).catch(() => {}); }}>{t('action.readStores')}}>{t('portfolio.notReadNotice')} }
- 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')} />
+ : t('kpi.none')} />
`${r.store === 'ios' ? 'iOS' : 'Android'} ${r.version}`).join(' · ') : t('kpi.none')} tone={review.length ? 'warn' : undefined} />
@@ -505,3 +512,17 @@ export function Portfolio() {
);
}
+
+/** 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 (
+
+ {versions.slice(0, 2).map(v => {v} {line(v)} )}
+ {versions.length > 2 && {t('kpi.more', { count: versions.length - 2 })} }
+
+ );
+}
diff --git a/apps/desktop/src/styles.css b/apps/desktop/src/styles.css
index 8be9191..77bc271 100644
--- a/apps/desktop/src/styles.css
+++ b/apps/desktop/src/styles.css
@@ -1048,7 +1048,7 @@ button.ck.ck-pick[aria-checked="true"] { border-color: var(--accent-solid); box-
.pf-shots { display: flex; gap: 10px; height: 200px; padding: 14px 14px 0; overflow: hidden; background: linear-gradient(160deg, hsl(var(--tint-h) var(--pf-tint-s) var(--pf-tint-l) / .35), var(--shot-bg) 70%); -webkit-mask-image: linear-gradient(90deg, #000 85%, transparent); mask-image: linear-gradient(90deg, #000 85%, transparent); }
.pf-shots img { height: 100%; width: auto; aspect-ratio: 9 / 19.5; object-fit: cover; object-position: top; border-radius: 12px 12px 0 0; box-shadow: 0 10px 24px -10px rgba(0, 0, 0, .45); flex: none; transition: transform var(--speed) var(--ease); }
.pf-card:hover .pf-shots img { transform: translateY(-3px); }
-.pf-shots.empty { align-items: center; justify-content: center; padding: 0; -webkit-mask-image: none; mask-image: none;
+.pf-shots.empty { align-items: center; justify-content: center; gap: 22px; padding: 0 22px; -webkit-mask-image: none; mask-image: none;
background: radial-gradient(60% 90% at 50% 60%, hsl(var(--tint-h) var(--pf-tint-s) var(--pf-tint-l) / .45), transparent 70%), linear-gradient(160deg, hsl(var(--tint-h) var(--pf-tint-s) var(--pf-tint-l) / .22), var(--shot-bg)); }
.pf-shots .pf-hero-icon { width: 96px; height: 96px; border-radius: 24px; font-size: 30px; box-shadow: 0 18px 40px -14px hsl(var(--tint-h) 70% 30% / .7), inset 0 0 0 1px rgba(255, 255, 255, .12); transition: transform var(--speed) var(--ease); }
.pf-card:hover .pf-hero-icon { transform: scale(1.04) rotate(-2deg); }
@@ -1110,3 +1110,16 @@ button.ck.ck-pick[aria-checked="true"] { border-color: var(--accent-solid); box-
@media (max-width: 1100px) { .pf-cols { grid-template-columns: minmax(0, 1fr); } }
@media (prefers-reduced-motion: reduce) { .pf-card, .pf-card:hover, .pf-shots img, .pf-hero-icon { transform: none !important; transition: none; } }
:root[data-motion="reduce"] .pf-card, :root[data-motion="reduce"] .pf-card:hover, :root[data-motion="reduce"] .pf-shots img, :root[data-motion="reduce"] .pf-hero-icon { transform: none !important; transition: none; }
+.reader-line { display: grid; gap: 5px; }
+.reader-line .row span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.reader-bar { height: 3px; border-radius: 2px; background: var(--line); overflow: hidden; margin-left: 21px; }
+.reader-bar i { display: block; height: 100%; background: var(--accent-solid); transition: width var(--speed) var(--ease); }
+.kpi .k-sub .k-lines { display: grid; gap: 1px; white-space: nowrap; }
+.kpi .k-sub .k-line { overflow: hidden; text-overflow: ellipsis; }
+.kpi .k-sub:has(.k-lines) { white-space: normal; overflow: visible; }
+.pf-shots.empty .pf-hero-icon { width: 76px; height: 76px; border-radius: 20px; flex: none; }
+.pf-live { display: grid; gap: 8px; min-width: 0; }
+.pf-live-row { display: flex; align-items: center; gap: 8px; height: 28px; padding: 0 10px; border-radius: 9px; background: color-mix(in srgb, var(--surface) 70%, transparent); border: 1px solid var(--line); font-size: var(--t-meta); white-space: nowrap; }
+.pf-live-row svg, .pf-live-row .store-mark { width: 14px; height: 14px; flex: none; }
+.pf-live-s { color: var(--muted); }
+.pf-live-note { font-size: var(--t-micro, 11px); color: var(--muted); }
diff --git a/apps/desktop/src/ui/icons.tsx b/apps/desktop/src/ui/icons.tsx
index f57cc74..6d0cb1c 100644
--- a/apps/desktop/src/ui/icons.tsx
+++ b/apps/desktop/src/ui/icons.tsx
@@ -27,6 +27,7 @@ import { DotIcon } from '@phosphor-icons/react/dist/csr/Dot';
import { DownloadSimpleIcon } from '@phosphor-icons/react/dist/csr/DownloadSimple';
import { ExportIcon } from '@phosphor-icons/react/dist/csr/Export';
import { DotsThreeIcon } from '@phosphor-icons/react/dist/csr/DotsThree';
+import { GaugeIcon } from '@phosphor-icons/react/dist/csr/Gauge';
import { EyeIcon } from '@phosphor-icons/react/dist/csr/Eye';
import { EyeSlashIcon } from '@phosphor-icons/react/dist/csr/EyeSlash';
import { FileTextIcon } from '@phosphor-icons/react/dist/csr/FileText';
@@ -85,7 +86,7 @@ const ICONS = {
sun: SunIcon, terminal: TerminalWindowIcon, copy: CopyIcon, image: ImageIcon, users: UsersIcon, layers: StackIcon,
sparkle: SparkleIcon, pause: PauseIcon, cube: CubeIcon, arrowRight: ArrowRightIcon, arrowLeft: ArrowLeftIcon, dot: DotIcon,
phone: DeviceMobileIcon, keyboard: KeyboardIcon, database: DatabaseIcon, wand: MagicWandIcon, building: BuildingsIcon,
- more: DotsThreeIcon, eye: EyeIcon, eyeOff: EyeSlashIcon, link: LinkIcon, unlink: LinkBreakIcon, bell: BellIcon, filter: FunnelIcon, play: PlayIcon,
+ more: DotsThreeIcon, gauge: GaugeIcon, eye: EyeIcon, eyeOff: EyeSlashIcon, link: LinkIcon, unlink: LinkBreakIcon, bell: BellIcon, filter: FunnelIcon, play: PlayIcon,
send: PaperPlaneRightIcon, edit: PencilSimpleIcon, upload: UploadSimpleIcon, sliders: SlidersHorizontalIcon,
rotate: ArrowCounterClockwiseIcon, cloud: CloudIcon, coins: CoinsIcon, chart: TrendUpIcon, flask: FlaskIcon, grid: GridFourIcon,
clipboard: ClipboardTextIcon, share: ExportIcon, lock: LockIcon, question: QuestionIcon,