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 }))}>
@@ -402,21 +412,18 @@ export function Portfolio() {
{neverImported === 0 && } -
{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,