From 445cd77dcf0c1d006e122972f8f8f6230a66baa4 Mon Sep 17 00:00:00 2001 From: phantomptr Date: Sun, 5 Jul 2026 17:23:48 -0700 Subject: [PATCH] =?UTF-8?q?feat(install):=20"Install=20all"=20button=20?= =?UTF-8?q?=E2=80=94=20batch-install=20staged=20packages=20in=20order?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements #137's install-all request (and completes the WIP trimmed in #173). The Install Package screen gains an "Install all (N)" header button that installs every staged, not-yet-installed package sequentially through the existing readiness-gated install() cascade, in base -> update -> DLC order so an add-on never installs before its base (Sony's installer accepts an update/DLC whose base isn't present yet but then lands nothing). - pkgLibrary.ts: `pkgEntryInstallOrder` (mirrors uploadQueue.installOrderPriority for a PkgEntry) + `installAll` action + `installingAll` state. installAll snapshots the idle, not-installed rows, sorts by order, and awaits each install() (which self-serializes on `installing` and never throws), counting outcomes; one summary bell at the end. No-op if an install is already running. - InstallPackage screen: "Install all (N)" button, shown only when >1 row is installable (a single row already has its own Install button), disabled while any install runs. - pkgLibrary.test.ts: 5 cases pinning pkgEntryInstallOrder (category order, path-hint fallback, category-beats-path, batch sort). - en.ts + i18n-known-missing.json: two new keys, allowlisted for all 17 non-English locales per the coverage gate. Client-only; no payload/engine changes. typecheck + lint + 759 tests + i18n (18 langs) + vite build all green. Co-Authored-By: Claude Opus 4.8 --- client/src/i18n/locales/en.ts | 2 + client/src/screens/InstallPackage/index.tsx | 44 +++++++++- client/src/state/pkgLibrary.test.ts | 40 +++++++++ client/src/state/pkgLibrary.ts | 95 +++++++++++++++++++++ scripts/i18n-known-missing.json | 34 ++++++++ 5 files changed, 214 insertions(+), 1 deletion(-) diff --git a/client/src/i18n/locales/en.ts b/client/src/i18n/locales/en.ts index e89ad07e..7df5e449 100644 --- a/client/src/i18n/locales/en.ts +++ b/client/src/i18n/locales/en.ts @@ -650,6 +650,8 @@ queue_will_mount: "mount after upload", "pkglib.footer.count": "{n} packages", "pkglib.footer.size": "{size} on PS5", "pkglib.clearFinished": "Clear finished ({n})", +"pkglib.installAll": "Install all", +"pkglib.installAll.hint": "Install every staged package, base games before updates and DLC", "pkglib.clearAll": "Clear all", "pkglib.clearAll.confirmTitle": "Delete all staged packages?", "pkglib.clearAll.confirmBody": "This permanently deletes all {n} staged .pkg file(s) from the PS5. Installed games are not affected.", diff --git a/client/src/screens/InstallPackage/index.tsx b/client/src/screens/InstallPackage/index.tsx index dc41d3e4..0294cadf 100644 --- a/client/src/screens/InstallPackage/index.tsx +++ b/client/src/screens/InstallPackage/index.tsx @@ -298,11 +298,13 @@ export default function InstallPackageScreen() { const loading = usePkgLibrary(host, (s) => s.loading); const error = usePkgLibrary(host, (s) => s.error); const installing = usePkgLibrary(host, (s) => s.installing); + const installingAll = usePkgLibrary(host, (s) => s.installingAll); const busyNotice = usePkgLibrary(host, (s) => s.busyNotice); const installPending = usePkgLibrary(host, (s) => s.installPending); const refresh = usePkgLibrary(host, (s) => s.refresh); const addAndUpload = usePkgLibrary(host, (s) => s.addAndUpload); const install = usePkgLibrary(host, (s) => s.install); + const installAll = usePkgLibrary(host, (s) => s.installAll); const cancelPendingInstall = usePkgLibrary( host, (s) => s.cancelPendingInstall, @@ -530,6 +532,13 @@ export default function InstallPackageScreen() { () => entries.filter(isFinishedPkg).length, [entries], ); + // Count of not-yet-installed, idle rows — drives the "Install all (N)" + // button. Mirrors installAll's own target filter in the store so the count + // and the action agree. >1 so the button only appears when it saves taps. + const installableCount = useMemo( + () => entries.filter((e) => e.status === "idle" && !e.installedHere).length, + [entries], + ); const sorted = entries; // store already sorts by title // Installing swaps the payload out (it owns the transfer port :9113), so it // can't run concurrently with an upload. Rather than block the button, the @@ -556,13 +565,46 @@ export default function InstallPackageScreen() { single-PS5 users; color-matches the console's tab so it's unambiguous with multiple consoles. */} + {/* Install every staged, not-yet-installed package in one tap, + base → update → DLC order. Only shown when it saves taps + (>1 installable row — a single row has its own Install button). + Disabled while any install runs; the store queues behind an + active upload rather than failing. */} + {installableCount > 1 && ( + + )}