mirror of
https://github.com/phantomptr/ps5upload.git
synced 2026-10-19 02:20:02 -05:00
console files: shared favourites and Show on TV / Show on PC
The console page takes the PC's "Show on console" handoff every 3 s while visible (any section) and opens Files there: a folder as it is, a file in its folder with the file focused, a path that is gone opens nothing and says so. "Show on PC" in the actions sheet (once the helper is known to have handoffs) leaves the item for the PC app. Folders a PC upload is writing into (/api/jobs "uploading") show an "Uploading" badge. 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
8c86887b97
commit
4b51f802d3
28 files changed
+560
-10
No files matched your search
@@ -14,6 +14,7 @@ import { ReloadBanner } from "./shell/ReloadBanner";
|
||||
import { TopBar, type ConsoleStatus } from "./shell/TopBar";
|
||||
import { focusLost } from "./ui/restoreFocus";
|
||||
import { pollWhileVisible } from "./ui/visiblePoll";
|
||||
import { useHandoffPoll } from "./files/handoff";
|
||||
import { t } from "./strings";
|
||||
|
||||
/** The console chip refreshes this often (the temperature moves). */
|
||||
@@ -127,6 +128,17 @@ export function App({ inputMap }: { inputMap: InputMap }) {
|
||||
[active, show, openHelp],
|
||||
);
|
||||
|
||||
// "Show on TV": the PC app left a folder; Files opens there (any section).
|
||||
const openFromPc = useCallback(
|
||||
(p: SectionParams) => {
|
||||
show("files", p);
|
||||
const el = railButton("files");
|
||||
if (el) focusElement(el);
|
||||
},
|
||||
[show],
|
||||
);
|
||||
useHandoffPoll(openFromPc, !expired);
|
||||
|
||||
const ctx = useMemo<SectionContext>(
|
||||
() => ({
|
||||
status,
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { t } from "../strings";
|
||||
import { Modal } from "../ui/Modal";
|
||||
import { SHOW_ON_PC_ITEM } from "./handoff";
|
||||
|
||||
export type SheetAction =
|
||||
| "open"
|
||||
@@ -29,6 +30,7 @@ export type SheetAction =
|
||||
| "info"
|
||||
| "pin"
|
||||
| "unpin"
|
||||
| "showpc"
|
||||
| "fixperm"
|
||||
| "delete";
|
||||
|
||||
@@ -44,6 +46,7 @@ const ITEMS: Record<SheetAction, { icon: LucideIcon; key: string }> = {
|
||||
info: { icon: Info, key: "cw_files_info" },
|
||||
pin: { icon: Star, key: "cw_files_pin" },
|
||||
unpin: { icon: StarOff, key: "cw_files_unpin" },
|
||||
showpc: SHOW_ON_PC_ITEM,
|
||||
fixperm: { icon: ShieldCheck, key: "cw_files_fixperm" },
|
||||
delete: { icon: Trash2, key: "cw_files_delete" },
|
||||
};
|
||||
|
||||
@@ -58,6 +58,7 @@ export function DirList({
|
||||
label,
|
||||
limit = Infinity,
|
||||
onMoreRows,
|
||||
uploading,
|
||||
}: {
|
||||
entries: Entry[];
|
||||
selecting: boolean;
|
||||
@@ -69,6 +70,8 @@ export function DirList({
|
||||
/** Draw only the first `limit` rows; the rest wait behind "Show more rows". */
|
||||
limit?: number;
|
||||
onMoreRows?: () => void;
|
||||
/** Entries a running PC upload is writing into: an "Uploading" badge. */
|
||||
uploading?: ReadonlySet<string>;
|
||||
}) {
|
||||
const marked = new Set(selected);
|
||||
const drawn = entries.length > limit ? entries.slice(0, limit) : entries;
|
||||
@@ -89,6 +92,11 @@ export function DirList({
|
||||
<Icon className="cw-file-icon" aria-hidden />
|
||||
<span className="cw-file-text">
|
||||
<span className="cw-file-name">{e.name}</span>
|
||||
{uploading?.has(e.name) ? (
|
||||
<span className="cw-tag is-pc" data-uploading="">
|
||||
{t("cw_files_uploading")}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
<span className="cw-file-meta cw-dim">
|
||||
{e.kind === "dir" ? t("cw_files_kind_dir") : formatBytes(e.size)}
|
||||
|
||||
@@ -30,6 +30,7 @@ import { t } from "../strings";
|
||||
import { ConfirmDialog } from "../ui/ConfirmDialog";
|
||||
import { anchorOf, focusLost, restoreFocus, type FocusAnchor } from "../ui/restoreFocus";
|
||||
import { ActionsSheet, type SheetAction } from "./ActionsSheet";
|
||||
import { canShowOnPc, showOnPc, uploadingNames, useHandoffNotice } from "./handoff";
|
||||
import { DirList, ROW_WINDOW, viewKind } from "./DirList";
|
||||
import { canEject, DriveList, isImage, isSystemVolume, placeLabel } from "./DriveList";
|
||||
import { InfoDialog } from "./InfoDialog";
|
||||
@@ -201,6 +202,10 @@ export interface FilesViewProps {
|
||||
askError?: string | null;
|
||||
onPin?: (path: string) => void;
|
||||
onUnpin?: (path: string) => void;
|
||||
/** "Show on PC": the PC app's File System opens it. */
|
||||
onShowPc?: (path: string) => void;
|
||||
/** Entries of the folder shown that a PC upload is writing into. */
|
||||
uploading?: ReadonlySet<string>;
|
||||
onEject?: (path: string, force: boolean) => void;
|
||||
onUnmount?: (path: string) => void;
|
||||
onGoTo?: (path: string) => void;
|
||||
@@ -252,6 +257,7 @@ function sheetActions(names: string[], entries: Entry[], pinned: boolean | null)
|
||||
if (e && e.kind !== "dir" && viewKind(e.name)) out.push("view");
|
||||
out.push("clipcopy", "cut", "copy", "move", "rename", "mkdir", "info");
|
||||
if (e && e.kind === "dir" && pinned != null) out.push(pinned ? "unpin" : "pin");
|
||||
if (canShowOnPc()) out.push("showpc");
|
||||
out.push("fixperm", "delete");
|
||||
return out;
|
||||
}
|
||||
@@ -406,6 +412,8 @@ function askView(p: FilesViewProps) {
|
||||
return p.onPin?.(joinPath(path, name));
|
||||
case "unpin":
|
||||
return p.onUnpin?.(joinPath(path, name));
|
||||
case "showpc":
|
||||
return p.onShowPc?.(joinPath(path, name));
|
||||
case "open":
|
||||
return;
|
||||
case "fixperm":
|
||||
@@ -765,6 +773,7 @@ export function FilesView(p: FilesViewProps) {
|
||||
onToggleItem={p.onToggleItem}
|
||||
onActions={(name) => p.onAsk({ kind: "sheet", names: [name] })}
|
||||
label={p.path}
|
||||
uploading={p.uploading}
|
||||
/>
|
||||
{p.dir.more ? (
|
||||
// Reaching it with the D-pad reads the next page: big folders load as you scroll.
|
||||
@@ -833,11 +842,12 @@ function byAttr(root: Element, attr: string, value: string): HTMLElement | null
|
||||
/** The Files section: drives → folders, with the actions sheet, select, copy/move, viewer. */
|
||||
export function Files({ ctx }: { ctx: SectionContext }) {
|
||||
const wanted = ctx.params?.path ?? null;
|
||||
const wantedFocus = ctx.params?.focus ?? "";
|
||||
const [path, setPath] = useState<string | null>(wanted);
|
||||
const [volumesTick, setVolumesTick] = useState(0);
|
||||
const { volumes, error: volumesError } = useVolumes(path, volumesTick);
|
||||
const dir = useDir(path);
|
||||
const { jobs } = useJobs();
|
||||
const { jobs, uploading } = useJobs();
|
||||
const [selecting, setSelecting] = useState(false);
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
const [view, setView] = useState<ViewPrefs>(loadView);
|
||||
@@ -901,8 +911,11 @@ export function Files({ ctx }: { ctx: SectionContext }) {
|
||||
// Home's Browse USB (or any openSection) opens a path: like any folder change,
|
||||
// the selection is cleared and focus goes to its first row.
|
||||
useEffect(() => {
|
||||
if (wanted != null) go(wanted);
|
||||
}, [wanted, go]);
|
||||
if (wanted != null) go(wanted, wantedFocus);
|
||||
}, [wanted, wantedFocus, go]);
|
||||
// Opened by the PC ("Show on TV"): say so, or that the folder is gone.
|
||||
useHandoffNotice(ctx.params, setMessage);
|
||||
const uploadingHere = useMemo(() => uploadingNames(path, uploading), [path, uploading]);
|
||||
|
||||
// Opened without a place: back where Files was last (a folder that is gone falls back to the drives).
|
||||
useEffect(() => {
|
||||
@@ -1365,6 +1378,11 @@ export function Files({ ctx }: { ctx: SectionContext }) {
|
||||
askError={askError}
|
||||
onPin={onPin}
|
||||
onUnpin={onUnpin}
|
||||
onShowPc={(x) => {
|
||||
setAsk(null);
|
||||
void showOnPc(x).then((m) => mounted.current && setMessage(m));
|
||||
}}
|
||||
uploading={uploadingHere}
|
||||
onEject={onEject}
|
||||
onUnmount={onUnmount}
|
||||
onGoTo={onGoTo}
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
import { isValidElement, type ReactElement, type ReactNode } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("../api", async (importOriginal) => {
|
||||
const real = await importOriginal<typeof import("../api")>();
|
||||
return { ...real, call: vi.fn() };
|
||||
});
|
||||
|
||||
import { ApiError, call } from "../api";
|
||||
import { fetchJobsAll } from "../home/useJobs";
|
||||
import { t } from "../strings";
|
||||
import { ActionsSheet } from "./ActionsSheet";
|
||||
import { DirList } from "./DirList";
|
||||
import { FilesView, type FilesViewProps } from "./Files";
|
||||
import {
|
||||
canShowOnPc,
|
||||
checkHandoff,
|
||||
handoffMessage,
|
||||
handoffTarget,
|
||||
resetHandoffSupport,
|
||||
showOnPc,
|
||||
takeHandoff,
|
||||
uploadingNames,
|
||||
} from "./handoff";
|
||||
import type { DirState, Entry } from "./useDir";
|
||||
|
||||
const mockCall = vi.mocked(call);
|
||||
|
||||
function elements(node: ReactNode): ReactElement<Record<string, unknown>>[] {
|
||||
const out: ReactElement<Record<string, unknown>>[] = [];
|
||||
const walk = (n: ReactNode) => {
|
||||
if (Array.isArray(n)) return n.forEach(walk);
|
||||
if (!isValidElement(n)) return;
|
||||
const el = n as ReactElement<Record<string, unknown>>;
|
||||
out.push(el);
|
||||
walk(el.props.children as ReactNode);
|
||||
};
|
||||
walk(node);
|
||||
return out;
|
||||
}
|
||||
|
||||
const entry = (name: string, kind: Entry["kind"] = "file"): Entry => ({ name, kind, size: 10, mtime: 0 });
|
||||
const dir = (entries: Entry[]): DirState => ({ entries, more: false, loading: false, loaded: true, error: null });
|
||||
|
||||
function props(over: Partial<FilesViewProps> = {}): FilesViewProps {
|
||||
return {
|
||||
path: "/data",
|
||||
volumes: [],
|
||||
volumesError: false,
|
||||
dir: dir([entry("games", "dir"), entry("a.txt")]),
|
||||
selecting: false,
|
||||
selected: [],
|
||||
ask: null,
|
||||
busy: null,
|
||||
message: null,
|
||||
tasks: [],
|
||||
prefs: null,
|
||||
onOpen: vi.fn(),
|
||||
onUp: vi.fn(),
|
||||
onEntry: vi.fn(),
|
||||
onLoadMore: vi.fn(),
|
||||
onToggleSelecting: vi.fn(),
|
||||
onToggleItem: vi.fn(),
|
||||
onAsk: vi.fn(),
|
||||
onDismiss: vi.fn(),
|
||||
onConfirmDelete: vi.fn(),
|
||||
onConfirmFixPerm: vi.fn(),
|
||||
onRename: vi.fn(),
|
||||
onMkdir: vi.fn(),
|
||||
onPicked: vi.fn(),
|
||||
onShowPc: vi.fn(),
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockCall.mockReset();
|
||||
resetHandoffSupport();
|
||||
});
|
||||
|
||||
describe("Show on TV: the PC's handoff", () => {
|
||||
it("takes the waiting folder, and learns the helper has handoffs", async () => {
|
||||
mockCall.mockResolvedValueOnce({ path: "/mnt/usb0/games", age_ms: 900 } as never);
|
||||
expect(await takeHandoff()).toBe("/mnt/usb0/games");
|
||||
expect(mockCall).toHaveBeenCalledWith("/api/files/handoff/take", { for: "console" });
|
||||
expect(canShowOnPc()).toBe(true);
|
||||
mockCall.mockResolvedValueOnce({} as never);
|
||||
expect(await takeHandoff()).toBeNull();
|
||||
});
|
||||
|
||||
it("a helper without the routes (404): no handoffs, no Show on PC", async () => {
|
||||
mockCall.mockRejectedValueOnce(new ApiError(404, "not_found"));
|
||||
await expect(takeHandoff()).rejects.toBeInstanceOf(ApiError);
|
||||
expect(canShowOnPc()).toBe(false);
|
||||
});
|
||||
|
||||
it("opens a folder as it is, a file in its folder with the file focused", async () => {
|
||||
mockCall.mockResolvedValueOnce({ kind: "dir" } as never);
|
||||
expect(await handoffTarget("/data/homebrew")).toEqual({ path: "/data/homebrew", handoff: "pc" });
|
||||
mockCall.mockResolvedValueOnce({ kind: "file" } as never);
|
||||
expect(await handoffTarget("/data/homebrew/eboot.bin")).toEqual({
|
||||
path: "/data/homebrew",
|
||||
focus: "eboot.bin",
|
||||
handoff: "pc",
|
||||
});
|
||||
mockCall.mockResolvedValueOnce({ kind: "file" } as never);
|
||||
expect(await handoffTarget("/top.txt")).toEqual({ path: "/", focus: "top.txt", handoff: "pc" });
|
||||
});
|
||||
|
||||
it("a path that is gone opens no folder and says so", async () => {
|
||||
mockCall.mockRejectedValueOnce(new ApiError(404, "io_error"));
|
||||
const p = await handoffTarget("/mnt/usb0/gone");
|
||||
expect(p).toEqual({ handoff_gone: "/mnt/usb0/gone" });
|
||||
expect(handoffMessage(p)).toEqual({ tone: "info", text: t("cw_files_from_pc_gone", { path: "/mnt/usb0/gone" }) });
|
||||
expect(handoffMessage({ path: "/data", handoff: "pc" })).toEqual({ tone: "info", text: t("cw_files_from_pc") });
|
||||
expect(handoffMessage({ path: "/data" })).toBeNull();
|
||||
expect(handoffMessage(null)).toBeNull();
|
||||
});
|
||||
|
||||
it("one poll tick: a handoff opens Files at its target; nothing waiting opens nothing", async () => {
|
||||
const open = vi.fn();
|
||||
mockCall.mockImplementation((async (route: string) => {
|
||||
if (route === "/api/files/handoff/take") return { path: "/data/games", age_ms: 10 };
|
||||
if (route === "/api/fs/stat") return { kind: "dir" };
|
||||
throw new Error(route);
|
||||
}) as never);
|
||||
expect(await checkHandoff(open)).toBe(true);
|
||||
expect(open).toHaveBeenCalledWith({ path: "/data/games", handoff: "pc" });
|
||||
mockCall.mockReset();
|
||||
mockCall.mockResolvedValue({} as never);
|
||||
expect(await checkHandoff(open)).toBe(false);
|
||||
expect(open).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("a tick that ends after the page left opens nothing", async () => {
|
||||
const open = vi.fn();
|
||||
mockCall.mockResolvedValueOnce({ path: "/data", age_ms: 0 } as never).mockResolvedValueOnce({ kind: "dir" } as never);
|
||||
expect(await checkHandoff(open, () => false)).toBe(false);
|
||||
expect(open).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Show on PC", () => {
|
||||
it("is in the sheet only once the helper is known to have handoffs, and hands the item's path on", () => {
|
||||
const sheet = (p: FilesViewProps) => elements(FilesView(p)).find((e) => e.type === ActionsSheet)!;
|
||||
const before = props({ ask: { kind: "sheet", names: ["games"] } });
|
||||
expect(sheet(before).props.actions).not.toContain("showpc");
|
||||
resetHandoffSupport(true);
|
||||
const p = props({ ask: { kind: "sheet", names: ["a.txt"] } });
|
||||
expect(sheet(p).props.actions).toContain("showpc");
|
||||
(sheet(p).props.onChoose as (a: string) => void)("showpc");
|
||||
expect(p.onShowPc).toHaveBeenCalledWith("/data/a.txt");
|
||||
// Several items: not offered (one place at a time).
|
||||
const many = props({ ask: { kind: "sheet", names: ["a.txt", "games"] } });
|
||||
expect(sheet(many).props.actions).not.toContain("showpc");
|
||||
});
|
||||
|
||||
it("the sheet draws it with its label", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<ActionsSheet title="x" actions={["showpc"]} busy={false} onChoose={() => {}} onClose={() => {}} />,
|
||||
);
|
||||
expect(html).toContain(t("cw_files_show_pc"));
|
||||
});
|
||||
|
||||
it("puts the handoff for the PC and says so; a refusal says that", async () => {
|
||||
mockCall.mockResolvedValueOnce({} as never);
|
||||
expect(await showOnPc("/data/games")).toEqual({ tone: "info", text: t("cw_files_show_pc_done") });
|
||||
expect(mockCall).toHaveBeenCalledWith("/api/files/handoff/put", { to: "pc", path: "/data/games" });
|
||||
mockCall.mockRejectedValueOnce(new ApiError(400, "bad_side"));
|
||||
expect(await showOnPc("/data/games")).toEqual({ tone: "error", text: t("cw_files_show_pc_failed") });
|
||||
});
|
||||
});
|
||||
|
||||
describe("Uploading badge", () => {
|
||||
it("names the entries a running upload writes into", () => {
|
||||
const roots = ["/data/homebrew/Game", "/data/homebrew/Other/deep/x", "/mnt/usb0/a", "/data/homebrewX/y"];
|
||||
expect([...uploadingNames("/data/homebrew", roots)].sort()).toEqual(["Game", "Other"]);
|
||||
expect([...uploadingNames("/data/homebrew/", roots)].sort()).toEqual(["Game", "Other"]);
|
||||
expect([...uploadingNames("/", roots)].sort()).toEqual(["data", "mnt"]);
|
||||
expect(uploadingNames("/data/homebrew/Game", roots).size).toBe(0); // the root itself is shown one level up
|
||||
expect(uploadingNames(null, roots).size).toBe(0);
|
||||
});
|
||||
|
||||
it("draws the badge on those rows only", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<DirList
|
||||
entries={[entry("Game", "dir"), entry("Done", "dir")]}
|
||||
selecting={false}
|
||||
selected={[]}
|
||||
onEntry={() => {}}
|
||||
onToggleItem={() => {}}
|
||||
onActions={() => {}}
|
||||
uploading={new Set(["Game"])}
|
||||
/>,
|
||||
);
|
||||
expect(html.match(/data-uploading/g)?.length).toBe(1);
|
||||
expect(html).toContain(t("cw_files_uploading"));
|
||||
const game = html.slice(html.indexOf('data-entry="Game"'), html.indexOf('data-entry="Done"'));
|
||||
expect(game).toContain("data-uploading");
|
||||
});
|
||||
|
||||
it("FilesView passes the badges to the list", () => {
|
||||
const html = renderToStaticMarkup(<FilesView {...props({ uploading: new Set(["games"]) })} />);
|
||||
expect(html).toContain("data-uploading");
|
||||
});
|
||||
|
||||
it("/api/jobs' uploading list is read alongside the jobs (bad entries dropped)", async () => {
|
||||
mockCall.mockResolvedValueOnce({ jobs: [], uploading: ["/data/x", 3, "rel"] } as never);
|
||||
expect(await fetchJobsAll()).toEqual({ jobs: [], uploading: ["/data/x"] });
|
||||
mockCall.mockResolvedValueOnce({ jobs: [] } as never);
|
||||
expect(await fetchJobsAll()).toEqual({ jobs: [], uploading: [] });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Tied into the PC app: "Show on TV" (the PC's File System left a folder for
|
||||
* this page: App polls for it and opens Files there), "Show on PC" (the
|
||||
* actions sheet leaves a folder for the PC app, which polls the helper through
|
||||
* its engine), and the "Uploading" badge on folders a PC is uploading into.
|
||||
* Handoffs live in helper memory for two minutes (payload/src/files_handoff.c).
|
||||
*/
|
||||
import { useEffect } from "react";
|
||||
import { MonitorUp } from "lucide-react";
|
||||
import { ApiError, call } from "../api";
|
||||
import type { Message } from "../home/Home";
|
||||
import type { SectionParams } from "../sections";
|
||||
import { t } from "../strings";
|
||||
import { pollWhileVisible } from "../ui/visiblePoll";
|
||||
|
||||
/** How often the page asks for a handoff from the PC (while it is visible). */
|
||||
export const HANDOFF_POLL_MS = 3000;
|
||||
|
||||
/** The actions sheet's "Show on PC" item (ActionsSheet's ITEMS entry). */
|
||||
export const SHOW_ON_PC_ITEM = { icon: MonitorUp, key: "cw_files_show_pc" };
|
||||
|
||||
/**
|
||||
* Whether the helper has handoffs: unknown until the first poll answers, then
|
||||
* true, or false for good on a 404 (a helper without the routes). "Show on PC"
|
||||
* is offered only when true.
|
||||
*/
|
||||
let supported: boolean | null = null;
|
||||
|
||||
export function canShowOnPc(): boolean {
|
||||
return supported === true;
|
||||
}
|
||||
|
||||
/** Tests: forget what the last helper said. */
|
||||
export function resetHandoffSupport(value: boolean | null = null): void {
|
||||
supported = value;
|
||||
}
|
||||
|
||||
/** Takes the folder the PC left for this page: its path, or null when none is waiting. */
|
||||
export async function takeHandoff(): Promise<string | null> {
|
||||
try {
|
||||
const r = await call<{ path?: unknown }>("/api/files/handoff/take", { for: "console" });
|
||||
supported = true;
|
||||
return typeof r?.path === "string" && r.path.startsWith("/") ? r.path : null;
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError && e.status === 404) supported = false;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const parentOf = (p: string) => {
|
||||
const i = p.lastIndexOf("/");
|
||||
return i <= 0 ? "/" : p.slice(0, i);
|
||||
};
|
||||
|
||||
/**
|
||||
* Where Files opens for a handoff: the folder itself, or a file's folder with
|
||||
* the file focused. A path that is gone opens no folder but says so.
|
||||
*/
|
||||
export async function handoffTarget(path: string): Promise<SectionParams> {
|
||||
try {
|
||||
const st = await call<{ kind?: unknown }>("/api/fs/stat", { path });
|
||||
if (st?.kind === "dir") return { path, handoff: "pc" };
|
||||
return { path: parentOf(path), focus: path.slice(path.lastIndexOf("/") + 1), handoff: "pc" };
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError && e.status === 404) return { handoff_gone: path };
|
||||
// Any other refusal: open it, and Files says what is wrong with the folder.
|
||||
return { path, handoff: "pc" };
|
||||
}
|
||||
}
|
||||
|
||||
/** The line Files shows for the handoff it was opened with, or null. */
|
||||
export function handoffMessage(params: SectionParams | null): Message | null {
|
||||
if (!params) return null;
|
||||
if (params.handoff_gone) return { tone: "info", text: t("cw_files_from_pc_gone", { path: params.handoff_gone }) };
|
||||
if (params.handoff === "pc") return { tone: "info", text: t("cw_files_from_pc") };
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Files: show the handoff's line once per opening. */
|
||||
export function useHandoffNotice(params: SectionParams | null, show: (m: Message) => void): void {
|
||||
useEffect(() => {
|
||||
const m = handoffMessage(params);
|
||||
if (m) show(m);
|
||||
}, [params, show]);
|
||||
}
|
||||
|
||||
/**
|
||||
* One check: take the PC's handoff and, when there is one, open Files at its
|
||||
* target. True when something was opened.
|
||||
*/
|
||||
export async function checkHandoff(open: (params: SectionParams) => void, live: () => boolean = () => true): Promise<boolean> {
|
||||
const path = await takeHandoff();
|
||||
if (!path || !live()) return false;
|
||||
const target = await handoffTarget(path);
|
||||
if (!live()) return false;
|
||||
open(target);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* App: while the page is visible, ask every few seconds whether the PC left a
|
||||
* folder, and open Files there. Stops on a helper without handoffs.
|
||||
*/
|
||||
export function useHandoffPoll(open: (params: SectionParams) => void, enabled = true): void {
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
let live = true;
|
||||
let busy = false;
|
||||
let stop = () => {};
|
||||
const tick = () => {
|
||||
if (busy || supported === false) return;
|
||||
busy = true;
|
||||
checkHandoff(open, () => live)
|
||||
.catch(() => {
|
||||
if (supported === false) stop();
|
||||
})
|
||||
.finally(() => {
|
||||
busy = false;
|
||||
});
|
||||
};
|
||||
stop = pollWhileVisible(tick, HANDOFF_POLL_MS);
|
||||
return () => {
|
||||
live = false;
|
||||
stop();
|
||||
};
|
||||
}, [open, enabled]);
|
||||
}
|
||||
|
||||
/** "Show on PC": leave `path` for the PC app. The line to show either way. */
|
||||
export async function showOnPc(path: string): Promise<Message> {
|
||||
try {
|
||||
await call("/api/files/handoff/put", { to: "pc", path });
|
||||
return { tone: "info", text: t("cw_files_show_pc_done") };
|
||||
} catch {
|
||||
return { tone: "error", text: t("cw_files_show_pc_failed") };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The entries of folder `dir` that a running upload is writing into: an upload's
|
||||
* root is the entry itself or lies inside it.
|
||||
*/
|
||||
export function uploadingNames(dir: string | null, roots: readonly string[]): Set<string> {
|
||||
const out = new Set<string>();
|
||||
if (dir == null || !roots.length) return out;
|
||||
const base = dir === "/" ? "/" : `${dir.replace(/\/+$/, "")}/`;
|
||||
for (const r of roots) {
|
||||
if (!r.startsWith(base)) continue;
|
||||
const rest = r.slice(base.length);
|
||||
const name = rest.split("/")[0];
|
||||
if (name) out.add(name);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -39,10 +39,18 @@ export function visibleJobs(jobs: Job[]): Job[] {
|
||||
return running.concat(jobs.filter((j) => j.state !== "running" && j.mine));
|
||||
}
|
||||
|
||||
/** One read of /api/jobs: the jobs, and where running uploads (a PC's) are writing. */
|
||||
export async function fetchJobsAll(): Promise<{ jobs: Job[]; uploading: string[] }> {
|
||||
const r = await call<{ jobs?: unknown; uploading?: unknown }>("/api/jobs");
|
||||
const uploading = Array.isArray(r?.uploading)
|
||||
? r.uploading.filter((x): x is string => typeof x === "string" && x.startsWith("/"))
|
||||
: [];
|
||||
return { jobs: Array.isArray(r?.jobs) ? (r.jobs as Job[]) : [], uploading };
|
||||
}
|
||||
|
||||
/** One read of /api/jobs. */
|
||||
export async function fetchJobs(): Promise<Job[]> {
|
||||
const r = await call<{ jobs?: unknown }>("/api/jobs");
|
||||
return Array.isArray(r?.jobs) ? (r.jobs as Job[]) : [];
|
||||
return (await fetchJobsAll()).jobs;
|
||||
}
|
||||
|
||||
/** Ask the helper to stop job `id` (any owner's). */
|
||||
@@ -58,7 +66,10 @@ export interface JobsPoller {
|
||||
}
|
||||
|
||||
/** The polling loop behind useJobs, with no React in it. */
|
||||
export function createJobsPoller(opts: { onJobs: (jobs: Job[]) => void; onError: (err: unknown) => void }): JobsPoller {
|
||||
export function createJobsPoller(opts: {
|
||||
onJobs: (jobs: Job[], uploading: string[]) => void;
|
||||
onError: (err: unknown) => void;
|
||||
}): JobsPoller {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
let stopped = false;
|
||||
let inFlight = false;
|
||||
@@ -80,9 +91,9 @@ export function createJobsPoller(opts: { onJobs: (jobs: Job[]) => void; onError:
|
||||
inFlight = true;
|
||||
let delay = JOBS_SLOW_MS;
|
||||
try {
|
||||
const jobs = await fetchJobs();
|
||||
const { jobs, uploading } = await fetchJobsAll();
|
||||
if (stopped) return;
|
||||
opts.onJobs(jobs);
|
||||
opts.onJobs(jobs, uploading);
|
||||
delay = nextJobsDelay(jobs);
|
||||
} catch (e) {
|
||||
if (stopped) return;
|
||||
@@ -120,6 +131,8 @@ export function createJobsPoller(opts: { onJobs: (jobs: Job[]) => void; onError:
|
||||
|
||||
export interface JobsState {
|
||||
jobs: Job[];
|
||||
/** Where running uploads (a PC's) are writing: Files badges those folders. */
|
||||
uploading: string[];
|
||||
/** The first answer has come in. */
|
||||
loaded: boolean;
|
||||
/** The last read failed (null after a good one). */
|
||||
@@ -132,14 +145,16 @@ export interface JobsState {
|
||||
|
||||
export function useJobs(): JobsState {
|
||||
const [jobs, setJobs] = useState<Job[]>([]);
|
||||
const [uploading, setUploading] = useState<string[]>([]);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [error, setError] = useState<unknown>(null);
|
||||
const poller = useRef<JobsPoller | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const p = createJobsPoller({
|
||||
onJobs: (j) => {
|
||||
onJobs: (j, u) => {
|
||||
setJobs(j);
|
||||
setUploading((prev) => (prev.length === u.length && prev.every((x, i) => x === u[i]) ? prev : u));
|
||||
setLoaded(true);
|
||||
setError(null);
|
||||
},
|
||||
@@ -165,5 +180,5 @@ export function useJobs(): JobsState {
|
||||
poller.current?.refresh();
|
||||
}, []);
|
||||
|
||||
return { jobs, loaded, error, cancel, refresh };
|
||||
return { jobs, uploading, loaded, error, cancel, refresh };
|
||||
}
|
||||
@@ -99,6 +99,8 @@
|
||||
"cw_files_fixperm_body": "يجعل كل ما في هذا المجلد قابلاً للقراءة والتشغيل من قبل PS5 (chmod 777). استخدمه عندما لا تبدأ اللعبة بعد النسخ.",
|
||||
"cw_files_fixperm_done": "تم إصلاح الأذونات.",
|
||||
"cw_files_free": "{free} متاحة",
|
||||
"cw_files_from_pc": "فُتح من الكمبيوتر.",
|
||||
"cw_files_from_pc_gone": "أرسل الكمبيوتر {path}، لكنه لم يعد على الجهاز.",
|
||||
"cw_files_goto": "الانتقال إلى مسار…",
|
||||
"cw_files_goto_bad": "اكتب مسارًا كاملًا يبدأ بـ /.",
|
||||
"cw_files_goto_failed": "تعذّر التحقق من هذا المسار ({code}).",
|
||||
@@ -155,6 +157,9 @@
|
||||
"cw_files_select_invert": "عكس التحديد",
|
||||
"cw_files_select_none": "إلغاء التحديد",
|
||||
"cw_files_selected": "تم تحديد {n}",
|
||||
"cw_files_show_pc": "العرض على الكمبيوتر",
|
||||
"cw_files_show_pc_done": "أُرسل إلى الكمبيوتر. يُفتح في نظام ملفات PS5Upload.",
|
||||
"cw_files_show_pc_failed": "تعذّر الإرسال إلى الكمبيوتر.",
|
||||
"cw_files_some_failed": "فشل {n}: {reason}",
|
||||
"cw_files_sort_date": "ترتيب حسب التاريخ",
|
||||
"cw_files_sort_name": "ترتيب حسب الاسم",
|
||||
@@ -179,6 +184,7 @@
|
||||
"cw_files_unpin": "إلغاء التثبيت",
|
||||
"cw_files_unpin_done": "تم إلغاء تثبيت {name}.",
|
||||
"cw_files_up": "لأعلى",
|
||||
"cw_files_uploading": "جارٍ الرفع",
|
||||
"cw_files_view": "عرض",
|
||||
"cw_help_close": "إغلاق",
|
||||
"cw_help_controls": "التحكم",
|
||||
|
||||
@@ -99,6 +99,8 @@
|
||||
"cw_files_fixperm_body": "এই ফোল্ডারের সবকিছু PS5-এর জন্য পড়ার ও চালানোর উপযোগী করে (chmod 777)। কপি করার পর গেম চালু না হলে এটি ব্যবহার করুন।",
|
||||
"cw_files_fixperm_done": "অনুমতি ঠিক করা হয়েছে।",
|
||||
"cw_files_free": "{free} খালি",
|
||||
"cw_files_from_pc": "আপনার PC থেকে খোলা হয়েছে।",
|
||||
"cw_files_from_pc_gone": "আপনার PC {path} পাঠিয়েছে, কিন্তু এটি আর কনসোলে নেই।",
|
||||
"cw_files_goto": "পাথে যান…",
|
||||
"cw_files_goto_bad": "/ দিয়ে শুরু হওয়া একটি পূর্ণ পাথ লিখুন।",
|
||||
"cw_files_goto_failed": "ওই পাথটি যাচাই করা যায়নি ({code})।",
|
||||
@@ -155,6 +157,9 @@
|
||||
"cw_files_select_invert": "উল্টে দিন",
|
||||
"cw_files_select_none": "বাছাই সরান",
|
||||
"cw_files_selected": "{n}টি নির্বাচিত",
|
||||
"cw_files_show_pc": "PC-তে দেখান",
|
||||
"cw_files_show_pc_done": "আপনার PC-তে পাঠানো হয়েছে। এটি PS5Upload-এর ফাইল সিস্টেমে খুলবে।",
|
||||
"cw_files_show_pc_failed": "PC-তে পাঠানো যায়নি।",
|
||||
"cw_files_some_failed": "{n}টি ব্যর্থ: {reason}",
|
||||
"cw_files_sort_date": "তারিখ অনুযায়ী সাজান",
|
||||
"cw_files_sort_name": "নাম অনুযায়ী সাজান",
|
||||
@@ -179,6 +184,7 @@
|
||||
"cw_files_unpin": "পিন সরান",
|
||||
"cw_files_unpin_done": "{name}-এর পিন সরানো হয়েছে।",
|
||||
"cw_files_up": "উপরে",
|
||||
"cw_files_uploading": "আপলোড হচ্ছে",
|
||||
"cw_files_view": "দেখুন",
|
||||
"cw_help_close": "বন্ধ করুন",
|
||||
"cw_help_controls": "কন্ট্রোল",
|
||||
|
||||
@@ -99,6 +99,8 @@
|
||||
"cw_files_fixperm_body": "Macht alles in diesem Ordner für die PS5 lesbar und ausführbar (chmod 777). Nutze es, wenn ein Spiel nach dem Kopieren nicht startet.",
|
||||
"cw_files_fixperm_done": "Berechtigungen repariert.",
|
||||
"cw_files_free": "{free} frei",
|
||||
"cw_files_from_pc": "Von deinem PC geöffnet.",
|
||||
"cw_files_from_pc_gone": "Dein PC hat {path} gesendet, aber es ist nicht mehr auf der Konsole.",
|
||||
"cw_files_goto": "Gehe zu Pfad…",
|
||||
"cw_files_goto_bad": "Gib einen vollständigen Pfad ein, der mit / beginnt.",
|
||||
"cw_files_goto_failed": "Der Pfad konnte nicht geprüft werden ({code}).",
|
||||
@@ -155,6 +157,9 @@
|
||||
"cw_files_select_invert": "Umkehren",
|
||||
"cw_files_select_none": "Auswahl aufheben",
|
||||
"cw_files_selected": "{n} ausgewählt",
|
||||
"cw_files_show_pc": "Auf dem PC zeigen",
|
||||
"cw_files_show_pc_done": "An deinen PC gesendet. Es öffnet sich im Dateisystem von PS5Upload.",
|
||||
"cw_files_show_pc_failed": "Konnte nicht an den PC gesendet werden.",
|
||||
"cw_files_some_failed": "{n} fehlgeschlagen: {reason}",
|
||||
"cw_files_sort_date": "Nach Datum sortieren",
|
||||
"cw_files_sort_name": "Nach Name sortieren",
|
||||
@@ -179,6 +184,7 @@
|
||||
"cw_files_unpin": "Lösen",
|
||||
"cw_files_unpin_done": "{name} gelöst.",
|
||||
"cw_files_up": "Nach oben",
|
||||
"cw_files_uploading": "Wird hochgeladen",
|
||||
"cw_files_view": "Ansehen",
|
||||
"cw_help_close": "Schließen",
|
||||
"cw_help_controls": "Steuerung",
|
||||
|
||||
@@ -99,6 +99,8 @@
|
||||
"cw_files_fixperm_body": "Makes everything in this folder readable and runnable by the PS5 (chmod 777). Use it when a game won't start after copying.",
|
||||
"cw_files_fixperm_done": "Permissions fixed.",
|
||||
"cw_files_free": "{free} free",
|
||||
"cw_files_from_pc": "Opened from your PC.",
|
||||
"cw_files_from_pc_gone": "Your PC sent {path}, but it isn't on the console any more.",
|
||||
"cw_files_goto": "Go to path…",
|
||||
"cw_files_goto_bad": "Type a full path that starts with /.",
|
||||
"cw_files_goto_failed": "Couldn't check that path ({code}).",
|
||||
@@ -155,6 +157,9 @@
|
||||
"cw_files_select_invert": "Invert",
|
||||
"cw_files_select_none": "Select none",
|
||||
"cw_files_selected": "{n} selected",
|
||||
"cw_files_show_pc": "Show on PC",
|
||||
"cw_files_show_pc_done": "Sent to your PC. It opens in PS5Upload's File System.",
|
||||
"cw_files_show_pc_failed": "Couldn't send it to the PC.",
|
||||
"cw_files_some_failed": "{n} failed: {reason}",
|
||||
"cw_files_sort_date": "Sort by date",
|
||||
"cw_files_sort_name": "Sort by name",
|
||||
@@ -179,6 +184,7 @@
|
||||
"cw_files_unpin": "Unpin",
|
||||
"cw_files_unpin_done": "Unpinned {name}.",
|
||||
"cw_files_up": "Up",
|
||||
"cw_files_uploading": "Uploading",
|
||||
"cw_files_view": "View",
|
||||
"cw_help_close": "Close",
|
||||
"cw_help_controls": "Controls",
|
||||
|
||||
@@ -99,6 +99,8 @@
|
||||
"cw_files_fixperm_body": "Hace que todo lo de esta carpeta sea legible y ejecutable por la PS5 (chmod 777). Úsalo cuando un juego no arranque tras copiarlo.",
|
||||
"cw_files_fixperm_done": "Permisos corregidos.",
|
||||
"cw_files_free": "{free} libres",
|
||||
"cw_files_from_pc": "Abierto desde tu PC.",
|
||||
"cw_files_from_pc_gone": "Tu PC envió {path}, pero ya no está en la consola.",
|
||||
"cw_files_goto": "Ir a la ruta…",
|
||||
"cw_files_goto_bad": "Escribe una ruta completa que empiece por /.",
|
||||
"cw_files_goto_failed": "No se pudo comprobar esa ruta ({code}).",
|
||||
@@ -155,6 +157,9 @@
|
||||
"cw_files_select_invert": "Invertir",
|
||||
"cw_files_select_none": "Quitar selección",
|
||||
"cw_files_selected": "{n} seleccionados",
|
||||
"cw_files_show_pc": "Mostrar en el PC",
|
||||
"cw_files_show_pc_done": "Enviado a tu PC. Se abre en el sistema de archivos de PS5Upload.",
|
||||
"cw_files_show_pc_failed": "No se pudo enviar al PC.",
|
||||
"cw_files_some_failed": "{n} fallaron: {reason}",
|
||||
"cw_files_sort_date": "Ordenar por fecha",
|
||||
"cw_files_sort_name": "Ordenar por nombre",
|
||||
@@ -179,6 +184,7 @@
|
||||
"cw_files_unpin": "Quitar fijación",
|
||||
"cw_files_unpin_done": "{name} ya no está fijada.",
|
||||
"cw_files_up": "Subir",
|
||||
"cw_files_uploading": "Subiendo",
|
||||
"cw_files_view": "Ver",
|
||||
"cw_help_close": "Cerrar",
|
||||
"cw_help_controls": "Controles",
|
||||
|
||||
@@ -99,6 +99,8 @@
|
||||
"cw_files_fixperm_body": "همه چیز در این پوشه را برای PS5 قابل خواندن و اجرا میکند (chmod 777). وقتی بازی بعد از کپی شروع نمیشود از آن استفاده کنید.",
|
||||
"cw_files_fixperm_done": "مجوزها اصلاح شد.",
|
||||
"cw_files_free": "{free} آزاد",
|
||||
"cw_files_from_pc": "از رایانهٔ شما باز شد.",
|
||||
"cw_files_from_pc_gone": "رایانهٔ شما {path} را فرستاد، اما دیگر روی کنسول نیست.",
|
||||
"cw_files_goto": "رفتن به مسیر…",
|
||||
"cw_files_goto_bad": "یک مسیر کامل وارد کنید که با / شروع شود.",
|
||||
"cw_files_goto_failed": "بررسی این مسیر ممکن نشد ({code}).",
|
||||
@@ -155,6 +157,9 @@
|
||||
"cw_files_select_invert": "معکوس کردن",
|
||||
"cw_files_select_none": "لغو انتخاب",
|
||||
"cw_files_selected": "{n} انتخاب شده",
|
||||
"cw_files_show_pc": "نمایش روی رایانه",
|
||||
"cw_files_show_pc_done": "به رایانهٔ شما فرستاده شد. در سیستم فایل PS5Upload باز میشود.",
|
||||
"cw_files_show_pc_failed": "فرستادن به رایانه ممکن نشد.",
|
||||
"cw_files_some_failed": "{n} ناموفق: {reason}",
|
||||
"cw_files_sort_date": "مرتبسازی بر اساس تاریخ",
|
||||
"cw_files_sort_name": "مرتبسازی بر اساس نام",
|
||||
@@ -179,6 +184,7 @@
|
||||
"cw_files_unpin": "برداشتن سنجاق",
|
||||
"cw_files_unpin_done": "سنجاق {name} برداشته شد.",
|
||||
"cw_files_up": "بالا",
|
||||
"cw_files_uploading": "در حال بارگذاری",
|
||||
"cw_files_view": "نمایش",
|
||||
"cw_help_close": "بستن",
|
||||
"cw_help_controls": "کنترلها",
|
||||
|
||||
@@ -99,6 +99,8 @@
|
||||
"cw_files_fixperm_body": "Rend tout le contenu de ce dossier lisible et exécutable par la PS5 (chmod 777). À utiliser quand un jeu ne démarre pas après la copie.",
|
||||
"cw_files_fixperm_done": "Permissions corrigées.",
|
||||
"cw_files_free": "{free} libres",
|
||||
"cw_files_from_pc": "Ouvert depuis votre PC.",
|
||||
"cw_files_from_pc_gone": "Votre PC a envoyé {path}, mais il n'est plus sur la console.",
|
||||
"cw_files_goto": "Aller au chemin…",
|
||||
"cw_files_goto_bad": "Saisis un chemin complet qui commence par /.",
|
||||
"cw_files_goto_failed": "Impossible de vérifier ce chemin ({code}).",
|
||||
@@ -155,6 +157,9 @@
|
||||
"cw_files_select_invert": "Inverser",
|
||||
"cw_files_select_none": "Tout désélectionner",
|
||||
"cw_files_selected": "{n} sélectionné(s)",
|
||||
"cw_files_show_pc": "Afficher sur le PC",
|
||||
"cw_files_show_pc_done": "Envoyé à votre PC. Il s'ouvre dans le système de fichiers de PS5Upload.",
|
||||
"cw_files_show_pc_failed": "Impossible de l'envoyer au PC.",
|
||||
"cw_files_some_failed": "{n} en échec : {reason}",
|
||||
"cw_files_sort_date": "Trier par date",
|
||||
"cw_files_sort_name": "Trier par nom",
|
||||
@@ -179,6 +184,7 @@
|
||||
"cw_files_unpin": "Désépingler",
|
||||
"cw_files_unpin_done": "{name} désépinglé.",
|
||||
"cw_files_up": "Remonter",
|
||||
"cw_files_uploading": "Envoi en cours",
|
||||
"cw_files_view": "Afficher",
|
||||
"cw_help_close": "Fermer",
|
||||
"cw_help_controls": "Commandes",
|
||||
|
||||
@@ -99,6 +99,8 @@
|
||||
"cw_files_fixperm_body": "इस फ़ोल्डर की हर चीज़ को PS5 के लिए पढ़ने और चलाने योग्य बनाता है (chmod 777)। कॉपी के बाद गेम शुरू न हो तो इसका उपयोग करें।",
|
||||
"cw_files_fixperm_done": "अनुमतियाँ ठीक कर दी गईं।",
|
||||
"cw_files_free": "{free} खाली",
|
||||
"cw_files_from_pc": "आपके PC से खोला गया।",
|
||||
"cw_files_from_pc_gone": "आपके PC ने {path} भेजा, पर यह अब कंसोल पर नहीं है।",
|
||||
"cw_files_goto": "पथ पर जाएँ…",
|
||||
"cw_files_goto_bad": "/ से शुरू होने वाला पूरा पथ लिखें।",
|
||||
"cw_files_goto_failed": "उस पथ की जाँच नहीं हो सकी ({code})।",
|
||||
@@ -155,6 +157,9 @@
|
||||
"cw_files_select_invert": "चयन उलटें",
|
||||
"cw_files_select_none": "चयन हटाएँ",
|
||||
"cw_files_selected": "{n} चुने गए",
|
||||
"cw_files_show_pc": "PC पर दिखाएँ",
|
||||
"cw_files_show_pc_done": "आपके PC पर भेजा गया। यह PS5Upload के फ़ाइल सिस्टम में खुलेगा।",
|
||||
"cw_files_show_pc_failed": "PC पर नहीं भेज सके।",
|
||||
"cw_files_some_failed": "{n} विफल: {reason}",
|
||||
"cw_files_sort_date": "तारीख़ के अनुसार क्रम",
|
||||
"cw_files_sort_name": "नाम के अनुसार क्रम",
|
||||
@@ -179,6 +184,7 @@
|
||||
"cw_files_unpin": "पिन हटाएँ",
|
||||
"cw_files_unpin_done": "{name} का पिन हटाया गया।",
|
||||
"cw_files_up": "ऊपर",
|
||||
"cw_files_uploading": "अपलोड हो रहा है",
|
||||
"cw_files_view": "देखें",
|
||||
"cw_help_close": "बंद करें",
|
||||
"cw_help_controls": "कंट्रोल",
|
||||
|
||||
@@ -99,6 +99,8 @@
|
||||
"cw_files_fixperm_body": "Az ebben a mappában lévő mindent olvashatóvá és futtathatóvá tesz a PS5 számára (chmod 777). Akkor használd, ha egy játék másolás után nem indul.",
|
||||
"cw_files_fixperm_done": "Jogosultságok javítva.",
|
||||
"cw_files_free": "{free} szabad",
|
||||
"cw_files_from_pc": "Megnyitva a PC-dről.",
|
||||
"cw_files_from_pc_gone": "A PC-d elküldte ezt: {path}, de már nincs a konzolon.",
|
||||
"cw_files_goto": "Ugrás útvonalra…",
|
||||
"cw_files_goto_bad": "Adj meg egy teljes, / jellel kezdődő útvonalat.",
|
||||
"cw_files_goto_failed": "Az útvonalat nem sikerült ellenőrizni ({code}).",
|
||||
@@ -155,6 +157,9 @@
|
||||
"cw_files_select_invert": "Megfordítás",
|
||||
"cw_files_select_none": "Kijelölés törlése",
|
||||
"cw_files_selected": "{n} kijelölve",
|
||||
"cw_files_show_pc": "Megjelenítés a PC-n",
|
||||
"cw_files_show_pc_done": "Elküldve a PC-dre. A PS5Upload fájlrendszerében nyílik meg.",
|
||||
"cw_files_show_pc_failed": "Nem sikerült elküldeni a PC-re.",
|
||||
"cw_files_some_failed": "{n} sikertelen: {reason}",
|
||||
"cw_files_sort_date": "Rendezés dátum szerint",
|
||||
"cw_files_sort_name": "Rendezés név szerint",
|
||||
@@ -179,6 +184,7 @@
|
||||
"cw_files_unpin": "Kitűzés megszüntetése",
|
||||
"cw_files_unpin_done": "{name} kitűzése megszüntetve.",
|
||||
"cw_files_up": "Fel",
|
||||
"cw_files_uploading": "Feltöltés",
|
||||
"cw_files_view": "Megtekintés",
|
||||
"cw_help_close": "Bezárás",
|
||||
"cw_help_controls": "Irányítás",
|
||||
|
||||
@@ -99,6 +99,8 @@
|
||||
"cw_files_fixperm_body": "Membuat semua isi folder ini dapat dibaca dan dijalankan oleh PS5 (chmod 777). Gunakan saat game tidak mau mulai setelah disalin.",
|
||||
"cw_files_fixperm_done": "Izin diperbaiki.",
|
||||
"cw_files_free": "{free} kosong",
|
||||
"cw_files_from_pc": "Dibuka dari PC Anda.",
|
||||
"cw_files_from_pc_gone": "PC Anda mengirim {path}, tetapi sudah tidak ada di konsol.",
|
||||
"cw_files_goto": "Buka jalur…",
|
||||
"cw_files_goto_bad": "Ketik jalur lengkap yang diawali /.",
|
||||
"cw_files_goto_failed": "Tidak dapat memeriksa jalur itu ({code}).",
|
||||
@@ -155,6 +157,9 @@
|
||||
"cw_files_select_invert": "Balikkan",
|
||||
"cw_files_select_none": "Batalkan pilihan",
|
||||
"cw_files_selected": "{n} dipilih",
|
||||
"cw_files_show_pc": "Tampilkan di PC",
|
||||
"cw_files_show_pc_done": "Dikirim ke PC Anda. Terbuka di Sistem File PS5Upload.",
|
||||
"cw_files_show_pc_failed": "Tidak dapat mengirim ke PC.",
|
||||
"cw_files_some_failed": "{n} gagal: {reason}",
|
||||
"cw_files_sort_date": "Urutkan menurut tanggal",
|
||||
"cw_files_sort_name": "Urutkan menurut nama",
|
||||
@@ -179,6 +184,7 @@
|
||||
"cw_files_unpin": "Lepas sematan",
|
||||
"cw_files_unpin_done": "Sematan {name} dilepas.",
|
||||
"cw_files_up": "Naik",
|
||||
"cw_files_uploading": "Mengunggah",
|
||||
"cw_files_view": "Lihat",
|
||||
"cw_help_close": "Tutup",
|
||||
"cw_help_controls": "Kontrol",
|
||||
|
||||
@@ -99,6 +99,8 @@
|
||||
"cw_files_fixperm_body": "Rende tutto il contenuto di questa cartella leggibile ed eseguibile dalla PS5 (chmod 777). Usalo quando un gioco non si avvia dopo la copia.",
|
||||
"cw_files_fixperm_done": "Permessi corretti.",
|
||||
"cw_files_free": "{free} liberi",
|
||||
"cw_files_from_pc": "Aperto dal tuo PC.",
|
||||
"cw_files_from_pc_gone": "Il tuo PC ha inviato {path}, ma non è più sulla console.",
|
||||
"cw_files_goto": "Vai al percorso…",
|
||||
"cw_files_goto_bad": "Scrivi un percorso completo che inizi con /.",
|
||||
"cw_files_goto_failed": "Impossibile verificare il percorso ({code}).",
|
||||
@@ -155,6 +157,9 @@
|
||||
"cw_files_select_invert": "Inverti",
|
||||
"cw_files_select_none": "Deseleziona tutto",
|
||||
"cw_files_selected": "{n} selezionati",
|
||||
"cw_files_show_pc": "Mostra sul PC",
|
||||
"cw_files_show_pc_done": "Inviato al tuo PC. Si apre nel file system di PS5Upload.",
|
||||
"cw_files_show_pc_failed": "Impossibile inviarlo al PC.",
|
||||
"cw_files_some_failed": "{n} non riusciti: {reason}",
|
||||
"cw_files_sort_date": "Ordina per data",
|
||||
"cw_files_sort_name": "Ordina per nome",
|
||||
@@ -179,6 +184,7 @@
|
||||
"cw_files_unpin": "Rimuovi",
|
||||
"cw_files_unpin_done": "{name} rimossa dalle fissate.",
|
||||
"cw_files_up": "Su",
|
||||
"cw_files_uploading": "Caricamento",
|
||||
"cw_files_view": "Visualizza",
|
||||
"cw_help_close": "Chiudi",
|
||||
"cw_help_controls": "Comandi",
|
||||
|
||||
@@ -99,6 +99,8 @@
|
||||
"cw_files_fixperm_body": "このフォルダー内のすべてを PS5 が読み取り・実行できるようにします(chmod 777)。コピー後にゲームが起動しないときに使います。",
|
||||
"cw_files_fixperm_done": "権限を修正しました。",
|
||||
"cw_files_free": "空き {free}",
|
||||
"cw_files_from_pc": "PC から開きました。",
|
||||
"cw_files_from_pc_gone": "PC から {path} が送られましたが、もうコンソールにありません。",
|
||||
"cw_files_goto": "パスへ移動…",
|
||||
"cw_files_goto_bad": "/ で始まる完全なパスを入力してください。",
|
||||
"cw_files_goto_failed": "パスを確認できませんでした({code})。",
|
||||
@@ -155,6 +157,9 @@
|
||||
"cw_files_select_invert": "選択を反転",
|
||||
"cw_files_select_none": "選択を解除",
|
||||
"cw_files_selected": "{n} 件選択中",
|
||||
"cw_files_show_pc": "PC で表示",
|
||||
"cw_files_show_pc_done": "PC に送信しました。PS5Upload のファイルシステムで開きます。",
|
||||
"cw_files_show_pc_failed": "PC に送信できませんでした。",
|
||||
"cw_files_some_failed": "{n} 件失敗: {reason}",
|
||||
"cw_files_sort_date": "日付順",
|
||||
"cw_files_sort_name": "名前順",
|
||||
@@ -179,6 +184,7 @@
|
||||
"cw_files_unpin": "ピン留めを外す",
|
||||
"cw_files_unpin_done": "{name} のピン留めを外しました。",
|
||||
"cw_files_up": "上へ",
|
||||
"cw_files_uploading": "アップロード中",
|
||||
"cw_files_view": "表示",
|
||||
"cw_help_close": "閉じる",
|
||||
"cw_help_controls": "操作方法",
|
||||
|
||||
@@ -99,6 +99,8 @@
|
||||
"cw_files_fixperm_body": "이 폴더의 모든 항목을 PS5가 읽고 실행할 수 있게 합니다(chmod 777). 복사 후 게임이 시작되지 않을 때 사용하세요.",
|
||||
"cw_files_fixperm_done": "권한을 수정했습니다.",
|
||||
"cw_files_free": "{free} 남음",
|
||||
"cw_files_from_pc": "PC에서 열었습니다.",
|
||||
"cw_files_from_pc_gone": "PC가 {path}을(를) 보냈지만 더 이상 콘솔에 없습니다.",
|
||||
"cw_files_goto": "경로로 이동…",
|
||||
"cw_files_goto_bad": "/로 시작하는 전체 경로를 입력하세요.",
|
||||
"cw_files_goto_failed": "경로를 확인하지 못했습니다({code}).",
|
||||
@@ -155,6 +157,9 @@
|
||||
"cw_files_select_invert": "선택 반전",
|
||||
"cw_files_select_none": "선택 해제",
|
||||
"cw_files_selected": "{n}개 선택됨",
|
||||
"cw_files_show_pc": "PC에서 보기",
|
||||
"cw_files_show_pc_done": "PC로 보냈습니다. PS5Upload의 파일 시스템에서 열립니다.",
|
||||
"cw_files_show_pc_failed": "PC로 보낼 수 없습니다.",
|
||||
"cw_files_some_failed": "{n}개 실패: {reason}",
|
||||
"cw_files_sort_date": "날짜순",
|
||||
"cw_files_sort_name": "이름순",
|
||||
@@ -179,6 +184,7 @@
|
||||
"cw_files_unpin": "고정 해제",
|
||||
"cw_files_unpin_done": "{name} 고정을 해제했습니다.",
|
||||
"cw_files_up": "위로",
|
||||
"cw_files_uploading": "업로드 중",
|
||||
"cw_files_view": "보기",
|
||||
"cw_help_close": "닫기",
|
||||
"cw_help_controls": "조작법",
|
||||
|
||||
@@ -99,6 +99,8 @@
|
||||
"cw_files_fixperm_body": "Sprawia, że wszystko w tym folderze jest czytelne i wykonywalne dla PS5 (chmod 777). Użyj, gdy gra nie uruchamia się po skopiowaniu.",
|
||||
"cw_files_fixperm_done": "Uprawnienia naprawione.",
|
||||
"cw_files_free": "Wolne: {free}",
|
||||
"cw_files_from_pc": "Otwarto z twojego PC.",
|
||||
"cw_files_from_pc_gone": "Twój PC wysłał {path}, ale tego nie ma już na konsoli.",
|
||||
"cw_files_goto": "Przejdź do ścieżki…",
|
||||
"cw_files_goto_bad": "Wpisz pełną ścieżkę zaczynającą się od /.",
|
||||
"cw_files_goto_failed": "Nie udało się sprawdzić tej ścieżki ({code}).",
|
||||
@@ -155,6 +157,9 @@
|
||||
"cw_files_select_invert": "Odwróć",
|
||||
"cw_files_select_none": "Odznacz wszystko",
|
||||
"cw_files_selected": "Zaznaczono: {n}",
|
||||
"cw_files_show_pc": "Pokaż na PC",
|
||||
"cw_files_show_pc_done": "Wysłano do twojego PC. Otworzy się w systemie plików PS5Upload.",
|
||||
"cw_files_show_pc_failed": "Nie udało się wysłać do PC.",
|
||||
"cw_files_some_failed": "Nieudane ({n}): {reason}",
|
||||
"cw_files_sort_date": "Sortuj według daty",
|
||||
"cw_files_sort_name": "Sortuj według nazwy",
|
||||
@@ -179,6 +184,7 @@
|
||||
"cw_files_unpin": "Odepnij",
|
||||
"cw_files_unpin_done": "Odpięto: {name}.",
|
||||
"cw_files_up": "W górę",
|
||||
"cw_files_uploading": "Przesyłanie",
|
||||
"cw_files_view": "Pokaż",
|
||||
"cw_help_close": "Zamknij",
|
||||
"cw_help_controls": "Sterowanie",
|
||||
|
||||
@@ -99,6 +99,8 @@
|
||||
"cw_files_fixperm_body": "Torna tudo nesta pasta legível e executável pelo PS5 (chmod 777). Use quando um jogo não iniciar depois de copiar.",
|
||||
"cw_files_fixperm_done": "Permissões corrigidas.",
|
||||
"cw_files_free": "{free} livres",
|
||||
"cw_files_from_pc": "Aberto pelo seu PC.",
|
||||
"cw_files_from_pc_gone": "Seu PC enviou {path}, mas não está mais no console.",
|
||||
"cw_files_goto": "Ir para o caminho…",
|
||||
"cw_files_goto_bad": "Digite um caminho completo que comece com /.",
|
||||
"cw_files_goto_failed": "Não foi possível verificar esse caminho ({code}).",
|
||||
@@ -155,6 +157,9 @@
|
||||
"cw_files_select_invert": "Inverter",
|
||||
"cw_files_select_none": "Limpar seleção",
|
||||
"cw_files_selected": "{n} selecionados",
|
||||
"cw_files_show_pc": "Mostrar no PC",
|
||||
"cw_files_show_pc_done": "Enviado ao seu PC. Abre no sistema de arquivos do PS5Upload.",
|
||||
"cw_files_show_pc_failed": "Não foi possível enviar ao PC.",
|
||||
"cw_files_some_failed": "{n} falharam: {reason}",
|
||||
"cw_files_sort_date": "Ordenar por data",
|
||||
"cw_files_sort_name": "Ordenar por nome",
|
||||
@@ -179,6 +184,7 @@
|
||||
"cw_files_unpin": "Desafixar",
|
||||
"cw_files_unpin_done": "{name} desafixada.",
|
||||
"cw_files_up": "Acima",
|
||||
"cw_files_uploading": "Enviando",
|
||||
"cw_files_view": "Ver",
|
||||
"cw_help_close": "Fechar",
|
||||
"cw_help_controls": "Controles",
|
||||
|
||||
@@ -99,6 +99,8 @@
|
||||
"cw_files_fixperm_body": "Делает всё в этой папке доступным для чтения и запуска на PS5 (chmod 777). Используйте, если игра не запускается после копирования.",
|
||||
"cw_files_fixperm_done": "Права исправлены.",
|
||||
"cw_files_free": "Свободно: {free}",
|
||||
"cw_files_from_pc": "Открыто с вашего ПК.",
|
||||
"cw_files_from_pc_gone": "Ваш ПК отправил {path}, но этого больше нет на консоли.",
|
||||
"cw_files_goto": "Перейти по пути…",
|
||||
"cw_files_goto_bad": "Введите полный путь, начинающийся с /.",
|
||||
"cw_files_goto_failed": "Не удалось проверить путь ({code}).",
|
||||
@@ -155,6 +157,9 @@
|
||||
"cw_files_select_invert": "Инвертировать",
|
||||
"cw_files_select_none": "Снять выбор",
|
||||
"cw_files_selected": "Выбрано: {n}",
|
||||
"cw_files_show_pc": "Показать на ПК",
|
||||
"cw_files_show_pc_done": "Отправлено на ваш ПК. Откроется в файловой системе PS5Upload.",
|
||||
"cw_files_show_pc_failed": "Не удалось отправить на ПК.",
|
||||
"cw_files_some_failed": "Не удалось ({n}): {reason}",
|
||||
"cw_files_sort_date": "По дате",
|
||||
"cw_files_sort_name": "По имени",
|
||||
@@ -179,6 +184,7 @@
|
||||
"cw_files_unpin": "Открепить",
|
||||
"cw_files_unpin_done": "{name} откреплена.",
|
||||
"cw_files_up": "Вверх",
|
||||
"cw_files_uploading": "Загрузка",
|
||||
"cw_files_view": "Просмотр",
|
||||
"cw_help_close": "Закрыть",
|
||||
"cw_help_controls": "Управление",
|
||||
|
||||
@@ -99,6 +99,8 @@
|
||||
"cw_files_fixperm_body": "ทำให้ทุกอย่างในโฟลเดอร์นี้ PS5 อ่านและเรียกใช้ได้ (chmod 777) ใช้เมื่อเกมไม่เริ่มหลังคัดลอก",
|
||||
"cw_files_fixperm_done": "แก้ไขสิทธิ์แล้ว",
|
||||
"cw_files_free": "ว่าง {free}",
|
||||
"cw_files_from_pc": "เปิดจาก PC ของคุณ",
|
||||
"cw_files_from_pc_gone": "PC ของคุณส่ง {path} มา แต่ไม่มีอยู่บนเครื่องแล้ว",
|
||||
"cw_files_goto": "ไปที่พาธ…",
|
||||
"cw_files_goto_bad": "พิมพ์พาธแบบเต็มที่ขึ้นต้นด้วย /",
|
||||
"cw_files_goto_failed": "ตรวจสอบพาธนั้นไม่ได้ ({code})",
|
||||
@@ -155,6 +157,9 @@
|
||||
"cw_files_select_invert": "กลับการเลือก",
|
||||
"cw_files_select_none": "ไม่เลือกเลย",
|
||||
"cw_files_selected": "เลือกแล้ว {n}",
|
||||
"cw_files_show_pc": "แสดงบน PC",
|
||||
"cw_files_show_pc_done": "ส่งไปยัง PC ของคุณแล้ว จะเปิดในระบบไฟล์ของ PS5Upload",
|
||||
"cw_files_show_pc_failed": "ไม่สามารถส่งไปยัง PC ได้",
|
||||
"cw_files_some_failed": "ล้มเหลว {n}: {reason}",
|
||||
"cw_files_sort_date": "เรียงตามวันที่",
|
||||
"cw_files_sort_name": "เรียงตามชื่อ",
|
||||
@@ -179,6 +184,7 @@
|
||||
"cw_files_unpin": "เลิกปักหมุด",
|
||||
"cw_files_unpin_done": "เลิกปักหมุด {name} แล้ว",
|
||||
"cw_files_up": "ขึ้น",
|
||||
"cw_files_uploading": "กำลังอัปโหลด",
|
||||
"cw_files_view": "ดู",
|
||||
"cw_help_close": "ปิด",
|
||||
"cw_help_controls": "การควบคุม",
|
||||
|
||||
@@ -99,6 +99,8 @@
|
||||
"cw_files_fixperm_body": "Bu klasördeki her şeyi PS5 için okunabilir ve çalıştırılabilir yapar (chmod 777). Bir oyun kopyalandıktan sonra başlamazsa kullanın.",
|
||||
"cw_files_fixperm_done": "İzinler düzeltildi.",
|
||||
"cw_files_free": "{free} boş",
|
||||
"cw_files_from_pc": "PC'nden açıldı.",
|
||||
"cw_files_from_pc_gone": "PC'n {path} gönderdi, ancak artık konsolda değil.",
|
||||
"cw_files_goto": "Yola git…",
|
||||
"cw_files_goto_bad": "/ ile başlayan tam bir yol yazın.",
|
||||
"cw_files_goto_failed": "Bu yol kontrol edilemedi ({code}).",
|
||||
@@ -155,6 +157,9 @@
|
||||
"cw_files_select_invert": "Tersine çevir",
|
||||
"cw_files_select_none": "Seçimi kaldır",
|
||||
"cw_files_selected": "{n} seçili",
|
||||
"cw_files_show_pc": "PC'de göster",
|
||||
"cw_files_show_pc_done": "PC'ne gönderildi. PS5Upload'ın Dosya Sistemi'nde açılır.",
|
||||
"cw_files_show_pc_failed": "PC'ye gönderilemedi.",
|
||||
"cw_files_some_failed": "{n} başarısız: {reason}",
|
||||
"cw_files_sort_date": "Tarihe göre sırala",
|
||||
"cw_files_sort_name": "Ada göre sırala",
|
||||
@@ -179,6 +184,7 @@
|
||||
"cw_files_unpin": "Sabitlemeyi kaldır",
|
||||
"cw_files_unpin_done": "{name} sabitlemesi kaldırıldı.",
|
||||
"cw_files_up": "Yukarı",
|
||||
"cw_files_uploading": "Yükleniyor",
|
||||
"cw_files_view": "Görüntüle",
|
||||
"cw_help_close": "Kapat",
|
||||
"cw_help_controls": "Kontroller",
|
||||
|
||||
@@ -99,6 +99,8 @@
|
||||
"cw_files_fixperm_body": "Làm cho mọi thứ trong thư mục này PS5 có thể đọc và chạy được (chmod 777). Dùng khi game không khởi động sau khi sao chép.",
|
||||
"cw_files_fixperm_done": "Đã sửa quyền.",
|
||||
"cw_files_free": "Còn trống {free}",
|
||||
"cw_files_from_pc": "Đã mở từ PC của bạn.",
|
||||
"cw_files_from_pc_gone": "PC của bạn đã gửi {path}, nhưng nó không còn trên máy nữa.",
|
||||
"cw_files_goto": "Đi tới đường dẫn…",
|
||||
"cw_files_goto_bad": "Nhập đường dẫn đầy đủ bắt đầu bằng /.",
|
||||
"cw_files_goto_failed": "Không kiểm tra được đường dẫn đó ({code}).",
|
||||
@@ -155,6 +157,9 @@
|
||||
"cw_files_select_invert": "Đảo chọn",
|
||||
"cw_files_select_none": "Bỏ chọn",
|
||||
"cw_files_selected": "Đã chọn {n}",
|
||||
"cw_files_show_pc": "Hiển thị trên PC",
|
||||
"cw_files_show_pc_done": "Đã gửi đến PC của bạn. Sẽ mở trong Hệ thống tệp của PS5Upload.",
|
||||
"cw_files_show_pc_failed": "Không thể gửi đến PC.",
|
||||
"cw_files_some_failed": "{n} thất bại: {reason}",
|
||||
"cw_files_sort_date": "Sắp xếp theo ngày",
|
||||
"cw_files_sort_name": "Sắp xếp theo tên",
|
||||
@@ -179,6 +184,7 @@
|
||||
"cw_files_unpin": "Bỏ ghim",
|
||||
"cw_files_unpin_done": "Đã bỏ ghim {name}.",
|
||||
"cw_files_up": "Lên",
|
||||
"cw_files_uploading": "Đang tải lên",
|
||||
"cw_files_view": "Xem",
|
||||
"cw_help_close": "Đóng",
|
||||
"cw_help_controls": "Điều khiển",
|
||||
|
||||
@@ -99,6 +99,8 @@
|
||||
"cw_files_fixperm_body": "让此文件夹中的所有内容可被 PS5 读取和运行(chmod 777)。复制后游戏无法启动时使用。",
|
||||
"cw_files_fixperm_done": "权限已修复。",
|
||||
"cw_files_free": "可用 {free}",
|
||||
"cw_files_from_pc": "已从你的电脑打开。",
|
||||
"cw_files_from_pc_gone": "你的电脑发送了 {path},但它已不在主机上。",
|
||||
"cw_files_goto": "转到路径…",
|
||||
"cw_files_goto_bad": "请输入以 / 开头的完整路径。",
|
||||
"cw_files_goto_failed": "无法检查该路径({code})。",
|
||||
@@ -155,6 +157,9 @@
|
||||
"cw_files_select_invert": "反选",
|
||||
"cw_files_select_none": "取消全选",
|
||||
"cw_files_selected": "已选 {n} 项",
|
||||
"cw_files_show_pc": "在电脑上显示",
|
||||
"cw_files_show_pc_done": "已发送到你的电脑,将在 PS5Upload 的文件系统中打开。",
|
||||
"cw_files_show_pc_failed": "无法发送到电脑。",
|
||||
"cw_files_some_failed": "{n} 项失败:{reason}",
|
||||
"cw_files_sort_date": "按日期排序",
|
||||
"cw_files_sort_name": "按名称排序",
|
||||
@@ -179,6 +184,7 @@
|
||||
"cw_files_unpin": "取消固定",
|
||||
"cw_files_unpin_done": "已取消固定 {name}。",
|
||||
"cw_files_up": "上一级",
|
||||
"cw_files_uploading": "正在上传",
|
||||
"cw_files_view": "查看",
|
||||
"cw_help_close": "关闭",
|
||||
"cw_help_controls": "按键说明",
|
||||
|
||||
@@ -99,6 +99,8 @@
|
||||
"cw_files_fixperm_body": "讓此資料夾中的所有內容可被 PS5 讀取和執行(chmod 777)。複製後遊戲無法啟動時使用。",
|
||||
"cw_files_fixperm_done": "權限已修正。",
|
||||
"cw_files_free": "可用 {free}",
|
||||
"cw_files_from_pc": "已從你的電腦開啟。",
|
||||
"cw_files_from_pc_gone": "你的電腦傳送了 {path},但它已不在主機上。",
|
||||
"cw_files_goto": "前往路徑…",
|
||||
"cw_files_goto_bad": "請輸入以 / 開頭的完整路徑。",
|
||||
"cw_files_goto_failed": "無法檢查該路徑({code})。",
|
||||
@@ -155,6 +157,9 @@
|
||||
"cw_files_select_invert": "反選",
|
||||
"cw_files_select_none": "取消全選",
|
||||
"cw_files_selected": "已選取 {n} 項",
|
||||
"cw_files_show_pc": "在電腦上顯示",
|
||||
"cw_files_show_pc_done": "已傳送到你的電腦,將在 PS5Upload 的檔案系統中開啟。",
|
||||
"cw_files_show_pc_failed": "無法傳送到電腦。",
|
||||
"cw_files_some_failed": "{n} 項失敗:{reason}",
|
||||
"cw_files_sort_date": "依日期排序",
|
||||
"cw_files_sort_name": "依名稱排序",
|
||||
@@ -179,6 +184,7 @@
|
||||
"cw_files_unpin": "取消釘選",
|
||||
"cw_files_unpin_done": "已取消釘選 {name}。",
|
||||
"cw_files_up": "上一層",
|
||||
"cw_files_uploading": "正在上傳",
|
||||
"cw_files_view": "檢視",
|
||||
"cw_help_close": "關閉",
|
||||
"cw_help_controls": "按鍵說明",
|
||||
|
||||
Reference in new issue
Block a user