mirror of
https://github.com/phantomptr/ps5upload.git
synced 2026-10-19 02:20:02 -05:00
console page: F8 review fixes
- cheats: "attached to the game" (busy) only for EBUSY; other attach failures stay a plain "pt_attach failed (errno=N)". - Continue on your phone shows only in the console's own browser (127.0.0.1/localhost); a gap above the dialog buttons. - The token read gives up after 4 s (offline screen); a small splash instead of a blank page while it is asked. - No silent reload: an appcache update, or a helper version newer than the page's build, shows a one-press "updated: Reload" banner (Later hides it). cw_update_* in 21 locales. - own_addrs reports a getifaddrs failure and the list is kept (no flapping); EMFILE/ENFILE/ENOBUFS/ENOMEM never count toward a broken listener. - backHistory header: Escape/○ at Home closes the browser on the PS5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RxCGb8Asp1A1AQYiv1kbgS
This commit is contained in:
1 parent
f77c33e579
commit
831018b04d
58 files changed
+353
-72
No files matched your search
@@ -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;
|
globalThis.fetch = (async () => jsonResponse(500, { error: "x" })) as typeof fetch;
|
||||||
expect(await api.probeHelper()).toBe(true);
|
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<Response>((_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();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -39,12 +39,19 @@ export function onTokenExpired(fn: () => void): () => void {
|
|||||||
|
|
||||||
let tokenPromise: Promise<string> | null = null;
|
let tokenPromise: Promise<string> | 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<string> {
|
async function fetchToken(): Promise<string> {
|
||||||
let res: Response;
|
let res: Response;
|
||||||
|
const stop = typeof AbortController === "function" ? new AbortController() : null;
|
||||||
|
const timer = setTimeout(() => stop?.abort(), TOKEN_TIMEOUT_MS);
|
||||||
try {
|
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 {
|
} catch {
|
||||||
throw new ApiError(0, "offline");
|
throw new ApiError(0, "offline");
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
}
|
}
|
||||||
if (!res.ok) throw new ApiError(res.status, res.status === 503 ? "stopping" : "token_unavailable");
|
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.
|
// Anything but JSON (another server on the port, a captive page) is not the helper.
|
||||||
|
|||||||
@@ -21,7 +21,9 @@
|
|||||||
*
|
*
|
||||||
* On the PS5 (opts.onRoot) the guard stays up on the first screen too, and ○
|
* 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
|
* 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.
|
* 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
|
* WebKit can fire a popstate as the page loads that pops nothing. Such a pop
|
||||||
|
|||||||
@@ -1497,7 +1497,7 @@ button {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.9rem;
|
gap: 0.9rem;
|
||||||
margin: 0.9rem 0 0;
|
margin: 0.9rem 0 1rem;
|
||||||
padding: 0.6rem;
|
padding: 0.6rem;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: var(--color-surface-3);
|
background: var(--color-surface-3);
|
||||||
@@ -1522,3 +1522,14 @@ button {
|
|||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
overflow-wrap: anywhere;
|
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;
|
||||||
|
}
|
||||||
Vendored
+2
@@ -2,3 +2,5 @@
|
|||||||
declare module "virtual:pc-tokens.css";
|
declare module "virtual:pc-tokens.css";
|
||||||
/** The strings' build hash, defined by vite.console.config.ts (consoleStrings); absent under vitest. */
|
/** The strings' build hash, defined by vite.console.config.ts (consoleStrings); absent under vitest. */
|
||||||
declare const __CW_STRINGS_V__: string | undefined;
|
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;
|
||||||
@@ -414,5 +414,7 @@
|
|||||||
"cw_tour_label": "نصائح",
|
"cw_tour_label": "نصائح",
|
||||||
"cw_tour_next": "التالي",
|
"cw_tour_next": "التالي",
|
||||||
"cw_tour_skip": "تخطٍّ",
|
"cw_tour_skip": "تخطٍّ",
|
||||||
"cw_tour_step": "{n} من {total}"
|
"cw_tour_step": "{n} من {total}",
|
||||||
|
"cw_update_later": "لاحقًا",
|
||||||
|
"cw_update_ready": "تم تحديث PS5Upload Files. أعد التحميل لاستخدام الإصدار الجديد."
|
||||||
}
|
}
|
||||||
@@ -414,5 +414,7 @@
|
|||||||
"cw_tour_label": "টিপস",
|
"cw_tour_label": "টিপস",
|
||||||
"cw_tour_next": "পরের",
|
"cw_tour_next": "পরের",
|
||||||
"cw_tour_skip": "এড়িয়ে যান",
|
"cw_tour_skip": "এড়িয়ে যান",
|
||||||
"cw_tour_step": "{total}-এর মধ্যে {n}"
|
"cw_tour_step": "{total}-এর মধ্যে {n}",
|
||||||
|
"cw_update_later": "পরে",
|
||||||
|
"cw_update_ready": "PS5Upload Files আপডেট হয়েছে। নতুন সংস্করণ ব্যবহার করতে আবার লোড করুন।"
|
||||||
}
|
}
|
||||||
@@ -414,5 +414,7 @@
|
|||||||
"cw_tour_label": "Tipps",
|
"cw_tour_label": "Tipps",
|
||||||
"cw_tour_next": "Weiter",
|
"cw_tour_next": "Weiter",
|
||||||
"cw_tour_skip": "Überspringen",
|
"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."
|
||||||
}
|
}
|
||||||
@@ -414,5 +414,7 @@
|
|||||||
"cw_tour_label": "Tips",
|
"cw_tour_label": "Tips",
|
||||||
"cw_tour_next": "Next",
|
"cw_tour_next": "Next",
|
||||||
"cw_tour_skip": "Skip",
|
"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."
|
||||||
}
|
}
|
||||||
@@ -414,5 +414,7 @@
|
|||||||
"cw_tour_label": "Consejos",
|
"cw_tour_label": "Consejos",
|
||||||
"cw_tour_next": "Siguiente",
|
"cw_tour_next": "Siguiente",
|
||||||
"cw_tour_skip": "Omitir",
|
"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."
|
||||||
}
|
}
|
||||||
@@ -414,5 +414,7 @@
|
|||||||
"cw_tour_label": "نکتهها",
|
"cw_tour_label": "نکتهها",
|
||||||
"cw_tour_next": "بعدی",
|
"cw_tour_next": "بعدی",
|
||||||
"cw_tour_skip": "رد شدن",
|
"cw_tour_skip": "رد شدن",
|
||||||
"cw_tour_step": "{n} از {total}"
|
"cw_tour_step": "{n} از {total}",
|
||||||
|
"cw_update_later": "بعداً",
|
||||||
|
"cw_update_ready": "PS5Upload Files بهروزرسانی شد. برای استفاده از نسخهٔ جدید، دوباره بارگیری کنید."
|
||||||
}
|
}
|
||||||
@@ -414,5 +414,7 @@
|
|||||||
"cw_tour_label": "Astuces",
|
"cw_tour_label": "Astuces",
|
||||||
"cw_tour_next": "Suivant",
|
"cw_tour_next": "Suivant",
|
||||||
"cw_tour_skip": "Passer",
|
"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."
|
||||||
}
|
}
|
||||||
@@ -414,5 +414,7 @@
|
|||||||
"cw_tour_label": "सुझाव",
|
"cw_tour_label": "सुझाव",
|
||||||
"cw_tour_next": "आगे",
|
"cw_tour_next": "आगे",
|
||||||
"cw_tour_skip": "छोड़ें",
|
"cw_tour_skip": "छोड़ें",
|
||||||
"cw_tour_step": "{total} में से {n}"
|
"cw_tour_step": "{total} में से {n}",
|
||||||
|
"cw_update_later": "बाद में",
|
||||||
|
"cw_update_ready": "PS5Upload Files अपडेट हो गया है। नया संस्करण इस्तेमाल करने के लिए फिर से लोड करें।"
|
||||||
}
|
}
|
||||||
@@ -414,5 +414,7 @@
|
|||||||
"cw_tour_label": "Tippek",
|
"cw_tour_label": "Tippek",
|
||||||
"cw_tour_next": "Tovább",
|
"cw_tour_next": "Tovább",
|
||||||
"cw_tour_skip": "Kihagyás",
|
"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."
|
||||||
}
|
}
|
||||||
@@ -414,5 +414,7 @@
|
|||||||
"cw_tour_label": "Tips",
|
"cw_tour_label": "Tips",
|
||||||
"cw_tour_next": "Berikutnya",
|
"cw_tour_next": "Berikutnya",
|
||||||
"cw_tour_skip": "Lewati",
|
"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."
|
||||||
}
|
}
|
||||||
@@ -414,5 +414,7 @@
|
|||||||
"cw_tour_label": "Suggerimenti",
|
"cw_tour_label": "Suggerimenti",
|
||||||
"cw_tour_next": "Avanti",
|
"cw_tour_next": "Avanti",
|
||||||
"cw_tour_skip": "Salta",
|
"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."
|
||||||
}
|
}
|
||||||
@@ -414,5 +414,7 @@
|
|||||||
"cw_tour_label": "ヒント",
|
"cw_tour_label": "ヒント",
|
||||||
"cw_tour_next": "次へ",
|
"cw_tour_next": "次へ",
|
||||||
"cw_tour_skip": "スキップ",
|
"cw_tour_skip": "スキップ",
|
||||||
"cw_tour_step": "{n} / {total}"
|
"cw_tour_step": "{n} / {total}",
|
||||||
|
"cw_update_later": "後で",
|
||||||
|
"cw_update_ready": "PS5Upload Files が更新されました。新しいバージョンを使うには再読み込みしてください。"
|
||||||
}
|
}
|
||||||
@@ -414,5 +414,7 @@
|
|||||||
"cw_tour_label": "팁",
|
"cw_tour_label": "팁",
|
||||||
"cw_tour_next": "다음",
|
"cw_tour_next": "다음",
|
||||||
"cw_tour_skip": "건너뛰기",
|
"cw_tour_skip": "건너뛰기",
|
||||||
"cw_tour_step": "{n} / {total}"
|
"cw_tour_step": "{n} / {total}",
|
||||||
|
"cw_update_later": "나중에",
|
||||||
|
"cw_update_ready": "PS5Upload Files가 업데이트되었습니다. 새 버전을 사용하려면 다시 불러오세요."
|
||||||
}
|
}
|
||||||
@@ -414,5 +414,7 @@
|
|||||||
"cw_tour_label": "Wskazówki",
|
"cw_tour_label": "Wskazówki",
|
||||||
"cw_tour_next": "Dalej",
|
"cw_tour_next": "Dalej",
|
||||||
"cw_tour_skip": "Pomiń",
|
"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."
|
||||||
}
|
}
|
||||||
@@ -414,5 +414,7 @@
|
|||||||
"cw_tour_label": "Dicas",
|
"cw_tour_label": "Dicas",
|
||||||
"cw_tour_next": "Próxima",
|
"cw_tour_next": "Próxima",
|
||||||
"cw_tour_skip": "Pular",
|
"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."
|
||||||
}
|
}
|
||||||
@@ -414,5 +414,7 @@
|
|||||||
"cw_tour_label": "Советы",
|
"cw_tour_label": "Советы",
|
||||||
"cw_tour_next": "Далее",
|
"cw_tour_next": "Далее",
|
||||||
"cw_tour_skip": "Пропустить",
|
"cw_tour_skip": "Пропустить",
|
||||||
"cw_tour_step": "{n} из {total}"
|
"cw_tour_step": "{n} из {total}",
|
||||||
|
"cw_update_later": "Позже",
|
||||||
|
"cw_update_ready": "PS5Upload Files обновлён. Перезагрузите страницу, чтобы использовать новую версию."
|
||||||
}
|
}
|
||||||
@@ -414,5 +414,7 @@
|
|||||||
"cw_tour_label": "เคล็ดลับ",
|
"cw_tour_label": "เคล็ดลับ",
|
||||||
"cw_tour_next": "ถัดไป",
|
"cw_tour_next": "ถัดไป",
|
||||||
"cw_tour_skip": "ข้าม",
|
"cw_tour_skip": "ข้าม",
|
||||||
"cw_tour_step": "{n} จาก {total}"
|
"cw_tour_step": "{n} จาก {total}",
|
||||||
|
"cw_update_later": "ภายหลัง",
|
||||||
|
"cw_update_ready": "PS5Upload Files ได้รับการอัปเดตแล้ว โหลดใหม่เพื่อใช้เวอร์ชันใหม่"
|
||||||
}
|
}
|
||||||
@@ -414,5 +414,7 @@
|
|||||||
"cw_tour_label": "İpuçları",
|
"cw_tour_label": "İpuçları",
|
||||||
"cw_tour_next": "İleri",
|
"cw_tour_next": "İleri",
|
||||||
"cw_tour_skip": "Atla",
|
"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."
|
||||||
}
|
}
|
||||||
@@ -414,5 +414,7 @@
|
|||||||
"cw_tour_label": "Mẹo",
|
"cw_tour_label": "Mẹo",
|
||||||
"cw_tour_next": "Tiếp",
|
"cw_tour_next": "Tiếp",
|
||||||
"cw_tour_skip": "Bỏ qua",
|
"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."
|
||||||
}
|
}
|
||||||
@@ -414,5 +414,7 @@
|
|||||||
"cw_tour_label": "提示",
|
"cw_tour_label": "提示",
|
||||||
"cw_tour_next": "下一步",
|
"cw_tour_next": "下一步",
|
||||||
"cw_tour_skip": "跳过",
|
"cw_tour_skip": "跳过",
|
||||||
"cw_tour_step": "{n} / {total}"
|
"cw_tour_step": "{n} / {total}",
|
||||||
|
"cw_update_later": "稍后",
|
||||||
|
"cw_update_ready": "PS5Upload Files 已更新。请重新加载以使用新版本。"
|
||||||
}
|
}
|
||||||
@@ -414,5 +414,7 @@
|
|||||||
"cw_tour_label": "提示",
|
"cw_tour_label": "提示",
|
||||||
"cw_tour_next": "下一步",
|
"cw_tour_next": "下一步",
|
||||||
"cw_tour_skip": "略過",
|
"cw_tour_skip": "略過",
|
||||||
"cw_tour_step": "{n} / {total}"
|
"cw_tour_step": "{n} / {total}",
|
||||||
|
"cw_update_later": "稍後",
|
||||||
|
"cw_update_ready": "PS5Upload Files 已更新。請重新載入以使用新版本。"
|
||||||
}
|
}
|
||||||
@@ -3,16 +3,18 @@ import { createRoot } from "react-dom/client";
|
|||||||
import "virtual:pc-tokens.css";
|
import "virtual:pc-tokens.css";
|
||||||
import "./console.css";
|
import "./console.css";
|
||||||
import { App } from "./App";
|
import { App } from "./App";
|
||||||
import { probeHelper } from "./api";
|
import { call, probeHelper } from "./api";
|
||||||
import { installBackHistory } from "./backHistory";
|
import { installBackHistory } from "./backHistory";
|
||||||
import { installFullscreen } from "./fullscreen";
|
import { installFullscreen } from "./fullscreen";
|
||||||
import { installMarquee } from "./ui/marquee";
|
import { installMarquee } from "./ui/marquee";
|
||||||
import { DEFAULT_INPUT_MAP, dispatchNav, installNavigation } from "./nav";
|
import { DEFAULT_INPUT_MAP, dispatchNav, installNavigation } from "./nav";
|
||||||
import { installOfflineCopy } from "./offline/offlineCopy";
|
import { installOfflineCopy } from "./offline/offlineCopy";
|
||||||
import { OfflinePage } from "./offline/OfflinePage";
|
import { OfflinePage } from "./offline/OfflinePage";
|
||||||
|
import { UpdateBanner } from "./offline/UpdateBanner";
|
||||||
|
import { isNewerHelper, markUpdateReady } from "./offline/update";
|
||||||
import { ErrorBoundary } from "./ui/ErrorBoundary";
|
import { ErrorBoundary } from "./ui/ErrorBoundary";
|
||||||
import { applyStoredTheme } from "./theme";
|
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
|
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
|
// English is in the page; the detected (or stored) language loads from the helper and the page
|
||||||
@@ -40,15 +42,35 @@ function Root() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let live = true;
|
let live = true;
|
||||||
void probeHelper().then((ok) => {
|
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 () => {
|
return () => {
|
||||||
live = false;
|
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 (
|
||||||
|
<div className="cw-splash" aria-busy="true">
|
||||||
|
{t("cw_settings_app_name")}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>{up ? <App inputMap={DEFAULT_INPUT_MAP} /> : <OfflinePage />}</ErrorBoundary>
|
<ErrorBoundary>
|
||||||
|
{up ? <App inputMap={DEFAULT_INPUT_MAP} /> : <OfflinePage />}
|
||||||
|
<UpdateBanner />
|
||||||
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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<HTMLButtonElement>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (ready) button.current?.focus();
|
||||||
|
}, [ready]);
|
||||||
|
if (!ready) return null;
|
||||||
|
return (
|
||||||
|
<div className="cw-banner" role="alertdialog" aria-labelledby="cw-update-title" data-nav-modal>
|
||||||
|
<h2 id="cw-update-title">{t("cw_update_ready")}</h2>
|
||||||
|
<div className="cw-dialog-buttons">
|
||||||
|
<button type="button" className="cw-focusable cw-button" onClick={dismissUpdate}>
|
||||||
|
<span>{t("cw_update_later")}</span>
|
||||||
|
</button>
|
||||||
|
<button ref={button} type="button" className="cw-focusable cw-button is-primary" onClick={reload}>
|
||||||
|
<RotateCw aria-hidden />
|
||||||
|
<span>{t("cw_error_reload")}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
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. */
|
/** The service worker run against a fake worker scope: caches in a Map, a scripted network. */
|
||||||
function worker(network: (url: string) => Promise<Response>) {
|
function worker(network: (url: string) => Promise<Response>) {
|
||||||
@@ -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", () => {
|
it("registers the worker at / in a secure context (the console's 127.0.0.1) only", () => {
|
||||||
const a = fakeWindow(true);
|
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: "/" });
|
expect(a.win.navigator.serviceWorker.register).toHaveBeenCalledWith("/sw.js", { scope: "/" });
|
||||||
const b = fakeWindow(false); // a phone at the console's LAN address
|
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();
|
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);
|
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();
|
a.fns.updateready();
|
||||||
expect(a.win.applicationCache.swapCache).toHaveBeenCalled();
|
expect(a.win.applicationCache.swapCache).toHaveBeenCalled();
|
||||||
expect(a.win.location.reload).toHaveBeenCalledTimes(1);
|
expect(onUpdate).toHaveBeenCalledTimes(1);
|
||||||
const b = fakeWindow(true);
|
expect(a.win.location.reload).not.toHaveBeenCalled();
|
||||||
installOfflineCopy(b.win as unknown as Window, Date.now() - APPCACHE_RELOAD_WINDOW_MS - 1);
|
|
||||||
b.fns.updateready();
|
|
||||||
expect(b.win.location.reload).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -11,8 +11,8 @@
|
|||||||
* - the appcache (<html manifest="/cache.appcache">), the fallback the PS5
|
* - the appcache (<html manifest="/cache.appcache">), the fallback the PS5
|
||||||
* browser honours for Payload Manager: the page is its only cached entry
|
* browser honours for Payload Manager: the page is its only cached entry
|
||||||
* (NETWORK: * lets everything else through). An appcache serves its copy
|
* (NETWORK: * lets everything else through). An appcache serves its copy
|
||||||
* first and updates behind it, so a newer page reloads once on
|
* first and updates behind it, so on "updateready" the page offers a
|
||||||
* "updateready" (only right after the page opened, never mid-task).
|
* 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
|
* 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
|
* 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.
|
* 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. */
|
/** 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) */
|
export const SW_SOURCE = `/* PS5Upload Files: offline copy of the page (generated; see offlineCopy.ts) */
|
||||||
"use strict";
|
"use strict";
|
||||||
@@ -82,9 +84,6 @@ export function serviceWorkerSource(version: string): string {
|
|||||||
return SW_SOURCE.split("__CW_SW_V__").join(version);
|
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 {
|
interface AppCacheLike {
|
||||||
addEventListener(type: "updateready", fn: () => void): void;
|
addEventListener(type: "updateready", fn: () => void): void;
|
||||||
swapCache?: () => 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
|
* 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.
|
* 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 {
|
try {
|
||||||
const nav = win.navigator as Navigator & { serviceWorker?: ServiceWorkerContainer };
|
const nav = win.navigator as Navigator & { serviceWorker?: ServiceWorkerContainer };
|
||||||
if (win.isSecureContext && nav.serviceWorker && typeof nav.serviceWorker.register === "function") {
|
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;
|
const ac = (win as unknown as { applicationCache?: AppCacheLike }).applicationCache;
|
||||||
if (ac && typeof ac.addEventListener === "function") {
|
if (ac && typeof ac.addEventListener === "function") {
|
||||||
ac.addEventListener("updateready", () => {
|
ac.addEventListener("updateready", () => {
|
||||||
if (Date.now() - startedAt > APPCACHE_RELOAD_WINDOW_MS) return;
|
|
||||||
try {
|
try {
|
||||||
ac.swapCache?.();
|
ac.swapCache?.();
|
||||||
} catch {
|
} catch {
|
||||||
/* already swapped */
|
/* already swapped */
|
||||||
}
|
}
|
||||||
win.location.reload();
|
onUpdate(); // the banner: one press reloads into the new page
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -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(<UpdateBanner />)).toBe("");
|
||||||
|
markUpdateReady();
|
||||||
|
const html = renderToStaticMarkup(<UpdateBanner />);
|
||||||
|
expect(html).toContain(t("cw_update_ready"));
|
||||||
|
expect(html).toContain(t("cw_error_reload"));
|
||||||
|
expect(html).toContain(t("cw_update_later"));
|
||||||
|
dismissUpdate();
|
||||||
|
expect(renderToStaticMarkup(<UpdateBanner />)).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { phoneUrl } from "./PhoneHandoff";
|
import { onConsoleBrowser, phoneUrl } from "./PhoneHandoff";
|
||||||
|
|
||||||
describe("continue on your phone", () => {
|
describe("continue on your phone", () => {
|
||||||
it("is this page on the console's LAN address while LAN access is on", () => {
|
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({ lan: true, addr: "evil.example/x?" }, "9180")).toBeNull();
|
||||||
expect(phoneUrl(null, "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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -17,6 +17,11 @@ export function phoneUrl(info: LanInfo | null, port: string): string | null {
|
|||||||
return `http://${info.addr}:${p}/`;
|
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. */
|
/** Read once per page and kept briefly: every text dialog asks, the answer rarely changes. */
|
||||||
const KEEP_MS = 30_000;
|
const KEEP_MS = 30_000;
|
||||||
let cached: { at: number; url: string | null } | null = null;
|
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
|
* "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
|
* 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
|
* 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() {
|
export function PhoneHandoff({ host }: { host?: string } = {}) {
|
||||||
const [url, setUrl] = useState<string | null>(cached?.url ?? null);
|
const here = onConsoleBrowser(host);
|
||||||
|
const [url, setUrl] = useState<string | null>(here ? (cached?.url ?? null) : null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!here) return;
|
||||||
let live = true;
|
let live = true;
|
||||||
void loadPhoneUrl().then((u) => {
|
void loadPhoneUrl().then((u) => {
|
||||||
if (live) setUrl(u);
|
if (live) setUrl(u);
|
||||||
@@ -65,7 +73,7 @@ export function PhoneHandoff() {
|
|||||||
return () => {
|
return () => {
|
||||||
live = false;
|
live = false;
|
||||||
};
|
};
|
||||||
}, []);
|
}, [here]);
|
||||||
const path = useMemo(() => {
|
const path = useMemo(() => {
|
||||||
if (!url) return null;
|
if (!url) return null;
|
||||||
try {
|
try {
|
||||||
@@ -75,7 +83,7 @@ export function PhoneHandoff() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}, [url]);
|
}, [url]);
|
||||||
if (!url || !path) return null;
|
if (!here || !url || !path) return null;
|
||||||
return (
|
return (
|
||||||
<div className="cw-phone" data-phone="">
|
<div className="cw-phone" data-phone="">
|
||||||
<svg viewBox={`0 0 ${path.n} ${path.n}`} role="img" aria-label={t("cw_phone_qr_label")} shapeRendering="crispEdges">
|
<svg viewBox={`0 0 ${path.n} ${path.n}`} role="img" aria-label={t("cw_phone_qr_label")} shapeRendering="crispEdges">
|
||||||
|
|||||||
@@ -4074,6 +4074,8 @@ c2c_test_deep_bad: "لم يصل الإرسال التجريبي بحجم 4 KiB:
|
|||||||
"cw_phone_qr_label": "رمز QR لعنوان هذه الصفحة",
|
"cw_phone_qr_label": "رمز QR لعنوان هذه الصفحة",
|
||||||
"cw_files_fixperm_many": "إصلاح أذونات {n} عناصر",
|
"cw_files_fixperm_many": "إصلاح أذونات {n} عناصر",
|
||||||
"cw_cheats_err_busy": "يغيّر تطبيق آخر غشّ هذه اللعبة الآن. حاول مرة أخرى بعد قليل.",
|
"cw_cheats_err_busy": "يغيّر تطبيق آخر غشّ هذه اللعبة الآن. حاول مرة أخرى بعد قليل.",
|
||||||
|
"cw_update_ready": "تم تحديث PS5Upload Files. أعد التحميل لاستخدام الإصدار الجديد.",
|
||||||
|
"cw_update_later": "لاحقًا",
|
||||||
"cw_hints_label": "أزرار وحدة التحكم",
|
"cw_hints_label": "أزرار وحدة التحكم",
|
||||||
"cw_task_from_console": "من هذا الجهاز",
|
"cw_task_from_console": "من هذا الجهاز",
|
||||||
"cw_task_from_pc": "من حاسوب",
|
"cw_task_from_pc": "من حاسوب",
|
||||||
|
|||||||
@@ -4074,6 +4074,8 @@ c2c_test_deep_bad: "4 KiB-এর পরীক্ষামূলক পাঠা
|
|||||||
"cw_phone_qr_label": "এই পাতার ঠিকানার QR কোড",
|
"cw_phone_qr_label": "এই পাতার ঠিকানার QR কোড",
|
||||||
"cw_files_fixperm_many": "{n}টি আইটেমের অনুমতি ঠিক করুন",
|
"cw_files_fixperm_many": "{n}টি আইটেমের অনুমতি ঠিক করুন",
|
||||||
"cw_cheats_err_busy": "অন্য একটি অ্যাপ এখন এই গেমের চিট বদলাচ্ছে। একটু পরে আবার চেষ্টা করুন।",
|
"cw_cheats_err_busy": "অন্য একটি অ্যাপ এখন এই গেমের চিট বদলাচ্ছে। একটু পরে আবার চেষ্টা করুন।",
|
||||||
|
"cw_update_ready": "PS5Upload Files আপডেট হয়েছে। নতুন সংস্করণ ব্যবহার করতে আবার লোড করুন।",
|
||||||
|
"cw_update_later": "পরে",
|
||||||
"cw_hints_label": "কন্ট্রোলারের বোতাম",
|
"cw_hints_label": "কন্ট্রোলারের বোতাম",
|
||||||
"cw_task_from_console": "এই কনসোল থেকে",
|
"cw_task_from_console": "এই কনসোল থেকে",
|
||||||
"cw_task_from_pc": "PC থেকে",
|
"cw_task_from_pc": "PC থেকে",
|
||||||
|
|||||||
@@ -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_phone_qr_label": "QR-Code der Adresse dieser Seite",
|
||||||
"cw_files_fixperm_many": "Berechtigungen von {n} Elementen reparieren",
|
"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_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_hints_label": "Controller-Tasten",
|
||||||
"cw_task_from_console": "Von dieser Konsole",
|
"cw_task_from_console": "Von dieser Konsole",
|
||||||
"cw_task_from_pc": "Von einem PC",
|
"cw_task_from_pc": "Von einem PC",
|
||||||
|
|||||||
@@ -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_phone_qr_label": "QR code of this page's address",
|
||||||
"cw_files_fixperm_many": "Fix permissions of {n} items",
|
"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_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_hints_label": "Controller buttons",
|
||||||
"cw_task_from_console": "From this console",
|
"cw_task_from_console": "From this console",
|
||||||
"cw_task_from_pc": "From a PC",
|
"cw_task_from_pc": "From a PC",
|
||||||
|
|||||||
@@ -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_phone_qr_label": "Código QR de la dirección de esta página",
|
||||||
"cw_files_fixperm_many": "Corregir permisos de {n} elementos",
|
"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_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_hints_label": "Botones del mando",
|
||||||
"cw_task_from_console": "Desde esta consola",
|
"cw_task_from_console": "Desde esta consola",
|
||||||
"cw_task_from_pc": "Desde un PC",
|
"cw_task_from_pc": "Desde un PC",
|
||||||
|
|||||||
@@ -4375,6 +4375,8 @@ c2c_test_deep_bad: "ارسال آزمایشی ۴ KiB نرسید: {detail}",
|
|||||||
"cw_phone_qr_label": "کد QR نشانی این صفحه",
|
"cw_phone_qr_label": "کد QR نشانی این صفحه",
|
||||||
"cw_files_fixperm_many": "اصلاح مجوزهای {n} مورد",
|
"cw_files_fixperm_many": "اصلاح مجوزهای {n} مورد",
|
||||||
"cw_cheats_err_busy": "برنامهٔ دیگری همین حالا تقلبهای این بازی را تغییر میدهد. کمی بعد دوباره امتحان کنید.",
|
"cw_cheats_err_busy": "برنامهٔ دیگری همین حالا تقلبهای این بازی را تغییر میدهد. کمی بعد دوباره امتحان کنید.",
|
||||||
|
"cw_update_ready": "PS5Upload Files بهروزرسانی شد. برای استفاده از نسخهٔ جدید، دوباره بارگیری کنید.",
|
||||||
|
"cw_update_later": "بعداً",
|
||||||
"cw_hints_label": "دکمههای دسته",
|
"cw_hints_label": "دکمههای دسته",
|
||||||
"cw_task_from_console": "از این کنسول",
|
"cw_task_from_console": "از این کنسول",
|
||||||
"cw_task_from_pc": "از رایانه",
|
"cw_task_from_pc": "از رایانه",
|
||||||
|
|||||||
@@ -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_phone_qr_label": "Code QR de l'adresse de cette page",
|
||||||
"cw_files_fixperm_many": "Corriger les autorisations de {n} éléments",
|
"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_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_hints_label": "Boutons de la manette",
|
||||||
"cw_task_from_console": "Depuis cette console",
|
"cw_task_from_console": "Depuis cette console",
|
||||||
"cw_task_from_pc": "Depuis un PC",
|
"cw_task_from_pc": "Depuis un PC",
|
||||||
|
|||||||
@@ -4090,6 +4090,8 @@ c2c_test_deep_bad: "4 KiB का परीक्षण नहीं पहुँ
|
|||||||
"cw_phone_qr_label": "इस पेज के पते का QR कोड",
|
"cw_phone_qr_label": "इस पेज के पते का QR कोड",
|
||||||
"cw_files_fixperm_many": "{n} आइटम की अनुमतियाँ ठीक करें",
|
"cw_files_fixperm_many": "{n} आइटम की अनुमतियाँ ठीक करें",
|
||||||
"cw_cheats_err_busy": "कोई दूसरा ऐप अभी इस गेम के चीट बदल रहा है। थोड़ी देर में फिर से कोशिश करें।",
|
"cw_cheats_err_busy": "कोई दूसरा ऐप अभी इस गेम के चीट बदल रहा है। थोड़ी देर में फिर से कोशिश करें।",
|
||||||
|
"cw_update_ready": "PS5Upload Files अपडेट हो गया है। नया संस्करण इस्तेमाल करने के लिए फिर से लोड करें।",
|
||||||
|
"cw_update_later": "बाद में",
|
||||||
"cw_hints_label": "कंट्रोलर के बटन",
|
"cw_hints_label": "कंट्रोलर के बटन",
|
||||||
"cw_task_from_console": "इस कंसोल से",
|
"cw_task_from_console": "इस कंसोल से",
|
||||||
"cw_task_from_pc": "PC से",
|
"cw_task_from_pc": "PC से",
|
||||||
|
|||||||
@@ -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_phone_qr_label": "Az oldal címének QR-kódja",
|
||||||
"cw_files_fixperm_many": "{n} elem jogosultságainak javítása",
|
"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_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_hints_label": "Kontroller gombjai",
|
||||||
"cw_task_from_console": "Erről a konzolról",
|
"cw_task_from_console": "Erről a konzolról",
|
||||||
"cw_task_from_pc": "Egy PC-ről",
|
"cw_task_from_pc": "Egy PC-ről",
|
||||||
|
|||||||
@@ -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_phone_qr_label": "Kode QR alamat halaman ini",
|
||||||
"cw_files_fixperm_many": "Perbaiki izin {n} item",
|
"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_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_hints_label": "Tombol kontroler",
|
||||||
"cw_task_from_console": "Dari konsol ini",
|
"cw_task_from_console": "Dari konsol ini",
|
||||||
"cw_task_from_pc": "Dari PC",
|
"cw_task_from_pc": "Dari PC",
|
||||||
|
|||||||
@@ -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_phone_qr_label": "Codice QR dell'indirizzo di questa pagina",
|
||||||
"cw_files_fixperm_many": "Correggi i permessi di {n} elementi",
|
"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_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_hints_label": "Tasti del controller",
|
||||||
"cw_task_from_console": "Da questa console",
|
"cw_task_from_console": "Da questa console",
|
||||||
"cw_task_from_pc": "Da un PC",
|
"cw_task_from_pc": "Da un PC",
|
||||||
|
|||||||
@@ -4095,6 +4095,8 @@ c2c_test_deep_bad: "4 KiB のテスト送信が届きませんでした: {detail
|
|||||||
"cw_phone_qr_label": "このページのアドレスの QR コード",
|
"cw_phone_qr_label": "このページのアドレスの QR コード",
|
||||||
"cw_files_fixperm_many": "{n} 件の権限を修正",
|
"cw_files_fixperm_many": "{n} 件の権限を修正",
|
||||||
"cw_cheats_err_busy": "別のアプリが今このゲームのチートを変更しています。少し待ってからもう一度お試しください。",
|
"cw_cheats_err_busy": "別のアプリが今このゲームのチートを変更しています。少し待ってからもう一度お試しください。",
|
||||||
|
"cw_update_ready": "PS5Upload Files が更新されました。新しいバージョンを使うには再読み込みしてください。",
|
||||||
|
"cw_update_later": "後で",
|
||||||
"cw_hints_label": "コントローラーのボタン",
|
"cw_hints_label": "コントローラーのボタン",
|
||||||
"cw_task_from_console": "このコンソールから",
|
"cw_task_from_console": "このコンソールから",
|
||||||
"cw_task_from_pc": "PCから",
|
"cw_task_from_pc": "PCから",
|
||||||
|
|||||||
@@ -4093,6 +4093,8 @@ c2c_test_deep_bad: "4 KiB 테스트 전송이 도착하지 않았습니다: {det
|
|||||||
"cw_phone_qr_label": "이 페이지 주소의 QR 코드",
|
"cw_phone_qr_label": "이 페이지 주소의 QR 코드",
|
||||||
"cw_files_fixperm_many": "항목 {n}개의 권한 수정",
|
"cw_files_fixperm_many": "항목 {n}개의 권한 수정",
|
||||||
"cw_cheats_err_busy": "다른 앱이 지금 이 게임의 치트를 바꾸고 있습니다. 잠시 후 다시 시도하세요.",
|
"cw_cheats_err_busy": "다른 앱이 지금 이 게임의 치트를 바꾸고 있습니다. 잠시 후 다시 시도하세요.",
|
||||||
|
"cw_update_ready": "PS5Upload Files가 업데이트되었습니다. 새 버전을 사용하려면 다시 불러오세요.",
|
||||||
|
"cw_update_later": "나중에",
|
||||||
"cw_hints_label": "컨트롤러 버튼",
|
"cw_hints_label": "컨트롤러 버튼",
|
||||||
"cw_task_from_console": "이 콘솔에서",
|
"cw_task_from_console": "이 콘솔에서",
|
||||||
"cw_task_from_pc": "PC에서",
|
"cw_task_from_pc": "PC에서",
|
||||||
|
|||||||
@@ -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_phone_qr_label": "Kod QR adresu tej strony",
|
||||||
"cw_files_fixperm_many": "Napraw uprawnienia {n} elementów",
|
"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_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_hints_label": "Przyciski kontrolera",
|
||||||
"cw_task_from_console": "Z tej konsoli",
|
"cw_task_from_console": "Z tej konsoli",
|
||||||
"cw_task_from_pc": "Z komputera",
|
"cw_task_from_pc": "Z komputera",
|
||||||
|
|||||||
@@ -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_phone_qr_label": "Código QR do endereço desta página",
|
||||||
"cw_files_fixperm_many": "Corrigir permissões de {n} itens",
|
"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_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_hints_label": "Botões do controle",
|
||||||
"cw_task_from_console": "Deste console",
|
"cw_task_from_console": "Deste console",
|
||||||
"cw_task_from_pc": "De um PC",
|
"cw_task_from_pc": "De um PC",
|
||||||
|
|||||||
@@ -4024,6 +4024,8 @@ c2c_test_deep_bad: "Тестовая отправка 4 КиБ не дошла:
|
|||||||
"cw_phone_qr_label": "QR-код адреса этой страницы",
|
"cw_phone_qr_label": "QR-код адреса этой страницы",
|
||||||
"cw_files_fixperm_many": "Исправить права у {n} элементов",
|
"cw_files_fixperm_many": "Исправить права у {n} элементов",
|
||||||
"cw_cheats_err_busy": "Другое приложение прямо сейчас меняет читы этой игры. Попробуйте ещё раз через пару секунд.",
|
"cw_cheats_err_busy": "Другое приложение прямо сейчас меняет читы этой игры. Попробуйте ещё раз через пару секунд.",
|
||||||
|
"cw_update_ready": "PS5Upload Files обновлён. Перезагрузите страницу, чтобы использовать новую версию.",
|
||||||
|
"cw_update_later": "Позже",
|
||||||
"cw_hints_label": "Кнопки геймпада",
|
"cw_hints_label": "Кнопки геймпада",
|
||||||
"cw_task_from_console": "С этой консоли",
|
"cw_task_from_console": "С этой консоли",
|
||||||
"cw_task_from_pc": "С ПК",
|
"cw_task_from_pc": "С ПК",
|
||||||
|
|||||||
@@ -4083,6 +4083,8 @@ c2c_test_deep_bad: "การส่งทดสอบ 4 KiB ไปไม่ถ
|
|||||||
"cw_phone_qr_label": "คิวอาร์โค้ดของที่อยู่หน้านี้",
|
"cw_phone_qr_label": "คิวอาร์โค้ดของที่อยู่หน้านี้",
|
||||||
"cw_files_fixperm_many": "แก้ไขสิทธิ์ของ {n} รายการ",
|
"cw_files_fixperm_many": "แก้ไขสิทธิ์ของ {n} รายการ",
|
||||||
"cw_cheats_err_busy": "มีแอปอื่นกำลังเปลี่ยนสูตรโกงของเกมนี้อยู่ ลองอีกครั้งในอีกสักครู่",
|
"cw_cheats_err_busy": "มีแอปอื่นกำลังเปลี่ยนสูตรโกงของเกมนี้อยู่ ลองอีกครั้งในอีกสักครู่",
|
||||||
|
"cw_update_ready": "PS5Upload Files ได้รับการอัปเดตแล้ว โหลดใหม่เพื่อใช้เวอร์ชันใหม่",
|
||||||
|
"cw_update_later": "ภายหลัง",
|
||||||
"cw_hints_label": "ปุ่มคอนโทรลเลอร์",
|
"cw_hints_label": "ปุ่มคอนโทรลเลอร์",
|
||||||
"cw_task_from_console": "จากคอนโซลนี้",
|
"cw_task_from_console": "จากคอนโซลนี้",
|
||||||
"cw_task_from_pc": "จาก PC",
|
"cw_task_from_pc": "จาก PC",
|
||||||
|
|||||||
@@ -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_phone_qr_label": "Bu sayfanın adresinin QR kodu",
|
||||||
"cw_files_fixperm_many": "{n} öğenin izinlerini düzelt",
|
"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_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_hints_label": "Kumanda düğmeleri",
|
||||||
"cw_task_from_console": "Bu konsoldan",
|
"cw_task_from_console": "Bu konsoldan",
|
||||||
"cw_task_from_pc": "Bir PC'den",
|
"cw_task_from_pc": "Bir PC'den",
|
||||||
|
|||||||
@@ -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_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_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_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_hints_label": "Nút tay cầm",
|
||||||
"cw_task_from_console": "Từ máy này",
|
"cw_task_from_console": "Từ máy này",
|
||||||
"cw_task_from_pc": "Từ PC",
|
"cw_task_from_pc": "Từ PC",
|
||||||
|
|||||||
@@ -4030,6 +4030,8 @@ c2c_test_deep_bad: "4 KiB 测试发送未到达:{detail}",
|
|||||||
"cw_phone_qr_label": "此页面地址的二维码",
|
"cw_phone_qr_label": "此页面地址的二维码",
|
||||||
"cw_files_fixperm_many": "修复 {n} 项的权限",
|
"cw_files_fixperm_many": "修复 {n} 项的权限",
|
||||||
"cw_cheats_err_busy": "另一个应用正在更改此游戏的金手指。请稍后再试。",
|
"cw_cheats_err_busy": "另一个应用正在更改此游戏的金手指。请稍后再试。",
|
||||||
|
"cw_update_ready": "PS5Upload Files 已更新。请重新加载以使用新版本。",
|
||||||
|
"cw_update_later": "稍后",
|
||||||
"cw_hints_label": "手柄按键",
|
"cw_hints_label": "手柄按键",
|
||||||
"cw_task_from_console": "来自本主机",
|
"cw_task_from_console": "来自本主机",
|
||||||
"cw_task_from_pc": "来自电脑",
|
"cw_task_from_pc": "来自电脑",
|
||||||
|
|||||||
@@ -4024,6 +4024,8 @@ c2c_test_deep_bad: "4 KiB 測試傳送未抵達:{detail}",
|
|||||||
"cw_phone_qr_label": "此頁面位址的 QR 碼",
|
"cw_phone_qr_label": "此頁面位址的 QR 碼",
|
||||||
"cw_files_fixperm_many": "修正 {n} 個項目的權限",
|
"cw_files_fixperm_many": "修正 {n} 個項目的權限",
|
||||||
"cw_cheats_err_busy": "另一個應用程式正在變更此遊戲的金手指。請稍後再試。",
|
"cw_cheats_err_busy": "另一個應用程式正在變更此遊戲的金手指。請稍後再試。",
|
||||||
|
"cw_update_ready": "PS5Upload Files 已更新。請重新載入以使用新版本。",
|
||||||
|
"cw_update_later": "稍後",
|
||||||
"cw_hints_label": "控制器按鍵",
|
"cw_hints_label": "控制器按鍵",
|
||||||
"cw_task_from_console": "來自本主機",
|
"cw_task_from_console": "來自本主機",
|
||||||
"cw_task_from_pc": "來自電腦",
|
"cw_task_from_pc": "來自電腦",
|
||||||
|
|||||||
@@ -202,7 +202,11 @@ function escapeRe(s: string): string {
|
|||||||
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
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({
|
export default defineConfig({
|
||||||
|
define: { __CW_APP_V__: JSON.stringify(APP_VERSION) },
|
||||||
root: fileURLToPath(new URL("./src/console", import.meta.url)),
|
root: fileURLToPath(new URL("./src/console", import.meta.url)),
|
||||||
base: "./",
|
base: "./",
|
||||||
publicDir: false,
|
publicDir: false,
|
||||||
|
|||||||
@@ -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:
|
* 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
|
* - 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;
|
* 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
|
* - the game is alive but another debugger is attached (EBUSY: the PC app toggling at the
|
||||||
* moment, or another cheat tool): "is attached to the game". */
|
* 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) {
|
if (pt_attach(pid) != 0) {
|
||||||
int e = errno;
|
int e = errno;
|
||||||
free(target_cf);
|
free(target_cf);
|
||||||
invalidate_rg_cache();
|
invalidate_rg_cache();
|
||||||
if (kill(pid, 0) != 0 && errno == ESRCH) {
|
if (kill(pid, 0) != 0 && errno == ESRCH) {
|
||||||
if (err) snprintf(err, err_cap, "no game is currently running");
|
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) {
|
} 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;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-16
@@ -138,11 +138,13 @@ static int thread_start(pthread_t *t, void *(*fn)(void *), void *arg) {
|
|||||||
return rc == 0 ? 0 : -1;
|
return rc == 0 ? 0 : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The console's IPv4 addresses, for the LAN Host check. */
|
/* The console's IPv4 addresses, for the LAN Host check; -1 when they could not be read
|
||||||
static size_t own_addrs(char out[WEBUI_MAX_ADDRS][INET_ADDRSTRLEN]) {
|
* (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;
|
struct ifaddrs *ifa = NULL, *p;
|
||||||
size_t n = 0;
|
int n = 0;
|
||||||
if (getifaddrs(&ifa) != 0) return 0;
|
if (getifaddrs(&ifa) != 0) return -1;
|
||||||
for (p = ifa; p && n < WEBUI_MAX_ADDRS; p = p->ifa_next) {
|
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 (!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,
|
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();
|
int64_t real = real_ms();
|
||||||
int resumed, lan, changed = 0;
|
int resumed, lan, changed = 0;
|
||||||
char addrs[WEBUI_MAX_ADDRS][INET_ADDRSTRLEN];
|
char addrs[WEBUI_MAX_ADDRS][INET_ADDRSTRLEN];
|
||||||
size_t n = 0;
|
int n = -1;
|
||||||
resumed = webui_watch_resumed(*mono_prev, mono, *real_prev, real);
|
resumed = webui_watch_resumed(*mono_prev, mono, *real_prev, real);
|
||||||
*mono_prev = mono;
|
*mono_prev = mono;
|
||||||
*real_prev = real;
|
*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);
|
pthread_mutex_unlock(&s->mu);
|
||||||
if (lan) n = own_addrs(addrs);
|
if (lan) n = own_addrs(addrs);
|
||||||
pthread_mutex_lock(&s->mu);
|
pthread_mutex_lock(&s->mu);
|
||||||
if (lan && s->lan && webui_addrs_differ((const char (*)[INET_ADDRSTRLEN])s->addrs, s->n_addrs,
|
if (lan && s->lan && n >= 0 &&
|
||||||
(const char (*)[INET_ADDRSTRLEN])addrs, n)) {
|
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);
|
memcpy(s->addrs, addrs, sizeof addrs);
|
||||||
s->n_addrs = n;
|
s->n_addrs = (size_t)n;
|
||||||
changed = 1;
|
changed = 1;
|
||||||
}
|
}
|
||||||
if ((resumed || changed) && s->listen_fd >= 0) drop_listener_locked(s);
|
if ((resumed || changed) && s->listen_fd >= 0) drop_listener_locked(s);
|
||||||
pthread_mutex_unlock(&s->mu);
|
pthread_mutex_unlock(&s->mu);
|
||||||
if (resumed) fprintf(stderr, "webui: resumed (clock gap); rebinding port %u\n", (unsigned)s->port);
|
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) {
|
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
|
/* 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
|
* server); back off briefly on the ones that would otherwise spin, and rebind when they
|
||||||
* go on for WEBUI_BROKEN_MS. */
|
* 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();
|
if (!err_since) err_since = mono_ms();
|
||||||
else if (webui_watch_broken(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",
|
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);
|
pthread_mutex_lock(&s->mu);
|
||||||
if (s->listen_fd == fd) drop_listener_locked(s);
|
if (s->listen_fd == fd) drop_listener_locked(s);
|
||||||
pthread_mutex_unlock(&s->mu);
|
pthread_mutex_unlock(&s->mu);
|
||||||
@@ -714,7 +722,10 @@ int webui_start(const webui_cfg_t *cfg) {
|
|||||||
s->route = cfg->route;
|
s->route = cfg->route;
|
||||||
s->lan = cfg->lan ? 1 : 0;
|
s->lan = cfg->lan ? 1 : 0;
|
||||||
webui_token_new(s->token);
|
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
|
/* 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). */
|
* 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;
|
size_t n = 0;
|
||||||
unsigned gen;
|
unsigned gen;
|
||||||
uint64_t end;
|
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);
|
pthread_mutex_lock(&g_life);
|
||||||
s = g_srv;
|
s = g_srv;
|
||||||
if (!s) {
|
if (!s) {
|
||||||
@@ -799,8 +813,10 @@ int webui_restart(int lan) {
|
|||||||
}
|
}
|
||||||
pthread_mutex_lock(&s->mu);
|
pthread_mutex_lock(&s->mu);
|
||||||
s->lan = lan ? 1 : 0;
|
s->lan = lan ? 1 : 0;
|
||||||
s->n_addrs = n;
|
if (!lan || got >= 0) { /* a failed read keeps the list it had */
|
||||||
if (n) memcpy(s->addrs, addrs, sizeof addrs);
|
s->n_addrs = n;
|
||||||
|
if (n) memcpy(s->addrs, addrs, sizeof addrs);
|
||||||
|
}
|
||||||
s->rebind_req = 1;
|
s->rebind_req = 1;
|
||||||
gen = s->bind_gen;
|
gen = s->bind_gen;
|
||||||
s->refs++;
|
s->refs++;
|
||||||
|
|||||||
Reference in new issue
Block a user