diff --git a/client/src/console/api.test.ts b/client/src/console/api.test.ts index dde7a29b..6b72a70b 100644 --- a/client/src/console/api.test.ts +++ b/client/src/console/api.test.ts @@ -118,4 +118,20 @@ describe("probeHelper (is the helper there, or is this the offline copy?)", () = globalThis.fetch = (async () => jsonResponse(500, { error: "x" })) as typeof fetch; expect(await api.probeHelper()).toBe(true); }); + + it("a token read that never answers gives up after TOKEN_TIMEOUT_MS (offline screen, not a blank page)", async () => { + vi.useFakeTimers(); + try { + globalThis.fetch = ((_u: unknown, init?: RequestInit) => + new Promise((_res, rej) => { + init?.signal?.addEventListener("abort", () => rej(new Error("aborted"))); + })) as typeof fetch; + const p = api.probeHelper(); + await vi.advanceTimersByTimeAsync(api.TOKEN_TIMEOUT_MS); + expect(await p).toBe(false); + } finally { + vi.useRealTimers(); + } + }); }); + diff --git a/client/src/console/api.ts b/client/src/console/api.ts index 11c853a8..635e7214 100644 --- a/client/src/console/api.ts +++ b/client/src/console/api.ts @@ -39,12 +39,19 @@ export function onTokenExpired(fn: () => void): () => void { let tokenPromise: Promise | null = null; +/** A token read that has not answered by then counts as "nothing answered" (the offline screen). */ +export const TOKEN_TIMEOUT_MS = 4000; + async function fetchToken(): Promise { let res: Response; + const stop = typeof AbortController === "function" ? new AbortController() : null; + const timer = setTimeout(() => stop?.abort(), TOKEN_TIMEOUT_MS); try { - res = await fetch("/api/token", { method: "GET", credentials: "same-origin", cache: "no-store" }); + res = await fetch("/api/token", { method: "GET", credentials: "same-origin", cache: "no-store", signal: stop?.signal }); } catch { throw new ApiError(0, "offline"); + } finally { + clearTimeout(timer); } if (!res.ok) throw new ApiError(res.status, res.status === 503 ? "stopping" : "token_unavailable"); // Anything but JSON (another server on the port, a captive page) is not the helper. diff --git a/client/src/console/backHistory.ts b/client/src/console/backHistory.ts index 786aa705..a8d1855b 100644 --- a/client/src/console/backHistory.ts +++ b/client/src/console/backHistory.ts @@ -21,7 +21,9 @@ * * On the PS5 (opts.onRoot) the guard stays up on the first screen too, and ○ * there calls onRoot (window.close(): the browser goes back to the PS5's home - * screen, as Payload Manager does). If the page is still open after that, the + * screen, as Payload Manager does). Escape, Backspace and a gamepad ○ take + * the same path (requestBack → history.back()), so on the PS5 they too close + * the browser from Home with nothing open. If the page is still open after that, the * guard is left down so the next ○ does what the browser does by itself. * * WebKit can fire a popstate as the page loads that pops nothing. Such a pop diff --git a/client/src/console/console.css b/client/src/console/console.css index ccee5a31..83626d99 100644 --- a/client/src/console/console.css +++ b/client/src/console/console.css @@ -1497,7 +1497,7 @@ button { display: flex; align-items: center; gap: 0.9rem; - margin: 0.9rem 0 0; + margin: 0.9rem 0 1rem; padding: 0.6rem; border-radius: 12px; background: var(--color-surface-3); @@ -1522,3 +1522,14 @@ button { font-size: 0.95rem; overflow-wrap: anywhere; } + +/* ---- first moment while the helper is asked (main.tsx) ---- */ +.cw-splash { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: var(--color-canvas); + color: var(--color-muted); + font-size: 1.4rem; +} diff --git a/client/src/console/env.d.ts b/client/src/console/env.d.ts index f228747d..0e4442e3 100644 --- a/client/src/console/env.d.ts +++ b/client/src/console/env.d.ts @@ -2,3 +2,5 @@ declare module "virtual:pc-tokens.css"; /** The strings' build hash, defined by vite.console.config.ts (consoleStrings); absent under vitest. */ declare const __CW_STRINGS_V__: string | undefined; +/** The app version the page was built from (vite.console.config.ts); absent under vitest. */ +declare const __CW_APP_V__: string | undefined; diff --git a/client/src/console/locales/ar.json b/client/src/console/locales/ar.json index 8a0b9846..8c0604d6 100644 --- a/client/src/console/locales/ar.json +++ b/client/src/console/locales/ar.json @@ -414,5 +414,7 @@ "cw_tour_label": "نصائح", "cw_tour_next": "التالي", "cw_tour_skip": "تخطٍّ", - "cw_tour_step": "{n} من {total}" + "cw_tour_step": "{n} من {total}", + "cw_update_later": "لاحقًا", + "cw_update_ready": "تم تحديث PS5Upload Files. أعد التحميل لاستخدام الإصدار الجديد." } diff --git a/client/src/console/locales/bn.json b/client/src/console/locales/bn.json index b1118aa7..630e16cd 100644 --- a/client/src/console/locales/bn.json +++ b/client/src/console/locales/bn.json @@ -414,5 +414,7 @@ "cw_tour_label": "টিপস", "cw_tour_next": "পরের", "cw_tour_skip": "এড়িয়ে যান", - "cw_tour_step": "{total}-এর মধ্যে {n}" + "cw_tour_step": "{total}-এর মধ্যে {n}", + "cw_update_later": "পরে", + "cw_update_ready": "PS5Upload Files আপডেট হয়েছে। নতুন সংস্করণ ব্যবহার করতে আবার লোড করুন।" } diff --git a/client/src/console/locales/de.json b/client/src/console/locales/de.json index fe251cd8..b2747085 100644 --- a/client/src/console/locales/de.json +++ b/client/src/console/locales/de.json @@ -414,5 +414,7 @@ "cw_tour_label": "Tipps", "cw_tour_next": "Weiter", "cw_tour_skip": "Überspringen", - "cw_tour_step": "{n} von {total}" + "cw_tour_step": "{n} von {total}", + "cw_update_later": "Später", + "cw_update_ready": "PS5Upload Files wurde aktualisiert. Lade neu, um die neue Version zu verwenden." } diff --git a/client/src/console/locales/en.json b/client/src/console/locales/en.json index 87fd4aa0..07ee1a25 100644 --- a/client/src/console/locales/en.json +++ b/client/src/console/locales/en.json @@ -414,5 +414,7 @@ "cw_tour_label": "Tips", "cw_tour_next": "Next", "cw_tour_skip": "Skip", - "cw_tour_step": "{n} of {total}" + "cw_tour_step": "{n} of {total}", + "cw_update_later": "Later", + "cw_update_ready": "PS5Upload Files was updated. Reload to use the new version." } diff --git a/client/src/console/locales/es.json b/client/src/console/locales/es.json index bb9ba070..ae808190 100644 --- a/client/src/console/locales/es.json +++ b/client/src/console/locales/es.json @@ -414,5 +414,7 @@ "cw_tour_label": "Consejos", "cw_tour_next": "Siguiente", "cw_tour_skip": "Omitir", - "cw_tour_step": "{n} de {total}" + "cw_tour_step": "{n} de {total}", + "cw_update_later": "Más tarde", + "cw_update_ready": "PS5Upload Files se actualizó. Recarga para usar la nueva versión." } diff --git a/client/src/console/locales/fa.json b/client/src/console/locales/fa.json index f2c1f1bc..6112c107 100644 --- a/client/src/console/locales/fa.json +++ b/client/src/console/locales/fa.json @@ -414,5 +414,7 @@ "cw_tour_label": "نکته‌ها", "cw_tour_next": "بعدی", "cw_tour_skip": "رد شدن", - "cw_tour_step": "{n} از {total}" + "cw_tour_step": "{n} از {total}", + "cw_update_later": "بعداً", + "cw_update_ready": "PS5Upload Files به‌روزرسانی شد. برای استفاده از نسخهٔ جدید، دوباره بارگیری کنید." } diff --git a/client/src/console/locales/fr.json b/client/src/console/locales/fr.json index 96e22669..8ad7d385 100644 --- a/client/src/console/locales/fr.json +++ b/client/src/console/locales/fr.json @@ -414,5 +414,7 @@ "cw_tour_label": "Astuces", "cw_tour_next": "Suivant", "cw_tour_skip": "Passer", - "cw_tour_step": "{n} sur {total}" + "cw_tour_step": "{n} sur {total}", + "cw_update_later": "Plus tard", + "cw_update_ready": "PS5Upload Files a été mis à jour. Rechargez pour utiliser la nouvelle version." } diff --git a/client/src/console/locales/hi.json b/client/src/console/locales/hi.json index 2fb8eed1..9b67b669 100644 --- a/client/src/console/locales/hi.json +++ b/client/src/console/locales/hi.json @@ -414,5 +414,7 @@ "cw_tour_label": "सुझाव", "cw_tour_next": "आगे", "cw_tour_skip": "छोड़ें", - "cw_tour_step": "{total} में से {n}" + "cw_tour_step": "{total} में से {n}", + "cw_update_later": "बाद में", + "cw_update_ready": "PS5Upload Files अपडेट हो गया है। नया संस्करण इस्तेमाल करने के लिए फिर से लोड करें।" } diff --git a/client/src/console/locales/hu.json b/client/src/console/locales/hu.json index cbc26582..cdbffbc9 100644 --- a/client/src/console/locales/hu.json +++ b/client/src/console/locales/hu.json @@ -414,5 +414,7 @@ "cw_tour_label": "Tippek", "cw_tour_next": "Tovább", "cw_tour_skip": "Kihagyás", - "cw_tour_step": "{n} / {total}" + "cw_tour_step": "{n} / {total}", + "cw_update_later": "Később", + "cw_update_ready": "A PS5Upload Files frissült. Töltsd újra az új verzió használatához." } diff --git a/client/src/console/locales/id.json b/client/src/console/locales/id.json index 7cdb4d44..24deeae8 100644 --- a/client/src/console/locales/id.json +++ b/client/src/console/locales/id.json @@ -414,5 +414,7 @@ "cw_tour_label": "Tips", "cw_tour_next": "Berikutnya", "cw_tour_skip": "Lewati", - "cw_tour_step": "{n} dari {total}" + "cw_tour_step": "{n} dari {total}", + "cw_update_later": "Nanti", + "cw_update_ready": "PS5Upload Files telah diperbarui. Muat ulang untuk memakai versi baru." } diff --git a/client/src/console/locales/it.json b/client/src/console/locales/it.json index de7c6633..3c211cf6 100644 --- a/client/src/console/locales/it.json +++ b/client/src/console/locales/it.json @@ -414,5 +414,7 @@ "cw_tour_label": "Suggerimenti", "cw_tour_next": "Avanti", "cw_tour_skip": "Salta", - "cw_tour_step": "{n} di {total}" + "cw_tour_step": "{n} di {total}", + "cw_update_later": "Più tardi", + "cw_update_ready": "PS5Upload Files è stato aggiornato. Ricarica per usare la nuova versione." } diff --git a/client/src/console/locales/ja.json b/client/src/console/locales/ja.json index be0c1dfd..581e38cc 100644 --- a/client/src/console/locales/ja.json +++ b/client/src/console/locales/ja.json @@ -414,5 +414,7 @@ "cw_tour_label": "ヒント", "cw_tour_next": "次へ", "cw_tour_skip": "スキップ", - "cw_tour_step": "{n} / {total}" + "cw_tour_step": "{n} / {total}", + "cw_update_later": "後で", + "cw_update_ready": "PS5Upload Files が更新されました。新しいバージョンを使うには再読み込みしてください。" } diff --git a/client/src/console/locales/ko.json b/client/src/console/locales/ko.json index d23e198e..40801e40 100644 --- a/client/src/console/locales/ko.json +++ b/client/src/console/locales/ko.json @@ -414,5 +414,7 @@ "cw_tour_label": "팁", "cw_tour_next": "다음", "cw_tour_skip": "건너뛰기", - "cw_tour_step": "{n} / {total}" + "cw_tour_step": "{n} / {total}", + "cw_update_later": "나중에", + "cw_update_ready": "PS5Upload Files가 업데이트되었습니다. 새 버전을 사용하려면 다시 불러오세요." } diff --git a/client/src/console/locales/pl.json b/client/src/console/locales/pl.json index 552b97d0..349ba2c2 100644 --- a/client/src/console/locales/pl.json +++ b/client/src/console/locales/pl.json @@ -414,5 +414,7 @@ "cw_tour_label": "Wskazówki", "cw_tour_next": "Dalej", "cw_tour_skip": "Pomiń", - "cw_tour_step": "{n} z {total}" + "cw_tour_step": "{n} z {total}", + "cw_update_later": "Później", + "cw_update_ready": "PS5Upload Files został zaktualizowany. Wczytaj ponownie, aby użyć nowej wersji." } diff --git a/client/src/console/locales/pt-BR.json b/client/src/console/locales/pt-BR.json index 7378249b..c157ae4d 100644 --- a/client/src/console/locales/pt-BR.json +++ b/client/src/console/locales/pt-BR.json @@ -414,5 +414,7 @@ "cw_tour_label": "Dicas", "cw_tour_next": "Próxima", "cw_tour_skip": "Pular", - "cw_tour_step": "{n} de {total}" + "cw_tour_step": "{n} de {total}", + "cw_update_later": "Depois", + "cw_update_ready": "O PS5Upload Files foi atualizado. Recarregue para usar a nova versão." } diff --git a/client/src/console/locales/ru.json b/client/src/console/locales/ru.json index 2794ece7..fce7bc72 100644 --- a/client/src/console/locales/ru.json +++ b/client/src/console/locales/ru.json @@ -414,5 +414,7 @@ "cw_tour_label": "Советы", "cw_tour_next": "Далее", "cw_tour_skip": "Пропустить", - "cw_tour_step": "{n} из {total}" + "cw_tour_step": "{n} из {total}", + "cw_update_later": "Позже", + "cw_update_ready": "PS5Upload Files обновлён. Перезагрузите страницу, чтобы использовать новую версию." } diff --git a/client/src/console/locales/th.json b/client/src/console/locales/th.json index 38939155..3dddbcdc 100644 --- a/client/src/console/locales/th.json +++ b/client/src/console/locales/th.json @@ -414,5 +414,7 @@ "cw_tour_label": "เคล็ดลับ", "cw_tour_next": "ถัดไป", "cw_tour_skip": "ข้าม", - "cw_tour_step": "{n} จาก {total}" + "cw_tour_step": "{n} จาก {total}", + "cw_update_later": "ภายหลัง", + "cw_update_ready": "PS5Upload Files ได้รับการอัปเดตแล้ว โหลดใหม่เพื่อใช้เวอร์ชันใหม่" } diff --git a/client/src/console/locales/tr.json b/client/src/console/locales/tr.json index d6a0a76e..a3aaeb7a 100644 --- a/client/src/console/locales/tr.json +++ b/client/src/console/locales/tr.json @@ -414,5 +414,7 @@ "cw_tour_label": "İpuçları", "cw_tour_next": "İleri", "cw_tour_skip": "Atla", - "cw_tour_step": "{n} / {total}" + "cw_tour_step": "{n} / {total}", + "cw_update_later": "Sonra", + "cw_update_ready": "PS5Upload Files güncellendi. Yeni sürümü kullanmak için yeniden yükleyin." } diff --git a/client/src/console/locales/vi.json b/client/src/console/locales/vi.json index d9b5a1fd..35a34d2a 100644 --- a/client/src/console/locales/vi.json +++ b/client/src/console/locales/vi.json @@ -414,5 +414,7 @@ "cw_tour_label": "Mẹo", "cw_tour_next": "Tiếp", "cw_tour_skip": "Bỏ qua", - "cw_tour_step": "{n} / {total}" + "cw_tour_step": "{n} / {total}", + "cw_update_later": "Để sau", + "cw_update_ready": "PS5Upload Files đã được cập nhật. Hãy tải lại để dùng phiên bản mới." } diff --git a/client/src/console/locales/zh-CN.json b/client/src/console/locales/zh-CN.json index ae5dbceb..8d5ec449 100644 --- a/client/src/console/locales/zh-CN.json +++ b/client/src/console/locales/zh-CN.json @@ -414,5 +414,7 @@ "cw_tour_label": "提示", "cw_tour_next": "下一步", "cw_tour_skip": "跳过", - "cw_tour_step": "{n} / {total}" + "cw_tour_step": "{n} / {total}", + "cw_update_later": "稍后", + "cw_update_ready": "PS5Upload Files 已更新。请重新加载以使用新版本。" } diff --git a/client/src/console/locales/zh-TW.json b/client/src/console/locales/zh-TW.json index 7096e140..483778cc 100644 --- a/client/src/console/locales/zh-TW.json +++ b/client/src/console/locales/zh-TW.json @@ -414,5 +414,7 @@ "cw_tour_label": "提示", "cw_tour_next": "下一步", "cw_tour_skip": "略過", - "cw_tour_step": "{n} / {total}" + "cw_tour_step": "{n} / {total}", + "cw_update_later": "稍後", + "cw_update_ready": "PS5Upload Files 已更新。請重新載入以使用新版本。" } diff --git a/client/src/console/main.tsx b/client/src/console/main.tsx index 96ae3dab..7d3b5802 100644 --- a/client/src/console/main.tsx +++ b/client/src/console/main.tsx @@ -3,16 +3,18 @@ import { createRoot } from "react-dom/client"; import "virtual:pc-tokens.css"; import "./console.css"; import { App } from "./App"; -import { probeHelper } from "./api"; +import { call, probeHelper } from "./api"; import { installBackHistory } from "./backHistory"; import { installFullscreen } from "./fullscreen"; import { installMarquee } from "./ui/marquee"; import { DEFAULT_INPUT_MAP, dispatchNav, installNavigation } from "./nav"; import { installOfflineCopy } from "./offline/offlineCopy"; import { OfflinePage } from "./offline/OfflinePage"; +import { UpdateBanner } from "./offline/UpdateBanner"; +import { isNewerHelper, markUpdateReady } from "./offline/update"; import { ErrorBoundary } from "./ui/ErrorBoundary"; import { applyStoredTheme } from "./theme"; -import { getLanguage, onLanguageChange, setLanguage, stringsVersion } from "./strings"; +import { getLanguage, onLanguageChange, setLanguage, stringsVersion, t } from "./strings"; applyStoredTheme(); // before the first paint: no flash of the wrong theme // English is in the page; the detected (or stored) language loads from the helper and the page @@ -40,15 +42,35 @@ function Root() { useEffect(() => { let live = true; void probeHelper().then((ok) => { - if (live) setUp(ok); + if (!live) return; + setUp(ok); + // An older page than the helper (the browser's offline copy, or the helper updated + // underneath): offer the one-press reload. An older helper's reply is just not compared. + if (ok) { + call<{ version?: unknown }>("/api/settings").then( + (s) => { + if (isNewerHelper(s?.version)) markUpdateReady(); + }, + () => undefined, + ); + } }); return () => { live = false; }; }, []); - if (up === null) return null; + // A moment at most (the token read gives up after 4 s): the name, not a blank screen. + if (up === null) + return ( +
+ {t("cw_settings_app_name")} +
+ ); return ( - {up ? : } + + {up ? : } + + ); } diff --git a/client/src/console/offline/UpdateBanner.tsx b/client/src/console/offline/UpdateBanner.tsx new file mode 100644 index 00000000..f26b79e5 --- /dev/null +++ b/client/src/console/offline/UpdateBanner.tsx @@ -0,0 +1,28 @@ +import { useEffect, useRef } from "react"; +import { RotateCw } from "lucide-react"; +import { t } from "../strings"; +import { dismissUpdate, useUpdateReady } from "./update"; + +/** A newer page is waiting (update.ts): one press reloads into it; Later keeps working on this one. */ +export function UpdateBanner({ reload = () => window.location.reload() }: { reload?: () => void }) { + const ready = useUpdateReady(); + const button = useRef(null); + useEffect(() => { + if (ready) button.current?.focus(); + }, [ready]); + if (!ready) return null; + return ( +
+

{t("cw_update_ready")}

+
+ + +
+
+ ); +} diff --git a/client/src/console/offline/offlineCopy.test.ts b/client/src/console/offline/offlineCopy.test.ts index 68b84cf5..5ed52fd3 100644 --- a/client/src/console/offline/offlineCopy.test.ts +++ b/client/src/console/offline/offlineCopy.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -import { APPCACHE_RELOAD_WINDOW_MS, appcacheManifest, installOfflineCopy, serviceWorkerSource } from "./offlineCopy"; +import { appcacheManifest, installOfflineCopy, serviceWorkerSource } from "./offlineCopy"; /** The service worker run against a fake worker scope: caches in a Map, a scripted network. */ function worker(network: (url: string) => Promise) { @@ -134,22 +134,20 @@ describe("offline copy: appcache and registration", () => { it("registers the worker at / in a secure context (the console's 127.0.0.1) only", () => { const a = fakeWindow(true); - installOfflineCopy(a.win as unknown as Window, Date.now()); + installOfflineCopy(a.win as unknown as Window, vi.fn()); expect(a.win.navigator.serviceWorker.register).toHaveBeenCalledWith("/sw.js", { scope: "/" }); const b = fakeWindow(false); // a phone at the console's LAN address - installOfflineCopy(b.win as unknown as Window, Date.now()); + installOfflineCopy(b.win as unknown as Window, vi.fn()); expect(b.win.navigator.serviceWorker.register).not.toHaveBeenCalled(); }); - it("a newer appcache copy reloads right after opening, never later", () => { + it("a newer appcache copy offers the reload banner instead of reloading by itself", () => { const a = fakeWindow(true); - installOfflineCopy(a.win as unknown as Window, Date.now()); + const onUpdate = vi.fn(); + installOfflineCopy(a.win as unknown as Window, onUpdate); a.fns.updateready(); expect(a.win.applicationCache.swapCache).toHaveBeenCalled(); - expect(a.win.location.reload).toHaveBeenCalledTimes(1); - const b = fakeWindow(true); - installOfflineCopy(b.win as unknown as Window, Date.now() - APPCACHE_RELOAD_WINDOW_MS - 1); - b.fns.updateready(); - expect(b.win.location.reload).not.toHaveBeenCalled(); + expect(onUpdate).toHaveBeenCalledTimes(1); + expect(a.win.location.reload).not.toHaveBeenCalled(); }); }); diff --git a/client/src/console/offline/offlineCopy.ts b/client/src/console/offline/offlineCopy.ts index 1816d10f..4a956be6 100644 --- a/client/src/console/offline/offlineCopy.ts +++ b/client/src/console/offline/offlineCopy.ts @@ -11,8 +11,8 @@ * - the appcache (), the fallback the PS5 * browser honours for Payload Manager: the page is its only cached entry * (NETWORK: * lets everything else through). An appcache serves its copy - * first and updates behind it, so a newer page reloads once on - * "updateready" (only right after the page opened, never mid-task). + * first and updates behind it, so on "updateready" the page offers a + * one-press reload (UpdateBanner, update.ts), never reloading by itself. * * vite.console.config.ts writes both files next to the page with the page's * hash in them (a new build = a new cache), and payload/Makefile embeds them in @@ -20,6 +20,8 @@ * main.tsx shows the offline screen (OfflinePage) and reloads once it answers. */ +import { markUpdateReady } from "./update"; + /** The service worker. `__CW_SW_V__` is replaced with the page's build hash. */ export const SW_SOURCE = `/* PS5Upload Files: offline copy of the page (generated; see offlineCopy.ts) */ "use strict"; @@ -82,9 +84,6 @@ export function serviceWorkerSource(version: string): string { return SW_SOURCE.split("__CW_SW_V__").join(version); } -/** An appcache update this soon after the page opened reloads into it; later it waits for the next open. */ -export const APPCACHE_RELOAD_WINDOW_MS = 20_000; - interface AppCacheLike { addEventListener(type: "updateready", fn: () => void): void; swapCache?: () => void; @@ -95,7 +94,7 @@ interface AppCacheLike { * console's own browser at 127.0.0.1, not a phone at the console's LAN address) and follow the * appcache's updates. Never throws; nothing here is needed for the page to work. */ -export function installOfflineCopy(win: Window = window, startedAt: number = Date.now()): void { +export function installOfflineCopy(win: Window = window, onUpdate: () => void = markUpdateReady): void { try { const nav = win.navigator as Navigator & { serviceWorker?: ServiceWorkerContainer }; if (win.isSecureContext && nav.serviceWorker && typeof nav.serviceWorker.register === "function") { @@ -108,13 +107,12 @@ export function installOfflineCopy(win: Window = window, startedAt: number = Dat const ac = (win as unknown as { applicationCache?: AppCacheLike }).applicationCache; if (ac && typeof ac.addEventListener === "function") { ac.addEventListener("updateready", () => { - if (Date.now() - startedAt > APPCACHE_RELOAD_WINDOW_MS) return; try { ac.swapCache?.(); } catch { /* already swapped */ } - win.location.reload(); + onUpdate(); // the banner: one press reloads into the new page }); } } catch { diff --git a/client/src/console/offline/update.test.tsx b/client/src/console/offline/update.test.tsx new file mode 100644 index 00000000..32d21dc1 --- /dev/null +++ b/client/src/console/offline/update.test.tsx @@ -0,0 +1,28 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { beforeEach, describe, expect, it } from "vitest"; +import { t } from "../strings"; +import { UpdateBanner } from "./UpdateBanner"; +import { _resetUpdateForTests, dismissUpdate, isNewerHelper, markUpdateReady } from "./update"; + +beforeEach(() => _resetUpdateForTests()); + +describe("updated page banner", () => { + it("a helper version other than the page's means a newer page; unknowns are never compared", () => { + expect(isNewerHelper("7.1.0", "7.0.34")).toBe(true); + expect(isNewerHelper("7.0.34", "7.0.34")).toBe(false); + expect(isNewerHelper(undefined, "7.0.34")).toBe(false); // an older helper's reply + expect(isNewerHelper("", "7.0.34")).toBe(false); + expect(isNewerHelper("7.1.0", "")).toBe(false); // tests / dev build + }); + + it("shows a one-press Reload (and Later) only once marked, and Later hides it", () => { + expect(renderToStaticMarkup()).toBe(""); + markUpdateReady(); + const html = renderToStaticMarkup(); + expect(html).toContain(t("cw_update_ready")); + expect(html).toContain(t("cw_error_reload")); + expect(html).toContain(t("cw_update_later")); + dismissUpdate(); + expect(renderToStaticMarkup()).toBe(""); + }); +}); diff --git a/client/src/console/offline/update.ts b/client/src/console/offline/update.ts new file mode 100644 index 00000000..affd2384 --- /dev/null +++ b/client/src/console/offline/update.ts @@ -0,0 +1,47 @@ +/** + * "PS5Upload Files was updated": the page shown is older than the helper's. + * That happens when the browser served its offline copy (the appcache serves + * its copy first and fetches the new page behind it: "updateready"), or when + * the helper was updated under an open page. Either way the page offers a + * one-press Reload (UpdateBanner) instead of reloading by itself mid-task. + */ +import { useSyncExternalStore } from "react"; + +/** The version this page was built from ("" under tests: never compared). */ +export const PAGE_VERSION: string = typeof __CW_APP_V__ === "string" ? __CW_APP_V__ : ""; + +let ready = false; +const listeners = new Set<() => void>(); + +/** A newer page is waiting: show the banner (once). */ +export function markUpdateReady(): void { + if (ready) return; + ready = true; + listeners.forEach((fn) => fn()); +} + +/** The helper's version (/api/settings) against this page's; a difference means a newer page. */ +export function isNewerHelper(helperVersion: unknown, page: string = PAGE_VERSION): boolean { + return !!page && typeof helperVersion === "string" && !!helperVersion && helperVersion !== page; +} + +/** Later: hide the banner for this page's life. */ +export function dismissUpdate(): void { + if (!ready) return; + ready = false; + listeners.forEach((fn) => fn()); +} + +function subscribe(fn: () => void): () => void { + listeners.add(fn); + return () => { + listeners.delete(fn); + }; +} + +export const useUpdateReady = (): boolean => useSyncExternalStore(subscribe, () => ready, () => ready); + +/** Tests. */ +export function _resetUpdateForTests(): void { + ready = false; +} diff --git a/client/src/console/ui/PhoneHandoff.test.tsx b/client/src/console/ui/PhoneHandoff.test.tsx index 918e6077..5f3ca66a 100644 --- a/client/src/console/ui/PhoneHandoff.test.tsx +++ b/client/src/console/ui/PhoneHandoff.test.tsx @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { phoneUrl } from "./PhoneHandoff"; +import { onConsoleBrowser, phoneUrl } from "./PhoneHandoff"; describe("continue on your phone", () => { it("is this page on the console's LAN address while LAN access is on", () => { @@ -15,4 +15,11 @@ describe("continue on your phone", () => { expect(phoneUrl({ lan: true, addr: "evil.example/x?" }, "9180")).toBeNull(); expect(phoneUrl(null, "9180")).toBeNull(); }); + + it("is offered only in the console's own browser, not on a phone or PC already", () => { + expect(onConsoleBrowser("127.0.0.1")).toBe(true); + expect(onConsoleBrowser("localhost")).toBe(true); + expect(onConsoleBrowser("192.168.86.99")).toBe(false); + expect(onConsoleBrowser("")).toBe(false); + }); }); diff --git a/client/src/console/ui/PhoneHandoff.tsx b/client/src/console/ui/PhoneHandoff.tsx index 0b6202c0..059d49b0 100644 --- a/client/src/console/ui/PhoneHandoff.tsx +++ b/client/src/console/ui/PhoneHandoff.tsx @@ -17,6 +17,11 @@ export function phoneUrl(info: LanInfo | null, port: string): string | null { return `http://${info.addr}:${p}/`; } +/** The console's own browser (the page at 127.0.0.1): a phone or PC already has a keyboard. */ +export function onConsoleBrowser(host: string = typeof location !== "undefined" ? location.hostname : ""): boolean { + return host === "127.0.0.1" || host === "localhost"; +} + /** Read once per page and kept briefly: every text dialog asks, the answer rarely changes. */ const KEEP_MS = 30_000; let cached: { at: number; url: string | null } | null = null; @@ -53,11 +58,14 @@ export function forgetPhoneUrl(): void { * "Continue on your phone": in dialogs where the user types (rename, bulk * rename, go to path), when LAN access is on, a QR code and the address of this * page on the console's IP, so the typing can happen on a phone. Nothing shows - * while LAN access is off (the address would not answer). + * while LAN access is off (the address would not answer), nor when the page is + * already open on a phone or PC (not at 127.0.0.1). */ -export function PhoneHandoff() { - const [url, setUrl] = useState(cached?.url ?? null); +export function PhoneHandoff({ host }: { host?: string } = {}) { + const here = onConsoleBrowser(host); + const [url, setUrl] = useState(here ? (cached?.url ?? null) : null); useEffect(() => { + if (!here) return; let live = true; void loadPhoneUrl().then((u) => { if (live) setUrl(u); @@ -65,7 +73,7 @@ export function PhoneHandoff() { return () => { live = false; }; - }, []); + }, [here]); const path = useMemo(() => { if (!url) return null; try { @@ -75,7 +83,7 @@ export function PhoneHandoff() { return null; } }, [url]); - if (!url || !path) return null; + if (!here || !url || !path) return null; return (
diff --git a/client/src/i18n/locales/ar.ts b/client/src/i18n/locales/ar.ts index eb2ba252..08369403 100644 --- a/client/src/i18n/locales/ar.ts +++ b/client/src/i18n/locales/ar.ts @@ -4074,6 +4074,8 @@ c2c_test_deep_bad: "لم يصل الإرسال التجريبي بحجم 4 KiB: "cw_phone_qr_label": "رمز QR لعنوان هذه الصفحة", "cw_files_fixperm_many": "إصلاح أذونات {n} عناصر", "cw_cheats_err_busy": "يغيّر تطبيق آخر غشّ هذه اللعبة الآن. حاول مرة أخرى بعد قليل.", +"cw_update_ready": "تم تحديث PS5Upload Files. أعد التحميل لاستخدام الإصدار الجديد.", +"cw_update_later": "لاحقًا", "cw_hints_label": "أزرار وحدة التحكم", "cw_task_from_console": "من هذا الجهاز", "cw_task_from_pc": "من حاسوب", diff --git a/client/src/i18n/locales/bn.ts b/client/src/i18n/locales/bn.ts index 05c7c51b..1b29a8f8 100644 --- a/client/src/i18n/locales/bn.ts +++ b/client/src/i18n/locales/bn.ts @@ -4074,6 +4074,8 @@ c2c_test_deep_bad: "4 KiB-এর পরীক্ষামূলক পাঠা "cw_phone_qr_label": "এই পাতার ঠিকানার QR কোড", "cw_files_fixperm_many": "{n}টি আইটেমের অনুমতি ঠিক করুন", "cw_cheats_err_busy": "অন্য একটি অ্যাপ এখন এই গেমের চিট বদলাচ্ছে। একটু পরে আবার চেষ্টা করুন।", +"cw_update_ready": "PS5Upload Files আপডেট হয়েছে। নতুন সংস্করণ ব্যবহার করতে আবার লোড করুন।", +"cw_update_later": "পরে", "cw_hints_label": "কন্ট্রোলারের বোতাম", "cw_task_from_console": "এই কনসোল থেকে", "cw_task_from_pc": "PC থেকে", diff --git a/client/src/i18n/locales/de.ts b/client/src/i18n/locales/de.ts index f9662fd9..30923855 100644 --- a/client/src/i18n/locales/de.ts +++ b/client/src/i18n/locales/de.ts @@ -4082,6 +4082,8 @@ c2c_test_deep_bad: "Die 4-KiB-Testsendung ist nicht angekommen: {detail}", "cw_phone_qr_label": "QR-Code der Adresse dieser Seite", "cw_files_fixperm_many": "Berechtigungen von {n} Elementen reparieren", "cw_cheats_err_busy": "Eine andere App ändert gerade die Cheats dieses Spiels. Versuche es gleich noch einmal.", +"cw_update_ready": "PS5Upload Files wurde aktualisiert. Lade neu, um die neue Version zu verwenden.", +"cw_update_later": "Später", "cw_hints_label": "Controller-Tasten", "cw_task_from_console": "Von dieser Konsole", "cw_task_from_pc": "Von einem PC", diff --git a/client/src/i18n/locales/en.ts b/client/src/i18n/locales/en.ts index 55612f81..9d680f52 100644 --- a/client/src/i18n/locales/en.ts +++ b/client/src/i18n/locales/en.ts @@ -4397,6 +4397,8 @@ c2c_test_deep_bad: "The 4 KiB test send did not arrive: {detail}", "cw_phone_qr_label": "QR code of this page's address", "cw_files_fixperm_many": "Fix permissions of {n} items", "cw_cheats_err_busy": "Another app is changing this game's cheats right now. Try again in a moment.", +"cw_update_ready": "PS5Upload Files was updated. Reload to use the new version.", +"cw_update_later": "Later", "cw_hints_label": "Controller buttons", "cw_task_from_console": "From this console", "cw_task_from_pc": "From a PC", diff --git a/client/src/i18n/locales/es.ts b/client/src/i18n/locales/es.ts index 1edfc581..fdb7db85 100644 --- a/client/src/i18n/locales/es.ts +++ b/client/src/i18n/locales/es.ts @@ -4024,6 +4024,8 @@ c2c_test_deep_bad: "El envío de prueba de 4 KiB no llegó: {detail}", "cw_phone_qr_label": "Código QR de la dirección de esta página", "cw_files_fixperm_many": "Corregir permisos de {n} elementos", "cw_cheats_err_busy": "Otra app está cambiando los trucos de este juego ahora mismo. Inténtalo de nuevo en un momento.", +"cw_update_ready": "PS5Upload Files se actualizó. Recarga para usar la nueva versión.", +"cw_update_later": "Más tarde", "cw_hints_label": "Botones del mando", "cw_task_from_console": "Desde esta consola", "cw_task_from_pc": "Desde un PC", diff --git a/client/src/i18n/locales/fa.ts b/client/src/i18n/locales/fa.ts index 325e0e22..af61b6d2 100644 --- a/client/src/i18n/locales/fa.ts +++ b/client/src/i18n/locales/fa.ts @@ -4375,6 +4375,8 @@ c2c_test_deep_bad: "ارسال آزمایشی ۴ KiB نرسید: {detail}", "cw_phone_qr_label": "کد QR نشانی این صفحه", "cw_files_fixperm_many": "اصلاح مجوزهای {n} مورد", "cw_cheats_err_busy": "برنامهٔ دیگری همین حالا تقلب‌های این بازی را تغییر می‌دهد. کمی بعد دوباره امتحان کنید.", +"cw_update_ready": "PS5Upload Files به‌روزرسانی شد. برای استفاده از نسخهٔ جدید، دوباره بارگیری کنید.", +"cw_update_later": "بعداً", "cw_hints_label": "دکمه‌های دسته", "cw_task_from_console": "از این کنسول", "cw_task_from_pc": "از رایانه", diff --git a/client/src/i18n/locales/fr.ts b/client/src/i18n/locales/fr.ts index 09878a0e..fc12d923 100644 --- a/client/src/i18n/locales/fr.ts +++ b/client/src/i18n/locales/fr.ts @@ -4092,6 +4092,8 @@ c2c_test_deep_bad: "L’envoi test de 4 Kio n’est pas arrivé : {detail}", "cw_phone_qr_label": "Code QR de l'adresse de cette page", "cw_files_fixperm_many": "Corriger les autorisations de {n} éléments", "cw_cheats_err_busy": "Une autre app modifie les codes de triche de ce jeu en ce moment. Réessayez dans un instant.", +"cw_update_ready": "PS5Upload Files a été mis à jour. Rechargez pour utiliser la nouvelle version.", +"cw_update_later": "Plus tard", "cw_hints_label": "Boutons de la manette", "cw_task_from_console": "Depuis cette console", "cw_task_from_pc": "Depuis un PC", diff --git a/client/src/i18n/locales/hi.ts b/client/src/i18n/locales/hi.ts index ae9ab7ef..6493bfca 100644 --- a/client/src/i18n/locales/hi.ts +++ b/client/src/i18n/locales/hi.ts @@ -4090,6 +4090,8 @@ c2c_test_deep_bad: "4 KiB का परीक्षण नहीं पहुँ "cw_phone_qr_label": "इस पेज के पते का QR कोड", "cw_files_fixperm_many": "{n} आइटम की अनुमतियाँ ठीक करें", "cw_cheats_err_busy": "कोई दूसरा ऐप अभी इस गेम के चीट बदल रहा है। थोड़ी देर में फिर से कोशिश करें।", +"cw_update_ready": "PS5Upload Files अपडेट हो गया है। नया संस्करण इस्तेमाल करने के लिए फिर से लोड करें।", +"cw_update_later": "बाद में", "cw_hints_label": "कंट्रोलर के बटन", "cw_task_from_console": "इस कंसोल से", "cw_task_from_pc": "PC से", diff --git a/client/src/i18n/locales/hu.ts b/client/src/i18n/locales/hu.ts index b973e79a..d0b75389 100644 --- a/client/src/i18n/locales/hu.ts +++ b/client/src/i18n/locales/hu.ts @@ -4389,6 +4389,8 @@ c2c_test_deep_bad: "A 4 KiB-os tesztküldés nem érkezett meg: {detail}", "cw_phone_qr_label": "Az oldal címének QR-kódja", "cw_files_fixperm_many": "{n} elem jogosultságainak javítása", "cw_cheats_err_busy": "Egy másik alkalmazás éppen most módosítja a játék csaláskódjait. Próbáld újra egy kicsit később.", +"cw_update_ready": "A PS5Upload Files frissült. Töltsd újra az új verzió használatához.", +"cw_update_later": "Később", "cw_hints_label": "Kontroller gombjai", "cw_task_from_console": "Erről a konzolról", "cw_task_from_pc": "Egy PC-ről", diff --git a/client/src/i18n/locales/id.ts b/client/src/i18n/locales/id.ts index ca53dac3..c54daf04 100644 --- a/client/src/i18n/locales/id.ts +++ b/client/src/i18n/locales/id.ts @@ -4083,6 +4083,8 @@ c2c_test_deep_bad: "Kiriman uji 4 KiB tidak tiba: {detail}", "cw_phone_qr_label": "Kode QR alamat halaman ini", "cw_files_fixperm_many": "Perbaiki izin {n} item", "cw_cheats_err_busy": "Aplikasi lain sedang mengubah cheat game ini sekarang. Coba lagi sebentar lagi.", +"cw_update_ready": "PS5Upload Files telah diperbarui. Muat ulang untuk memakai versi baru.", +"cw_update_later": "Nanti", "cw_hints_label": "Tombol kontroler", "cw_task_from_console": "Dari konsol ini", "cw_task_from_pc": "Dari PC", diff --git a/client/src/i18n/locales/it.ts b/client/src/i18n/locales/it.ts index 43695781..a58ab9d5 100644 --- a/client/src/i18n/locales/it.ts +++ b/client/src/i18n/locales/it.ts @@ -4024,6 +4024,8 @@ c2c_test_deep_bad: "L’invio di prova di 4 KiB non è arrivato: {detail}", "cw_phone_qr_label": "Codice QR dell'indirizzo di questa pagina", "cw_files_fixperm_many": "Correggi i permessi di {n} elementi", "cw_cheats_err_busy": "Un'altra app sta cambiando i trucchi di questo gioco proprio ora. Riprova tra un attimo.", +"cw_update_ready": "PS5Upload Files è stato aggiornato. Ricarica per usare la nuova versione.", +"cw_update_later": "Più tardi", "cw_hints_label": "Tasti del controller", "cw_task_from_console": "Da questa console", "cw_task_from_pc": "Da un PC", diff --git a/client/src/i18n/locales/ja.ts b/client/src/i18n/locales/ja.ts index 626ecdc2..7095e730 100644 --- a/client/src/i18n/locales/ja.ts +++ b/client/src/i18n/locales/ja.ts @@ -4095,6 +4095,8 @@ c2c_test_deep_bad: "4 KiB のテスト送信が届きませんでした: {detail "cw_phone_qr_label": "このページのアドレスの QR コード", "cw_files_fixperm_many": "{n} 件の権限を修正", "cw_cheats_err_busy": "別のアプリが今このゲームのチートを変更しています。少し待ってからもう一度お試しください。", +"cw_update_ready": "PS5Upload Files が更新されました。新しいバージョンを使うには再読み込みしてください。", +"cw_update_later": "後で", "cw_hints_label": "コントローラーのボタン", "cw_task_from_console": "このコンソールから", "cw_task_from_pc": "PCから", diff --git a/client/src/i18n/locales/ko.ts b/client/src/i18n/locales/ko.ts index 29f3321a..d6f00d51 100644 --- a/client/src/i18n/locales/ko.ts +++ b/client/src/i18n/locales/ko.ts @@ -4093,6 +4093,8 @@ c2c_test_deep_bad: "4 KiB 테스트 전송이 도착하지 않았습니다: {det "cw_phone_qr_label": "이 페이지 주소의 QR 코드", "cw_files_fixperm_many": "항목 {n}개의 권한 수정", "cw_cheats_err_busy": "다른 앱이 지금 이 게임의 치트를 바꾸고 있습니다. 잠시 후 다시 시도하세요.", +"cw_update_ready": "PS5Upload Files가 업데이트되었습니다. 새 버전을 사용하려면 다시 불러오세요.", +"cw_update_later": "나중에", "cw_hints_label": "컨트롤러 버튼", "cw_task_from_console": "이 콘솔에서", "cw_task_from_pc": "PC에서", diff --git a/client/src/i18n/locales/pl.ts b/client/src/i18n/locales/pl.ts index 6b32033a..c3ecce18 100644 --- a/client/src/i18n/locales/pl.ts +++ b/client/src/i18n/locales/pl.ts @@ -4329,6 +4329,8 @@ c2c_test_deep_bad: "Testowa wysyłka 4 KiB nie dotarła: {detail}", "cw_phone_qr_label": "Kod QR adresu tej strony", "cw_files_fixperm_many": "Napraw uprawnienia {n} elementów", "cw_cheats_err_busy": "Inna aplikacja właśnie zmienia kody tej gry. Spróbuj ponownie za chwilę.", +"cw_update_ready": "PS5Upload Files został zaktualizowany. Wczytaj ponownie, aby użyć nowej wersji.", +"cw_update_later": "Później", "cw_hints_label": "Przyciski kontrolera", "cw_task_from_console": "Z tej konsoli", "cw_task_from_pc": "Z komputera", diff --git a/client/src/i18n/locales/pt-BR.ts b/client/src/i18n/locales/pt-BR.ts index cc331cb0..8d0bf7ae 100644 --- a/client/src/i18n/locales/pt-BR.ts +++ b/client/src/i18n/locales/pt-BR.ts @@ -4091,6 +4091,8 @@ c2c_test_deep_bad: "O envio de teste de 4 KiB não chegou: {detail}", "cw_phone_qr_label": "Código QR do endereço desta página", "cw_files_fixperm_many": "Corrigir permissões de {n} itens", "cw_cheats_err_busy": "Outro app está alterando as trapaças deste jogo agora. Tente de novo em um instante.", +"cw_update_ready": "O PS5Upload Files foi atualizado. Recarregue para usar a nova versão.", +"cw_update_later": "Depois", "cw_hints_label": "Botões do controle", "cw_task_from_console": "Deste console", "cw_task_from_pc": "De um PC", diff --git a/client/src/i18n/locales/ru.ts b/client/src/i18n/locales/ru.ts index e868584d..076815e8 100644 --- a/client/src/i18n/locales/ru.ts +++ b/client/src/i18n/locales/ru.ts @@ -4024,6 +4024,8 @@ c2c_test_deep_bad: "Тестовая отправка 4 КиБ не дошла: "cw_phone_qr_label": "QR-код адреса этой страницы", "cw_files_fixperm_many": "Исправить права у {n} элементов", "cw_cheats_err_busy": "Другое приложение прямо сейчас меняет читы этой игры. Попробуйте ещё раз через пару секунд.", +"cw_update_ready": "PS5Upload Files обновлён. Перезагрузите страницу, чтобы использовать новую версию.", +"cw_update_later": "Позже", "cw_hints_label": "Кнопки геймпада", "cw_task_from_console": "С этой консоли", "cw_task_from_pc": "С ПК", diff --git a/client/src/i18n/locales/th.ts b/client/src/i18n/locales/th.ts index 0a6e2510..5b8701b1 100644 --- a/client/src/i18n/locales/th.ts +++ b/client/src/i18n/locales/th.ts @@ -4083,6 +4083,8 @@ c2c_test_deep_bad: "การส่งทดสอบ 4 KiB ไปไม่ถ "cw_phone_qr_label": "คิวอาร์โค้ดของที่อยู่หน้านี้", "cw_files_fixperm_many": "แก้ไขสิทธิ์ของ {n} รายการ", "cw_cheats_err_busy": "มีแอปอื่นกำลังเปลี่ยนสูตรโกงของเกมนี้อยู่ ลองอีกครั้งในอีกสักครู่", +"cw_update_ready": "PS5Upload Files ได้รับการอัปเดตแล้ว โหลดใหม่เพื่อใช้เวอร์ชันใหม่", +"cw_update_later": "ภายหลัง", "cw_hints_label": "ปุ่มคอนโทรลเลอร์", "cw_task_from_console": "จากคอนโซลนี้", "cw_task_from_pc": "จาก PC", diff --git a/client/src/i18n/locales/tr.ts b/client/src/i18n/locales/tr.ts index abed0183..3acd64a7 100644 --- a/client/src/i18n/locales/tr.ts +++ b/client/src/i18n/locales/tr.ts @@ -4081,6 +4081,8 @@ c2c_test_deep_bad: "4 KiB'lik test gönderimi ulaşmadı: {detail}", "cw_phone_qr_label": "Bu sayfanın adresinin QR kodu", "cw_files_fixperm_many": "{n} öğenin izinlerini düzelt", "cw_cheats_err_busy": "Başka bir uygulama şu anda bu oyunun hilelerini değiştiriyor. Birazdan yeniden deneyin.", +"cw_update_ready": "PS5Upload Files güncellendi. Yeni sürümü kullanmak için yeniden yükleyin.", +"cw_update_later": "Sonra", "cw_hints_label": "Kumanda düğmeleri", "cw_task_from_console": "Bu konsoldan", "cw_task_from_pc": "Bir PC'den", diff --git a/client/src/i18n/locales/vi.ts b/client/src/i18n/locales/vi.ts index 14debc14..141cac76 100644 --- a/client/src/i18n/locales/vi.ts +++ b/client/src/i18n/locales/vi.ts @@ -4076,6 +4076,8 @@ c2c_test_deep_bad: "Lượt gửi thử 4 KiB không tới: {detail}", "cw_phone_qr_label": "Mã QR địa chỉ của trang này", "cw_files_fixperm_many": "Sửa quyền của {n} mục", "cw_cheats_err_busy": "Một ứng dụng khác đang thay đổi cheat của trò chơi này. Hãy thử lại sau giây lát.", +"cw_update_ready": "PS5Upload Files đã được cập nhật. Hãy tải lại để dùng phiên bản mới.", +"cw_update_later": "Để sau", "cw_hints_label": "Nút tay cầm", "cw_task_from_console": "Từ máy này", "cw_task_from_pc": "Từ PC", diff --git a/client/src/i18n/locales/zh-CN.ts b/client/src/i18n/locales/zh-CN.ts index 7da9a897..2b05b2c2 100644 --- a/client/src/i18n/locales/zh-CN.ts +++ b/client/src/i18n/locales/zh-CN.ts @@ -4030,6 +4030,8 @@ c2c_test_deep_bad: "4 KiB 测试发送未到达:{detail}", "cw_phone_qr_label": "此页面地址的二维码", "cw_files_fixperm_many": "修复 {n} 项的权限", "cw_cheats_err_busy": "另一个应用正在更改此游戏的金手指。请稍后再试。", +"cw_update_ready": "PS5Upload Files 已更新。请重新加载以使用新版本。", +"cw_update_later": "稍后", "cw_hints_label": "手柄按键", "cw_task_from_console": "来自本主机", "cw_task_from_pc": "来自电脑", diff --git a/client/src/i18n/locales/zh-TW.ts b/client/src/i18n/locales/zh-TW.ts index 9b10a700..eb8e1293 100644 --- a/client/src/i18n/locales/zh-TW.ts +++ b/client/src/i18n/locales/zh-TW.ts @@ -4024,6 +4024,8 @@ c2c_test_deep_bad: "4 KiB 測試傳送未抵達:{detail}", "cw_phone_qr_label": "此頁面位址的 QR 碼", "cw_files_fixperm_many": "修正 {n} 個項目的權限", "cw_cheats_err_busy": "另一個應用程式正在變更此遊戲的金手指。請稍後再試。", +"cw_update_ready": "PS5Upload Files 已更新。請重新載入以使用新版本。", +"cw_update_later": "稍後", "cw_hints_label": "控制器按鍵", "cw_task_from_console": "來自本主機", "cw_task_from_pc": "來自電腦", diff --git a/client/vite.console.config.ts b/client/vite.console.config.ts index 28aca74d..7c7a65a9 100644 --- a/client/vite.console.config.ts +++ b/client/vite.console.config.ts @@ -202,7 +202,11 @@ function escapeRe(s: string): string { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } +/** The app version the page was built with (package.json; the helper's /api/settings version is the same). */ +const APP_VERSION = (JSON.parse(readFileSync(fileURLToPath(new URL("./package.json", import.meta.url)), "utf8")) as { version: string }).version; + export default defineConfig({ + define: { __CW_APP_V__: JSON.stringify(APP_VERSION) }, root: fileURLToPath(new URL("./src/console", import.meta.url)), base: "./", publicDir: false, diff --git a/payload/src/cheats.c b/payload/src/cheats.c index 0ecab327..f4c4d67c 100644 --- a/payload/src/cheats.c +++ b/payload/src/cheats.c @@ -1597,16 +1597,19 @@ int cheats_toggle(const char *title_id, int mod_index, int turn_on, * webui_cheats_toggle_error matches these texts; keep them in step) says each differently: * - the game exited a moment ago (the 2 s running-game cache still had its pid): the same * text as no game at all, and the cache is dropped so the next status read sees it gone; - * - the game is alive but another debugger is attached (the PC app toggling at the same - * moment, or another cheat tool): "is attached to the game". */ + * - the game is alive but another debugger is attached (EBUSY: the PC app toggling at the + * same moment, or another cheat tool): "is attached to the game"; + * - anything else: the plain "pt_attach failed (errno=N)". */ if (pt_attach(pid) != 0) { int e = errno; free(target_cf); invalidate_rg_cache(); if (kill(pid, 0) != 0 && errno == ESRCH) { if (err) snprintf(err, err_cap, "no game is currently running"); + } else if (e == EBUSY) { + if (err) snprintf(err, err_cap, "pt_attach failed: another debugger or cheat tool is attached to the game (errno=%d)", e); } else if (err) { - snprintf(err, err_cap, "pt_attach failed: another debugger or cheat tool is attached to the game (errno=%d)", e); + snprintf(err, err_cap, "pt_attach failed (errno=%d)", e); } return -1; } diff --git a/payload/src/webui_server.c b/payload/src/webui_server.c index 60423a96..b6536ca9 100644 --- a/payload/src/webui_server.c +++ b/payload/src/webui_server.c @@ -138,11 +138,13 @@ static int thread_start(pthread_t *t, void *(*fn)(void *), void *arg) { return rc == 0 ? 0 : -1; } -/* The console's IPv4 addresses, for the LAN Host check. */ -static size_t own_addrs(char out[WEBUI_MAX_ADDRS][INET_ADDRSTRLEN]) { +/* The console's IPv4 addresses, for the LAN Host check; -1 when they could not be read + * (getifaddrs failed: the caller keeps the list it has, so a passing failure never looks like an + * address change). */ +static int own_addrs(char out[WEBUI_MAX_ADDRS][INET_ADDRSTRLEN]) { struct ifaddrs *ifa = NULL, *p; - size_t n = 0; - if (getifaddrs(&ifa) != 0) return 0; + int n = 0; + if (getifaddrs(&ifa) != 0) return -1; for (p = ifa; p && n < WEBUI_MAX_ADDRS; p = p->ifa_next) { if (!p->ifa_addr || p->ifa_addr->sa_family != AF_INET) continue; if (!inet_ntop(AF_INET, &((struct sockaddr_in *)(void *)p->ifa_addr)->sin_addr, @@ -484,7 +486,7 @@ static void watch_listener(srv_t *s, uint64_t *mono_prev, int64_t *real_prev) { int64_t real = real_ms(); int resumed, lan, changed = 0; char addrs[WEBUI_MAX_ADDRS][INET_ADDRSTRLEN]; - size_t n = 0; + int n = -1; resumed = webui_watch_resumed(*mono_prev, mono, *real_prev, real); *mono_prev = mono; *real_prev = real; @@ -493,16 +495,17 @@ static void watch_listener(srv_t *s, uint64_t *mono_prev, int64_t *real_prev) { pthread_mutex_unlock(&s->mu); if (lan) n = own_addrs(addrs); pthread_mutex_lock(&s->mu); - if (lan && s->lan && webui_addrs_differ((const char (*)[INET_ADDRSTRLEN])s->addrs, s->n_addrs, - (const char (*)[INET_ADDRSTRLEN])addrs, n)) { + if (lan && s->lan && n >= 0 && + webui_addrs_differ((const char (*)[INET_ADDRSTRLEN])s->addrs, s->n_addrs, + (const char (*)[INET_ADDRSTRLEN])addrs, (size_t)n)) { memcpy(s->addrs, addrs, sizeof addrs); - s->n_addrs = n; + s->n_addrs = (size_t)n; changed = 1; } if ((resumed || changed) && s->listen_fd >= 0) drop_listener_locked(s); pthread_mutex_unlock(&s->mu); if (resumed) fprintf(stderr, "webui: resumed (clock gap); rebinding port %u\n", (unsigned)s->port); - if (changed) fprintf(stderr, "webui: console addresses changed (%u now); rebinding port %u\n", (unsigned)n, (unsigned)s->port); + if (changed) fprintf(stderr, "webui: console addresses changed (%d now); rebinding port %u\n", n, (unsigned)s->port); } static void *accept_main(void *arg) { @@ -592,11 +595,16 @@ static void *accept_main(void *arg) { /* Never leave the loop on an accept error (a stray errno once killed a helper's * server); back off briefly on the ones that would otherwise spin, and rebind when they * go on for WEBUI_BROKEN_MS. */ - if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR && errno != ECONNABORTED) { + int e = errno; + if (e == EMFILE || e == ENFILE || e == ENOBUFS || e == ENOMEM) { + /* Out of descriptors or buffers: the listener is fine, the process or system is + * busy. Back off; a rebind would not help and must not be counted toward one. */ + nap_ms(50); + } else if (e != EAGAIN && e != EWOULDBLOCK && e != EINTR && e != ECONNABORTED) { if (!err_since) err_since = mono_ms(); else if (webui_watch_broken(err_since, mono_ms())) { fprintf(stderr, "webui: accept on port %u failing for %u ms (errno %d); rebinding\n", - (unsigned)s->port, (unsigned)WEBUI_BROKEN_MS, errno); + (unsigned)s->port, (unsigned)WEBUI_BROKEN_MS, e); pthread_mutex_lock(&s->mu); if (s->listen_fd == fd) drop_listener_locked(s); pthread_mutex_unlock(&s->mu); @@ -714,7 +722,10 @@ int webui_start(const webui_cfg_t *cfg) { s->route = cfg->route; s->lan = cfg->lan ? 1 : 0; webui_token_new(s->token); - if (s->lan) s->n_addrs = own_addrs(s->addrs); + if (s->lan) { + int got = own_addrs(s->addrs); + s->n_addrs = got > 0 ? (size_t)got : 0; + } /* First bind here so the server answers as soon as start returns; on failure the accept * thread retries (a takeover's old instance may still hold the port for a moment). */ @@ -788,9 +799,12 @@ int webui_restart(int lan) { size_t n = 0; unsigned gen; uint64_t end; - int bound = 0; + int bound = 0, got = -1; - if (lan) n = own_addrs(addrs); + if (lan) { + got = own_addrs(addrs); + n = got > 0 ? (size_t)got : 0; + } pthread_mutex_lock(&g_life); s = g_srv; if (!s) { @@ -799,8 +813,10 @@ int webui_restart(int lan) { } pthread_mutex_lock(&s->mu); s->lan = lan ? 1 : 0; - s->n_addrs = n; - if (n) memcpy(s->addrs, addrs, sizeof addrs); + if (!lan || got >= 0) { /* a failed read keeps the list it had */ + s->n_addrs = n; + if (n) memcpy(s->addrs, addrs, sizeof addrs); + } s->rebind_req = 1; gen = s->bind_gen; s->refs++;