mirror of
https://github.com/phantomptr/ps5upload.git
synced 2026-10-18 18:10:05 -05:00
console files: one-row toolbar, more rows on screen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RxCGb8Asp1A1AQYiv1kbgS
This commit is contained in:
1 parent
f7daeec308
commit
4fcb9e913a
46 files changed
+276
-130
No files matched your search
@@ -1213,3 +1213,91 @@ button {
|
||||
height: 1.2rem;
|
||||
margin-right: 0.7rem;
|
||||
}
|
||||
|
||||
/* ---- Files: one toolbar row, more rows on screen ---- */
|
||||
.cw-files-screen .cw-h1 {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cw-toolbar-one {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
.cw-toolbar-one .cw-pathbar {
|
||||
flex: 1;
|
||||
margin: 0 0.8rem 0 0;
|
||||
}
|
||||
.cw-tools {
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.cw-tool {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.6rem;
|
||||
height: 2.4rem;
|
||||
margin-left: 0.3rem;
|
||||
color: inherit;
|
||||
background: var(--color-surface-3);
|
||||
}
|
||||
.cw-tool svg {
|
||||
width: 1.3rem;
|
||||
height: 1.3rem;
|
||||
}
|
||||
.cw-tool[aria-pressed="true"] {
|
||||
background: var(--color-accent-soft);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
.cw-tool.is-pinned svg {
|
||||
fill: currentColor;
|
||||
}
|
||||
.cw-tool:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: default;
|
||||
}
|
||||
.cw-tool-label {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(100% + 0.25rem);
|
||||
right: 0;
|
||||
z-index: 5;
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: var(--radius-control);
|
||||
background: var(--color-surface-3);
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text);
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
}
|
||||
.cw-tool:focus .cw-tool-label {
|
||||
display: block;
|
||||
}
|
||||
.cw-files-screen .cw-status {
|
||||
margin: 0 0 0.4rem;
|
||||
}
|
||||
.cw-files-screen .cw-file {
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
.cw-files-screen .cw-file-main {
|
||||
padding: 0.3rem 0.9rem;
|
||||
}
|
||||
.cw-files-screen .cw-file .cw-icon-button {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
.cw-hint-tip {
|
||||
margin-left: auto;
|
||||
color: var(--color-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -277,8 +277,11 @@ describe("Files listing controls", () => {
|
||||
)}
|
||||
</>,
|
||||
);
|
||||
expect(html).toContain('data-action="sortview"');
|
||||
expect(html).toContain('data-action="select-all"');
|
||||
expect(html).not.toContain('data-action="sortview"'); // the selection bar replaces the right side
|
||||
const plain = renderToStaticMarkup(<>{FilesView(props({ onView: vi.fn(), onGoTo: vi.fn(), prefs: { favorites: [], recent: [], last: null }, onPin: vi.fn(), onUnpin: vi.fn() }))}</>);
|
||||
for (const a of ["pin", "goto", "mkdir", "select", "sortview", "filter"]) expect(plain).toContain(`data-action="${a}"`);
|
||||
expect(plain).toContain('data-tip="Sort and view"');
|
||||
expect(html).toContain("700 items · 1 selected (10 B)");
|
||||
expect(html).toContain('data-action="more-rows"');
|
||||
expect(html.match(/data-entry="/g)).toHaveLength(300);
|
||||
|
||||
+129
-129
@@ -2,6 +2,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
ArrowDownUp,
|
||||
CheckCheck,
|
||||
ChevronsDown,
|
||||
Ellipsis,
|
||||
Filter,
|
||||
FilterX,
|
||||
FolderPlus,
|
||||
@@ -9,8 +11,10 @@ import {
|
||||
ListX,
|
||||
RotateCw,
|
||||
Square,
|
||||
SquareCheck,
|
||||
Star,
|
||||
TextCursorInput,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
import { formatBytes } from "../../lib/format";
|
||||
import { ApiError, call } from "../api";
|
||||
@@ -21,10 +25,8 @@ import { useBackLevel } from "../backHistory";
|
||||
import { onNavAction, topModal } from "../nav";
|
||||
import type { SectionContext } from "../sections";
|
||||
import { t } from "../strings";
|
||||
import { ActionsButton } from "../ui/ActionsButton";
|
||||
import { ConfirmDialog } from "../ui/ConfirmDialog";
|
||||
import { anchorOf, focusLost, restoreFocus, type FocusAnchor } from "../ui/restoreFocus";
|
||||
import { SelectToggle } from "../ui/SelectToggle";
|
||||
import { ActionsSheet, type SheetAction } from "./ActionsSheet";
|
||||
import { DirList, ROW_WINDOW, viewKind } from "./DirList";
|
||||
import { canEject, DriveList, isImage, isSystemVolume, placeLabel } from "./DriveList";
|
||||
@@ -455,28 +457,42 @@ function TasksLine({ tasks }: { tasks: Job[] }) {
|
||||
);
|
||||
}
|
||||
|
||||
/** ☆ / ★: pin the folder shown (it then shows under Pinned in the drive list), or unpin it. */
|
||||
function PinButton({
|
||||
path,
|
||||
pinned,
|
||||
onPin,
|
||||
onUnpin,
|
||||
/**
|
||||
* One toolbar button: an icon, named by aria-label; its name shows next to it
|
||||
* while it has focus (CSS) and in the hint bar (data-tip).
|
||||
*/
|
||||
function ToolButton({
|
||||
icon: Icon,
|
||||
label,
|
||||
action,
|
||||
onClick,
|
||||
pressed,
|
||||
disabled,
|
||||
className,
|
||||
}: {
|
||||
path: string;
|
||||
pinned: boolean;
|
||||
onPin: (path: string) => void;
|
||||
onUnpin: (path: string) => void;
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
action: string;
|
||||
onClick: () => void;
|
||||
pressed?: boolean;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`cw-focusable cw-button cw-pin${pinned ? " is-pinned" : ""}`}
|
||||
data-action="pin"
|
||||
aria-pressed={pinned}
|
||||
onClick={() => (pinned ? onUnpin(path) : onPin(path))}
|
||||
className={`cw-focusable cw-tool${className ? ` ${className}` : ""}`}
|
||||
data-action={action}
|
||||
data-tip={label}
|
||||
aria-label={label}
|
||||
aria-pressed={pressed}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Star aria-hidden />
|
||||
<span>{t(pinned ? "cw_files_pinned_on" : "cw_files_pin")}</span>
|
||||
<Icon aria-hidden />
|
||||
<span className="cw-tool-label" aria-hidden>
|
||||
{label}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -498,6 +514,16 @@ export function FilesView(p: FilesViewProps) {
|
||||
const view = p.view ?? DEFAULT_VIEW;
|
||||
const capped = p.dir.more && p.dir.entries.length >= LOAD_ALL_CAP;
|
||||
const st = p.status;
|
||||
const pinned = p.path != null && p.prefs ? isPinned(p.prefs, p.path) : false;
|
||||
const loadAllButton = (
|
||||
<ToolButton
|
||||
icon={ChevronsDown}
|
||||
label={t("cw_files_load_all")}
|
||||
action="load-all"
|
||||
disabled={p.dir.loading || capped}
|
||||
onClick={() => p.onLoadAll?.()}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<div className="cw-files-screen">
|
||||
<h1 className="cw-h1">{t("cw_nav_files")}</h1>
|
||||
@@ -539,123 +565,97 @@ export function FilesView(p: FilesViewProps) {
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<PathBar key={p.path} path={p.path} volumes={p.volumes ?? []} onOpen={p.onOpen} onUp={p.onUp} />
|
||||
<div className="cw-toolbar">
|
||||
{p.prefs && p.onPin && p.onUnpin ? (
|
||||
<PinButton path={p.path} pinned={isPinned(p.prefs, p.path)} onPin={p.onPin} onUnpin={p.onUnpin} />
|
||||
) : null}
|
||||
{p.onGoTo ? (
|
||||
<button
|
||||
type="button"
|
||||
className="cw-focusable cw-button"
|
||||
data-action="goto"
|
||||
onClick={() => p.onAsk({ kind: "goto" })}
|
||||
>
|
||||
<TextCursorInput aria-hidden />
|
||||
<span>{t("cw_files_goto")}</span>
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="cw-focusable cw-button"
|
||||
data-action="mkdir"
|
||||
onClick={() => p.onAsk({ kind: "mkdir" })}
|
||||
>
|
||||
<FolderPlus aria-hidden />
|
||||
<span>{t("cw_files_new_folder")}</span>
|
||||
</button>
|
||||
<SelectToggle on={p.selecting} onToggle={p.onToggleSelecting} />
|
||||
{p.selecting && p.selected.length ? (
|
||||
<>
|
||||
<span className="cw-tag">{t("cw_files_selected", { n: p.selected.length })}</span>
|
||||
<ActionsButton onPress={() => p.onAsk({ kind: "sheet", names: p.selected })} />
|
||||
</>
|
||||
) : null}
|
||||
{vol ? (
|
||||
<span className="cw-dim cw-toolbar-free">{t("cw_files_free", { free: formatBytes(vol.free_bytes) })}</span>
|
||||
) : null}
|
||||
</div>
|
||||
{p.onView ? (
|
||||
<div className="cw-toolbar cw-listbar">
|
||||
<button
|
||||
type="button"
|
||||
className="cw-focusable cw-button"
|
||||
data-action="sortview"
|
||||
onClick={() => p.onAsk({ kind: "viewmenu" })}
|
||||
>
|
||||
<ArrowDownUp aria-hidden />
|
||||
<span>{t("cw_files_sort_view")}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="cw-focusable cw-button"
|
||||
data-action="filter"
|
||||
aria-pressed={view.filter !== ""}
|
||||
onClick={() => p.onAsk({ kind: "filter" })}
|
||||
>
|
||||
<Filter aria-hidden />
|
||||
<span>{view.filter ? t("cw_files_filter_active", { q: view.filter }) : t("cw_files_filter")}</span>
|
||||
</button>
|
||||
{view.filter ? (
|
||||
<button
|
||||
type="button"
|
||||
className="cw-focusable cw-icon-button"
|
||||
data-action="filter-clear"
|
||||
aria-label={t("cw_files_filter_clear")}
|
||||
title={t("cw_files_filter_clear")}
|
||||
onClick={() => p.onView?.({ filter: "" })}
|
||||
>
|
||||
<FilterX aria-hidden />
|
||||
</button>
|
||||
) : null}
|
||||
{p.dir.more && p.onLoadAll ? (
|
||||
<button
|
||||
type="button"
|
||||
className="cw-focusable cw-button"
|
||||
data-action="load-all"
|
||||
disabled={p.dir.loading || capped}
|
||||
onClick={p.onLoadAll}
|
||||
>
|
||||
<span>{p.dir.loading ? t("cw_home_loading") : t("cw_files_load_all")}</span>
|
||||
</button>
|
||||
) : null}
|
||||
<div className="cw-toolbar-one">
|
||||
<PathBar key={p.path} path={p.path} volumes={p.volumes ?? []} onOpen={p.onOpen} onUp={p.onUp} />
|
||||
<div className="cw-tools" role="toolbar" aria-label={t("cw_files_tools")}>
|
||||
{p.selecting ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="cw-focusable cw-button"
|
||||
data-action="select-all"
|
||||
onClick={p.onSelectAll}
|
||||
>
|
||||
<CheckCheck aria-hidden />
|
||||
<span>{t("cw_files_select_all")}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="cw-focusable cw-button"
|
||||
data-action="select-none"
|
||||
onClick={p.onSelectNone}
|
||||
>
|
||||
<ListX aria-hidden />
|
||||
<span>{t("cw_files_select_none")}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="cw-focusable cw-button"
|
||||
data-action="select-invert"
|
||||
onClick={p.onSelectInvert}
|
||||
>
|
||||
<ListChecks aria-hidden />
|
||||
<span>{t("cw_files_select_invert")}</span>
|
||||
</button>
|
||||
<ToolButton icon={CheckCheck} label={t("cw_files_select_all")} action="select-all" onClick={() => p.onSelectAll?.()} />
|
||||
<ToolButton icon={ListX} label={t("cw_files_select_none")} action="select-none" onClick={() => p.onSelectNone?.()} />
|
||||
<ToolButton icon={ListChecks} label={t("cw_files_select_invert")} action="select-invert" onClick={() => p.onSelectInvert?.()} />
|
||||
{p.selected.length ? (
|
||||
<ToolButton
|
||||
icon={Ellipsis}
|
||||
label={t("cw_actions")}
|
||||
action="selection-actions"
|
||||
onClick={() => p.onAsk({ kind: "sheet", names: p.selected })}
|
||||
/>
|
||||
) : null}
|
||||
{p.dir.more && p.onLoadAll ? loadAllButton : null}
|
||||
<ToolButton
|
||||
icon={SquareCheck}
|
||||
label={t("cw_select")}
|
||||
action="select"
|
||||
pressed
|
||||
onClick={p.onToggleSelecting}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
) : (
|
||||
<>
|
||||
{p.prefs && p.onPin && p.onUnpin ? (
|
||||
<ToolButton
|
||||
icon={Star}
|
||||
label={t(pinned ? "cw_files_pinned_on" : "cw_files_pin")}
|
||||
action="pin"
|
||||
pressed={pinned}
|
||||
className={pinned ? "is-pinned" : undefined}
|
||||
onClick={() => (pinned ? p.onUnpin?.(p.path as string) : p.onPin?.(p.path as string))}
|
||||
/>
|
||||
) : null}
|
||||
{p.onGoTo ? (
|
||||
<ToolButton
|
||||
icon={TextCursorInput}
|
||||
label={t("cw_files_goto")}
|
||||
action="goto"
|
||||
onClick={() => p.onAsk({ kind: "goto" })}
|
||||
/>
|
||||
) : null}
|
||||
<ToolButton
|
||||
icon={FolderPlus}
|
||||
label={t("cw_files_new_folder")}
|
||||
action="mkdir"
|
||||
onClick={() => p.onAsk({ kind: "mkdir" })}
|
||||
/>
|
||||
<ToolButton icon={SquareCheck} label={t("cw_select")} action="select" onClick={p.onToggleSelecting} />
|
||||
{p.onView ? (
|
||||
<>
|
||||
<ToolButton
|
||||
icon={ArrowDownUp}
|
||||
label={t("cw_files_sort_view")}
|
||||
action="sortview"
|
||||
onClick={() => p.onAsk({ kind: "viewmenu" })}
|
||||
/>
|
||||
<ToolButton
|
||||
icon={Filter}
|
||||
label={view.filter ? t("cw_files_filter_active", { q: view.filter }) : t("cw_files_filter")}
|
||||
action="filter"
|
||||
pressed={view.filter !== ""}
|
||||
onClick={() => p.onAsk({ kind: "filter" })}
|
||||
/>
|
||||
{view.filter ? (
|
||||
<ToolButton
|
||||
icon={FilterX}
|
||||
label={t("cw_files_filter_clear")}
|
||||
action="filter-clear"
|
||||
onClick={() => p.onView?.({ filter: "" })}
|
||||
/>
|
||||
) : null}
|
||||
{p.dir.more && p.onLoadAll ? loadAllButton : null}
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{st && p.dir.loaded ? (
|
||||
<p className="cw-dim cw-status" role="status" data-status="">
|
||||
{statusText(st)}
|
||||
{capped ? ` · ${t("cw_files_capped", { n: p.dir.entries.length })}` : ""}
|
||||
{[
|
||||
statusText(st),
|
||||
capped ? t("cw_files_capped", { n: p.dir.entries.length }) : null,
|
||||
vol ? t("cw_files_free", { free: formatBytes(vol.free_bytes) }) : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ")}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
"cw_files_status_size_dirs": "{size}، بدون احتساب المجلدات",
|
||||
"cw_files_tasks": "المهام الجارية: {n}",
|
||||
"cw_files_too_large": "كبير جدًا للمعاينة.",
|
||||
"cw_files_tools": "أدوات المجلد",
|
||||
"cw_files_truncated": "يُعرض أول 1 MiB فقط.",
|
||||
"cw_files_unmount": "إلغاء التركيب",
|
||||
"cw_files_unmount_body": "ستتوقف اللعبة التي تعمل من هذه الصورة، وستغيب ملفاتها حتى يُعاد تركيب الصورة.",
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
"cw_files_status_size_dirs": "{size}, ফোল্ডার গণনা হয়নি",
|
||||
"cw_files_tasks": "চলমান কাজ: {n}",
|
||||
"cw_files_too_large": "প্রিভিউ করার জন্য খুব বড়।",
|
||||
"cw_files_tools": "ফোল্ডারের টুল",
|
||||
"cw_files_truncated": "প্রথম 1 MiB দেখানো হচ্ছে।",
|
||||
"cw_files_unmount": "আনমাউন্ট করুন",
|
||||
"cw_files_unmount_body": "এই ইমেজ থেকে চলা গেম বন্ধ হয়ে যাবে, এবং ইমেজটি আবার মাউন্ট না হওয়া পর্যন্ত এর ফাইলগুলো থাকবে না।",
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
"cw_files_status_size_dirs": "{size}, Ordner nicht mitgezählt",
|
||||
"cw_files_tasks": "Laufende Aufgaben: {n}",
|
||||
"cw_files_too_large": "Zu groß für die Vorschau.",
|
||||
"cw_files_tools": "Ordnerwerkzeuge",
|
||||
"cw_files_truncated": "Die ersten 1 MiB werden angezeigt.",
|
||||
"cw_files_unmount": "Aushängen",
|
||||
"cw_files_unmount_body": "Ein Spiel, das von diesem Image läuft, wird beendet, und seine Dateien fehlen, bis das Image wieder eingebunden ist.",
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
"cw_files_status_size_dirs": "{size}, folders not counted",
|
||||
"cw_files_tasks": "Tasks running: {n}",
|
||||
"cw_files_too_large": "Too large to preview.",
|
||||
"cw_files_tools": "Folder tools",
|
||||
"cw_files_truncated": "Showing the first 1 MiB.",
|
||||
"cw_files_unmount": "Unmount",
|
||||
"cw_files_unmount_body": "A game running from this image will stop, and its files are gone until the image is mounted again.",
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
"cw_files_status_size_dirs": "{size}, sin contar carpetas",
|
||||
"cw_files_tasks": "Tareas en curso: {n}",
|
||||
"cw_files_too_large": "Demasiado grande para la vista previa.",
|
||||
"cw_files_tools": "Herramientas de la carpeta",
|
||||
"cw_files_truncated": "Se muestra el primer 1 MiB.",
|
||||
"cw_files_unmount": "Desmontar",
|
||||
"cw_files_unmount_body": "Un juego que se ejecute desde esta imagen se detendrá, y sus archivos no estarán disponibles hasta que se vuelva a montar la imagen.",
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
"cw_files_status_size_dirs": "{size}، پوشهها محاسبه نشدهاند",
|
||||
"cw_files_tasks": "کارهای در حال اجرا: {n}",
|
||||
"cw_files_too_large": "برای پیشنمایش بیش از حد بزرگ است.",
|
||||
"cw_files_tools": "ابزارهای پوشه",
|
||||
"cw_files_truncated": "فقط ۱ مبیبایت اول نمایش داده میشود.",
|
||||
"cw_files_unmount": "پیاده کردن",
|
||||
"cw_files_unmount_body": "بازیای که از این ایمیج اجرا میشود متوقف میشود و فایلهایش تا سوار شدن دوبارهٔ ایمیج در دسترس نیستند.",
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
"cw_files_status_size_dirs": "{size}, dossiers non comptés",
|
||||
"cw_files_tasks": "Tâches en cours : {n}",
|
||||
"cw_files_too_large": "Trop volumineux pour l'aperçu.",
|
||||
"cw_files_tools": "Outils du dossier",
|
||||
"cw_files_truncated": "Affichage du premier Mio.",
|
||||
"cw_files_unmount": "Démonter",
|
||||
"cw_files_unmount_body": "Un jeu lancé depuis cette image s'arrêtera, et ses fichiers seront absents jusqu'à ce que l'image soit de nouveau montée.",
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
"cw_files_status_size_dirs": "{size}, फ़ोल्डर गिने नहीं गए",
|
||||
"cw_files_tasks": "चल रहे कार्य: {n}",
|
||||
"cw_files_too_large": "पूर्वावलोकन के लिए बहुत बड़ा है।",
|
||||
"cw_files_tools": "फ़ोल्डर के टूल",
|
||||
"cw_files_truncated": "पहला 1 MiB दिखाया जा रहा है।",
|
||||
"cw_files_unmount": "अनमाउंट करें",
|
||||
"cw_files_unmount_body": "इस इमेज से चल रहा गेम रुक जाएगा, और इमेज दोबारा माउंट होने तक उसकी फ़ाइलें उपलब्ध नहीं रहेंगी।",
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
"cw_files_status_size_dirs": "{size}, a mappák nincsenek beleszámítva",
|
||||
"cw_files_tasks": "Futó feladatok: {n}",
|
||||
"cw_files_too_large": "Túl nagy az előnézethez.",
|
||||
"cw_files_tools": "Mappaeszközök",
|
||||
"cw_files_truncated": "Az első 1 MiB látható.",
|
||||
"cw_files_unmount": "Leválasztás",
|
||||
"cw_files_unmount_body": "Az erről a képfájlról futó játék leáll, és a fájljai addig nem érhetők el, amíg a képfájlt újra nem csatolják.",
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
"cw_files_status_size_dirs": "{size}, folder tidak dihitung",
|
||||
"cw_files_tasks": "Tugas berjalan: {n}",
|
||||
"cw_files_too_large": "Terlalu besar untuk pratinjau.",
|
||||
"cw_files_tools": "Alat folder",
|
||||
"cw_files_truncated": "Menampilkan 1 MiB pertama.",
|
||||
"cw_files_unmount": "Lepas",
|
||||
"cw_files_unmount_body": "Game yang berjalan dari image ini akan berhenti, dan filenya hilang sampai image dipasang lagi.",
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
"cw_files_status_size_dirs": "{size}, cartelle escluse",
|
||||
"cw_files_tasks": "Attività in corso: {n}",
|
||||
"cw_files_too_large": "Troppo grande per l'anteprima.",
|
||||
"cw_files_tools": "Strumenti della cartella",
|
||||
"cw_files_truncated": "Viene mostrato il primo MiB.",
|
||||
"cw_files_unmount": "Smonta",
|
||||
"cw_files_unmount_body": "Un gioco avviato da questa immagine si fermerà, e i suoi file non ci saranno finché l'immagine non verrà montata di nuovo.",
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
"cw_files_status_size_dirs": "{size}、フォルダは含まず",
|
||||
"cw_files_tasks": "実行中のタスク: {n}",
|
||||
"cw_files_too_large": "大きすぎてプレビューできません。",
|
||||
"cw_files_tools": "フォルダのツール",
|
||||
"cw_files_truncated": "先頭 1 MiB を表示しています。",
|
||||
"cw_files_unmount": "マウント解除",
|
||||
"cw_files_unmount_body": "このイメージから起動しているゲームは停止し、イメージを再びマウントするまでそのファイルは使えなくなります。",
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
"cw_files_status_size_dirs": "{size}, 폴더는 제외",
|
||||
"cw_files_tasks": "실행 중인 작업: {n}",
|
||||
"cw_files_too_large": "너무 커서 미리 볼 수 없습니다.",
|
||||
"cw_files_tools": "폴더 도구",
|
||||
"cw_files_truncated": "처음 1 MiB를 표시합니다.",
|
||||
"cw_files_unmount": "마운트 해제",
|
||||
"cw_files_unmount_body": "이 이미지에서 실행 중인 게임은 멈추고, 이미지를 다시 마운트할 때까지 그 파일을 쓸 수 없습니다.",
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
"cw_files_status_size_dirs": "{size}, foldery nie wliczone",
|
||||
"cw_files_tasks": "Trwające zadania: {n}",
|
||||
"cw_files_too_large": "Za duży, by pokazać podgląd.",
|
||||
"cw_files_tools": "Narzędzia folderu",
|
||||
"cw_files_truncated": "Pokazano pierwszy 1 MiB.",
|
||||
"cw_files_unmount": "Odmontuj",
|
||||
"cw_files_unmount_body": "Gra uruchomiona z tego obrazu zatrzyma się, a jej pliki znikną, dopóki obraz nie zostanie ponownie zamontowany.",
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
"cw_files_status_size_dirs": "{size}, pastas não contadas",
|
||||
"cw_files_tasks": "Tarefas em andamento: {n}",
|
||||
"cw_files_too_large": "Grande demais para visualizar.",
|
||||
"cw_files_tools": "Ferramentas da pasta",
|
||||
"cw_files_truncated": "Mostrando o primeiro 1 MiB.",
|
||||
"cw_files_unmount": "Desmontar",
|
||||
"cw_files_unmount_body": "Um jogo rodando a partir desta imagem vai parar, e os arquivos dele somem até a imagem ser montada de novo.",
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
"cw_files_status_size_dirs": "{size}, папки не учтены",
|
||||
"cw_files_tasks": "Выполняется задач: {n}",
|
||||
"cw_files_too_large": "Слишком большой для просмотра.",
|
||||
"cw_files_tools": "Инструменты папки",
|
||||
"cw_files_truncated": "Показан первый 1 МиБ.",
|
||||
"cw_files_unmount": "Отключить",
|
||||
"cw_files_unmount_body": "Игра, запущенная из этого образа, остановится, а её файлы будут недоступны, пока образ не подключат снова.",
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
"cw_files_status_size_dirs": "{size} ไม่นับโฟลเดอร์",
|
||||
"cw_files_tasks": "งานที่กำลังทำ: {n}",
|
||||
"cw_files_too_large": "ใหญ่เกินกว่าจะแสดงตัวอย่าง",
|
||||
"cw_files_tools": "เครื่องมือโฟลเดอร์",
|
||||
"cw_files_truncated": "แสดง 1 MiB แรก",
|
||||
"cw_files_unmount": "ยกเลิกการเมานต์",
|
||||
"cw_files_unmount_body": "เกมที่รันจากอิมเมจนี้จะหยุดทำงาน และไฟล์ของเกมจะหายไปจนกว่าจะเมานต์อิมเมจอีกครั้ง",
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
"cw_files_status_size_dirs": "{size}, klasörler sayılmadı",
|
||||
"cw_files_tasks": "Çalışan görevler: {n}",
|
||||
"cw_files_too_large": "Önizleme için çok büyük.",
|
||||
"cw_files_tools": "Klasör araçları",
|
||||
"cw_files_truncated": "İlk 1 MiB gösteriliyor.",
|
||||
"cw_files_unmount": "Bağlantıyı kes",
|
||||
"cw_files_unmount_body": "Bu imajdan çalışan bir oyun durur ve imaj yeniden bağlanana kadar dosyaları erişilemez olur.",
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
"cw_files_status_size_dirs": "{size}, chưa tính thư mục",
|
||||
"cw_files_tasks": "Tác vụ đang chạy: {n}",
|
||||
"cw_files_too_large": "Quá lớn để xem trước.",
|
||||
"cw_files_tools": "Công cụ thư mục",
|
||||
"cw_files_truncated": "Đang hiện 1 MiB đầu tiên.",
|
||||
"cw_files_unmount": "Tháo gắn",
|
||||
"cw_files_unmount_body": "Trò chơi đang chạy từ ảnh đĩa này sẽ dừng, và các tệp của nó sẽ không còn cho đến khi ảnh đĩa được gắn lại.",
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
"cw_files_status_size_dirs": "{size},不含文件夹",
|
||||
"cw_files_tasks": "运行中的任务:{n}",
|
||||
"cw_files_too_large": "文件太大,无法预览。",
|
||||
"cw_files_tools": "文件夹工具",
|
||||
"cw_files_truncated": "仅显示前 1 MiB。",
|
||||
"cw_files_unmount": "卸载",
|
||||
"cw_files_unmount_body": "从此镜像运行的游戏会停止,在镜像重新挂载之前其文件将不可用。",
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
"cw_files_status_size_dirs": "{size},不含資料夾",
|
||||
"cw_files_tasks": "執行中的工作:{n}",
|
||||
"cw_files_too_large": "檔案太大,無法預覽。",
|
||||
"cw_files_tools": "資料夾工具",
|
||||
"cw_files_truncated": "只顯示前 1 MiB。",
|
||||
"cw_files_unmount": "卸載",
|
||||
"cw_files_unmount_body": "從此映像檔執行的遊戲會停止,在映像檔重新掛載之前其檔案將無法使用。",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { InputMap } from "../nav";
|
||||
import { barHints, type ScreenId } from "../help/hints";
|
||||
import { t } from "../strings";
|
||||
@@ -23,6 +24,13 @@ export function Glyphs({ glyphs }: { glyphs: string[] }) {
|
||||
*/
|
||||
export function HintBar({ map, screen }: { map: InputMap; screen: ScreenId }) {
|
||||
const hints = barHints(map, screen);
|
||||
// The focused toolbar button's name (icon-only buttons carry it in data-tip).
|
||||
const [tip, setTip] = useState("");
|
||||
useEffect(() => {
|
||||
const onIn = (e: Event) => setTip((e.target as Element | null)?.closest?.("[data-tip]")?.getAttribute("data-tip") ?? "");
|
||||
document.addEventListener("focusin", onIn);
|
||||
return () => document.removeEventListener("focusin", onIn);
|
||||
}, []);
|
||||
if (!hints.length) return null;
|
||||
return (
|
||||
<footer className="cw-hints" aria-label={t("cw_hints_label")} data-screen={screen}>
|
||||
@@ -32,6 +40,11 @@ export function HintBar({ map, screen }: { map: InputMap; screen: ScreenId }) {
|
||||
{t(h.labelKey)}
|
||||
</span>
|
||||
))}
|
||||
{tip ? (
|
||||
<span className="cw-hint-tip" aria-hidden>
|
||||
{tip}
|
||||
</span>
|
||||
) : null}
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -4128,6 +4128,7 @@ c2c_test_deep_bad: "لم يصل الإرسال التجريبي بحجم 4 KiB:
|
||||
"cw_files_status_selected": "تم تحديد {n} ({size})",
|
||||
"cw_files_status_size_dirs": "{size}، بدون احتساب المجلدات",
|
||||
"cw_files_none_match": "لا شيء يطابق التصفية.",
|
||||
"cw_files_tools": "أدوات المجلد",
|
||||
"cw_files_selected": "تم تحديد {n}",
|
||||
"cw_files_items": "{n} عناصر",
|
||||
"cw_files_free": "{free} متاحة",
|
||||
|
||||
@@ -4128,6 +4128,7 @@ c2c_test_deep_bad: "4 KiB-এর পরীক্ষামূলক পাঠা
|
||||
"cw_files_status_selected": "{n}টি বাছাই করা ({size})",
|
||||
"cw_files_status_size_dirs": "{size}, ফোল্ডার গণনা হয়নি",
|
||||
"cw_files_none_match": "ফিল্টারের সাথে কিছু মেলেনি।",
|
||||
"cw_files_tools": "ফোল্ডারের টুল",
|
||||
"cw_files_selected": "{n}টি নির্বাচিত",
|
||||
"cw_files_items": "{n}টি আইটেম",
|
||||
"cw_files_free": "{free} খালি",
|
||||
|
||||
@@ -4136,6 +4136,7 @@ c2c_test_deep_bad: "Die 4-KiB-Testsendung ist nicht angekommen: {detail}",
|
||||
"cw_files_status_selected": "{n} ausgewählt ({size})",
|
||||
"cw_files_status_size_dirs": "{size}, Ordner nicht mitgezählt",
|
||||
"cw_files_none_match": "Nichts passt zum Filter.",
|
||||
"cw_files_tools": "Ordnerwerkzeuge",
|
||||
"cw_files_selected": "{n} ausgewählt",
|
||||
"cw_files_items": "{n} Elemente",
|
||||
"cw_files_free": "{free} frei",
|
||||
|
||||
@@ -4451,6 +4451,7 @@ c2c_test_deep_bad: "The 4 KiB test send did not arrive: {detail}",
|
||||
"cw_files_status_selected": "{n} selected ({size})",
|
||||
"cw_files_status_size_dirs": "{size}, folders not counted",
|
||||
"cw_files_none_match": "Nothing matches the filter.",
|
||||
"cw_files_tools": "Folder tools",
|
||||
"cw_files_selected": "{n} selected",
|
||||
"cw_files_items": "{n} items",
|
||||
"cw_files_free": "{free} free",
|
||||
|
||||
@@ -4078,6 +4078,7 @@ c2c_test_deep_bad: "El envío de prueba de 4 KiB no llegó: {detail}",
|
||||
"cw_files_status_selected": "{n} seleccionados ({size})",
|
||||
"cw_files_status_size_dirs": "{size}, sin contar carpetas",
|
||||
"cw_files_none_match": "Nada coincide con el filtro.",
|
||||
"cw_files_tools": "Herramientas de la carpeta",
|
||||
"cw_files_selected": "{n} seleccionados",
|
||||
"cw_files_items": "{n} elementos",
|
||||
"cw_files_free": "{free} libres",
|
||||
|
||||
@@ -4429,6 +4429,7 @@ c2c_test_deep_bad: "ارسال آزمایشی ۴ KiB نرسید: {detail}",
|
||||
"cw_files_status_selected": "{n} انتخاب شده ({size})",
|
||||
"cw_files_status_size_dirs": "{size}، پوشهها محاسبه نشدهاند",
|
||||
"cw_files_none_match": "هیچ موردی با فیلتر مطابقت ندارد.",
|
||||
"cw_files_tools": "ابزارهای پوشه",
|
||||
"cw_files_selected": "{n} انتخاب شده",
|
||||
"cw_files_items": "{n} مورد",
|
||||
"cw_files_free": "{free} آزاد",
|
||||
|
||||
@@ -4146,6 +4146,7 @@ c2c_test_deep_bad: "L’envoi test de 4 Kio n’est pas arrivé : {detail}",
|
||||
"cw_files_status_selected": "{n} sélectionnés ({size})",
|
||||
"cw_files_status_size_dirs": "{size}, dossiers non comptés",
|
||||
"cw_files_none_match": "Aucun résultat pour ce filtre.",
|
||||
"cw_files_tools": "Outils du dossier",
|
||||
"cw_files_selected": "{n} sélectionné(s)",
|
||||
"cw_files_items": "{n} éléments",
|
||||
"cw_files_free": "{free} libres",
|
||||
|
||||
@@ -4144,6 +4144,7 @@ c2c_test_deep_bad: "4 KiB का परीक्षण नहीं पहुँ
|
||||
"cw_files_status_selected": "{n} चुने गए ({size})",
|
||||
"cw_files_status_size_dirs": "{size}, फ़ोल्डर गिने नहीं गए",
|
||||
"cw_files_none_match": "फ़िल्टर से कुछ मेल नहीं खाता।",
|
||||
"cw_files_tools": "फ़ोल्डर के टूल",
|
||||
"cw_files_selected": "{n} चुने गए",
|
||||
"cw_files_items": "{n} आइटम",
|
||||
"cw_files_free": "{free} खाली",
|
||||
|
||||
@@ -4443,6 +4443,7 @@ c2c_test_deep_bad: "A 4 KiB-os tesztküldés nem érkezett meg: {detail}",
|
||||
"cw_files_status_selected": "{n} kijelölve ({size})",
|
||||
"cw_files_status_size_dirs": "{size}, a mappák nincsenek beleszámítva",
|
||||
"cw_files_none_match": "Nincs a szűrőnek megfelelő elem.",
|
||||
"cw_files_tools": "Mappaeszközök",
|
||||
"cw_files_selected": "{n} kijelölve",
|
||||
"cw_files_items": "{n} elem",
|
||||
"cw_files_free": "{free} szabad",
|
||||
|
||||
@@ -4137,6 +4137,7 @@ c2c_test_deep_bad: "Kiriman uji 4 KiB tidak tiba: {detail}",
|
||||
"cw_files_status_selected": "{n} dipilih ({size})",
|
||||
"cw_files_status_size_dirs": "{size}, folder tidak dihitung",
|
||||
"cw_files_none_match": "Tidak ada yang cocok dengan filter.",
|
||||
"cw_files_tools": "Alat folder",
|
||||
"cw_files_selected": "{n} dipilih",
|
||||
"cw_files_items": "{n} item",
|
||||
"cw_files_free": "{free} kosong",
|
||||
|
||||
@@ -4078,6 +4078,7 @@ c2c_test_deep_bad: "L’invio di prova di 4 KiB non è arrivato: {detail}",
|
||||
"cw_files_status_selected": "{n} selezionati ({size})",
|
||||
"cw_files_status_size_dirs": "{size}, cartelle escluse",
|
||||
"cw_files_none_match": "Nessun risultato per il filtro.",
|
||||
"cw_files_tools": "Strumenti della cartella",
|
||||
"cw_files_selected": "{n} selezionati",
|
||||
"cw_files_items": "{n} elementi",
|
||||
"cw_files_free": "{free} liberi",
|
||||
|
||||
@@ -4149,6 +4149,7 @@ c2c_test_deep_bad: "4 KiB のテスト送信が届きませんでした: {detail
|
||||
"cw_files_status_selected": "{n} 件選択 ({size})",
|
||||
"cw_files_status_size_dirs": "{size}、フォルダは含まず",
|
||||
"cw_files_none_match": "絞り込みに一致するものがありません。",
|
||||
"cw_files_tools": "フォルダのツール",
|
||||
"cw_files_selected": "{n} 件選択中",
|
||||
"cw_files_items": "{n} 件",
|
||||
"cw_files_free": "空き {free}",
|
||||
|
||||
@@ -4147,6 +4147,7 @@ c2c_test_deep_bad: "4 KiB 테스트 전송이 도착하지 않았습니다: {det
|
||||
"cw_files_status_selected": "{n}개 선택됨 ({size})",
|
||||
"cw_files_status_size_dirs": "{size}, 폴더는 제외",
|
||||
"cw_files_none_match": "필터와 일치하는 항목이 없습니다.",
|
||||
"cw_files_tools": "폴더 도구",
|
||||
"cw_files_selected": "{n}개 선택됨",
|
||||
"cw_files_items": "{n}개 항목",
|
||||
"cw_files_free": "{free} 남음",
|
||||
|
||||
@@ -4383,6 +4383,7 @@ c2c_test_deep_bad: "Testowa wysyłka 4 KiB nie dotarła: {detail}",
|
||||
"cw_files_status_selected": "Zaznaczono: {n} ({size})",
|
||||
"cw_files_status_size_dirs": "{size}, foldery nie wliczone",
|
||||
"cw_files_none_match": "Nic nie pasuje do filtra.",
|
||||
"cw_files_tools": "Narzędzia folderu",
|
||||
"cw_files_selected": "Zaznaczono: {n}",
|
||||
"cw_files_items": "Elementy: {n}",
|
||||
"cw_files_free": "Wolne: {free}",
|
||||
|
||||
@@ -4145,6 +4145,7 @@ c2c_test_deep_bad: "O envio de teste de 4 KiB não chegou: {detail}",
|
||||
"cw_files_status_selected": "{n} selecionados ({size})",
|
||||
"cw_files_status_size_dirs": "{size}, pastas não contadas",
|
||||
"cw_files_none_match": "Nada corresponde ao filtro.",
|
||||
"cw_files_tools": "Ferramentas da pasta",
|
||||
"cw_files_selected": "{n} selecionados",
|
||||
"cw_files_items": "{n} itens",
|
||||
"cw_files_free": "{free} livres",
|
||||
|
||||
@@ -4078,6 +4078,7 @@ c2c_test_deep_bad: "Тестовая отправка 4 КиБ не дошла:
|
||||
"cw_files_status_selected": "Выбрано: {n} ({size})",
|
||||
"cw_files_status_size_dirs": "{size}, папки не учтены",
|
||||
"cw_files_none_match": "Ничего не подходит под фильтр.",
|
||||
"cw_files_tools": "Инструменты папки",
|
||||
"cw_files_selected": "Выбрано: {n}",
|
||||
"cw_files_items": "Элементов: {n}",
|
||||
"cw_files_free": "Свободно: {free}",
|
||||
|
||||
@@ -4137,6 +4137,7 @@ c2c_test_deep_bad: "การส่งทดสอบ 4 KiB ไปไม่ถ
|
||||
"cw_files_status_selected": "เลือกแล้ว {n} ({size})",
|
||||
"cw_files_status_size_dirs": "{size} ไม่นับโฟลเดอร์",
|
||||
"cw_files_none_match": "ไม่มีรายการที่ตรงกับตัวกรอง",
|
||||
"cw_files_tools": "เครื่องมือโฟลเดอร์",
|
||||
"cw_files_selected": "เลือกแล้ว {n}",
|
||||
"cw_files_items": "{n} รายการ",
|
||||
"cw_files_free": "ว่าง {free}",
|
||||
|
||||
@@ -4135,6 +4135,7 @@ c2c_test_deep_bad: "4 KiB'lik test gönderimi ulaşmadı: {detail}",
|
||||
"cw_files_status_selected": "{n} seçili ({size})",
|
||||
"cw_files_status_size_dirs": "{size}, klasörler sayılmadı",
|
||||
"cw_files_none_match": "Filtreyle eşleşen yok.",
|
||||
"cw_files_tools": "Klasör araçları",
|
||||
"cw_files_selected": "{n} seçili",
|
||||
"cw_files_items": "{n} öğe",
|
||||
"cw_files_free": "{free} boş",
|
||||
|
||||
@@ -4130,6 +4130,7 @@ c2c_test_deep_bad: "Lượt gửi thử 4 KiB không tới: {detail}",
|
||||
"cw_files_status_selected": "Đã chọn {n} ({size})",
|
||||
"cw_files_status_size_dirs": "{size}, chưa tính thư mục",
|
||||
"cw_files_none_match": "Không có mục nào khớp bộ lọc.",
|
||||
"cw_files_tools": "Công cụ thư mục",
|
||||
"cw_files_selected": "Đã chọn {n}",
|
||||
"cw_files_items": "{n} mục",
|
||||
"cw_files_free": "Còn trống {free}",
|
||||
|
||||
@@ -4084,6 +4084,7 @@ c2c_test_deep_bad: "4 KiB 测试发送未到达:{detail}",
|
||||
"cw_files_status_selected": "已选 {n} 项({size})",
|
||||
"cw_files_status_size_dirs": "{size},不含文件夹",
|
||||
"cw_files_none_match": "没有符合筛选的项目。",
|
||||
"cw_files_tools": "文件夹工具",
|
||||
"cw_files_selected": "已选 {n} 项",
|
||||
"cw_files_items": "{n} 项",
|
||||
"cw_files_free": "可用 {free}",
|
||||
|
||||
@@ -4078,6 +4078,7 @@ c2c_test_deep_bad: "4 KiB 測試傳送未抵達:{detail}",
|
||||
"cw_files_status_selected": "已選 {n} 項({size})",
|
||||
"cw_files_status_size_dirs": "{size},不含資料夾",
|
||||
"cw_files_none_match": "沒有符合篩選的項目。",
|
||||
"cw_files_tools": "資料夾工具",
|
||||
"cw_files_selected": "已選取 {n} 項",
|
||||
"cw_files_items": "{n} 項",
|
||||
"cw_files_free": "可用 {free}",
|
||||
|
||||
Reference in new issue
Block a user