diff --git a/client/src/console/console.css b/client/src/console/console.css index 131cb95b..f60fed15 100644 --- a/client/src/console/console.css +++ b/client/src/console/console.css @@ -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; +} diff --git a/client/src/console/files/Files.test.tsx b/client/src/console/files/Files.test.tsx index 5e2d4e7c..f08967f6 100644 --- a/client/src/console/files/Files.test.tsx +++ b/client/src/console/files/Files.test.tsx @@ -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); diff --git a/client/src/console/files/Files.tsx b/client/src/console/files/Files.tsx index f2a06820..77d07dac 100644 --- a/client/src/console/files/Files.tsx +++ b/client/src/console/files/Files.tsx @@ -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 ( ); } @@ -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 = ( + p.onLoadAll?.()} + /> + ); return (

{t("cw_nav_files")}

@@ -539,123 +565,97 @@ export function FilesView(p: FilesViewProps) { /> ) : ( <> - -
- {p.prefs && p.onPin && p.onUnpin ? ( - - ) : null} - {p.onGoTo ? ( - - ) : null} - - - {p.selecting && p.selected.length ? ( - <> - {t("cw_files_selected", { n: p.selected.length })} - p.onAsk({ kind: "sheet", names: p.selected })} /> - - ) : null} - {vol ? ( - {t("cw_files_free", { free: formatBytes(vol.free_bytes) })} - ) : null} -
- {p.onView ? ( -
- - - {view.filter ? ( - - ) : null} - {p.dir.more && p.onLoadAll ? ( - - ) : null} +
+ +
{p.selecting ? ( <> - - - + p.onSelectAll?.()} /> + p.onSelectNone?.()} /> + p.onSelectInvert?.()} /> + {p.selected.length ? ( + p.onAsk({ kind: "sheet", names: p.selected })} + /> + ) : null} + {p.dir.more && p.onLoadAll ? loadAllButton : null} + - ) : null} + ) : ( + <> + {p.prefs && p.onPin && p.onUnpin ? ( + (pinned ? p.onUnpin?.(p.path as string) : p.onPin?.(p.path as string))} + /> + ) : null} + {p.onGoTo ? ( + p.onAsk({ kind: "goto" })} + /> + ) : null} + p.onAsk({ kind: "mkdir" })} + /> + + {p.onView ? ( + <> + p.onAsk({ kind: "viewmenu" })} + /> + p.onAsk({ kind: "filter" })} + /> + {view.filter ? ( + p.onView?.({ filter: "" })} + /> + ) : null} + {p.dir.more && p.onLoadAll ? loadAllButton : null} + + ) : null} + + )}
- ) : null} +
{st && p.dir.loaded ? (

- {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(" · ")}

) : null} diff --git a/client/src/console/locales/ar.json b/client/src/console/locales/ar.json index 75de6e56..214e94e4 100644 --- a/client/src/console/locales/ar.json +++ b/client/src/console/locales/ar.json @@ -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": "ستتوقف اللعبة التي تعمل من هذه الصورة، وستغيب ملفاتها حتى يُعاد تركيب الصورة.", diff --git a/client/src/console/locales/bn.json b/client/src/console/locales/bn.json index e22429dd..b10d0ee6 100644 --- a/client/src/console/locales/bn.json +++ b/client/src/console/locales/bn.json @@ -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": "এই ইমেজ থেকে চলা গেম বন্ধ হয়ে যাবে, এবং ইমেজটি আবার মাউন্ট না হওয়া পর্যন্ত এর ফাইলগুলো থাকবে না।", diff --git a/client/src/console/locales/de.json b/client/src/console/locales/de.json index 654293ea..a945854f 100644 --- a/client/src/console/locales/de.json +++ b/client/src/console/locales/de.json @@ -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.", diff --git a/client/src/console/locales/en.json b/client/src/console/locales/en.json index cf446840..4d6fbc16 100644 --- a/client/src/console/locales/en.json +++ b/client/src/console/locales/en.json @@ -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.", diff --git a/client/src/console/locales/es.json b/client/src/console/locales/es.json index a1ae56a3..d3ecc0fb 100644 --- a/client/src/console/locales/es.json +++ b/client/src/console/locales/es.json @@ -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.", diff --git a/client/src/console/locales/fa.json b/client/src/console/locales/fa.json index 981f67a0..7d255bf6 100644 --- a/client/src/console/locales/fa.json +++ b/client/src/console/locales/fa.json @@ -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": "بازی‌ای که از این ایمیج اجرا می‌شود متوقف می‌شود و فایل‌هایش تا سوار شدن دوبارهٔ ایمیج در دسترس نیستند.", diff --git a/client/src/console/locales/fr.json b/client/src/console/locales/fr.json index 792faa5b..09c421af 100644 --- a/client/src/console/locales/fr.json +++ b/client/src/console/locales/fr.json @@ -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.", diff --git a/client/src/console/locales/hi.json b/client/src/console/locales/hi.json index c26e60c0..c820a870 100644 --- a/client/src/console/locales/hi.json +++ b/client/src/console/locales/hi.json @@ -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": "इस इमेज से चल रहा गेम रुक जाएगा, और इमेज दोबारा माउंट होने तक उसकी फ़ाइलें उपलब्ध नहीं रहेंगी।", diff --git a/client/src/console/locales/hu.json b/client/src/console/locales/hu.json index 6e32a3b6..3fd7a2ac 100644 --- a/client/src/console/locales/hu.json +++ b/client/src/console/locales/hu.json @@ -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.", diff --git a/client/src/console/locales/id.json b/client/src/console/locales/id.json index 435e29aa..cec97fb6 100644 --- a/client/src/console/locales/id.json +++ b/client/src/console/locales/id.json @@ -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.", diff --git a/client/src/console/locales/it.json b/client/src/console/locales/it.json index 95536e5b..3d6f8876 100644 --- a/client/src/console/locales/it.json +++ b/client/src/console/locales/it.json @@ -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.", diff --git a/client/src/console/locales/ja.json b/client/src/console/locales/ja.json index 864fda8a..9ebf6210 100644 --- a/client/src/console/locales/ja.json +++ b/client/src/console/locales/ja.json @@ -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": "このイメージから起動しているゲームは停止し、イメージを再びマウントするまでそのファイルは使えなくなります。", diff --git a/client/src/console/locales/ko.json b/client/src/console/locales/ko.json index de2f1041..ca08e22d 100644 --- a/client/src/console/locales/ko.json +++ b/client/src/console/locales/ko.json @@ -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": "이 이미지에서 실행 중인 게임은 멈추고, 이미지를 다시 마운트할 때까지 그 파일을 쓸 수 없습니다.", diff --git a/client/src/console/locales/pl.json b/client/src/console/locales/pl.json index ef92552b..e6c992c6 100644 --- a/client/src/console/locales/pl.json +++ b/client/src/console/locales/pl.json @@ -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.", diff --git a/client/src/console/locales/pt-BR.json b/client/src/console/locales/pt-BR.json index 7fdb7bee..9ac62f67 100644 --- a/client/src/console/locales/pt-BR.json +++ b/client/src/console/locales/pt-BR.json @@ -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.", diff --git a/client/src/console/locales/ru.json b/client/src/console/locales/ru.json index fa42541e..c1411834 100644 --- a/client/src/console/locales/ru.json +++ b/client/src/console/locales/ru.json @@ -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": "Игра, запущенная из этого образа, остановится, а её файлы будут недоступны, пока образ не подключат снова.", diff --git a/client/src/console/locales/th.json b/client/src/console/locales/th.json index eaa2f6d3..3da8abd4 100644 --- a/client/src/console/locales/th.json +++ b/client/src/console/locales/th.json @@ -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": "เกมที่รันจากอิมเมจนี้จะหยุดทำงาน และไฟล์ของเกมจะหายไปจนกว่าจะเมานต์อิมเมจอีกครั้ง", diff --git a/client/src/console/locales/tr.json b/client/src/console/locales/tr.json index 6d228881..2ec57e34 100644 --- a/client/src/console/locales/tr.json +++ b/client/src/console/locales/tr.json @@ -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.", diff --git a/client/src/console/locales/vi.json b/client/src/console/locales/vi.json index fcc0ef1c..2f6266ed 100644 --- a/client/src/console/locales/vi.json +++ b/client/src/console/locales/vi.json @@ -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.", diff --git a/client/src/console/locales/zh-CN.json b/client/src/console/locales/zh-CN.json index a4864ec0..8eab5e6b 100644 --- a/client/src/console/locales/zh-CN.json +++ b/client/src/console/locales/zh-CN.json @@ -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": "从此镜像运行的游戏会停止,在镜像重新挂载之前其文件将不可用。", diff --git a/client/src/console/locales/zh-TW.json b/client/src/console/locales/zh-TW.json index ecb2d52c..73a982c4 100644 --- a/client/src/console/locales/zh-TW.json +++ b/client/src/console/locales/zh-TW.json @@ -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": "從此映像檔執行的遊戲會停止,在映像檔重新掛載之前其檔案將無法使用。", diff --git a/client/src/console/shell/HintBar.tsx b/client/src/console/shell/HintBar.tsx index 78a4ecf5..6b1eb8dc 100644 --- a/client/src/console/shell/HintBar.tsx +++ b/client/src/console/shell/HintBar.tsx @@ -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 ( ); } diff --git a/client/src/i18n/locales/ar.ts b/client/src/i18n/locales/ar.ts index 6231c899..1c4a433a 100644 --- a/client/src/i18n/locales/ar.ts +++ b/client/src/i18n/locales/ar.ts @@ -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} متاحة", diff --git a/client/src/i18n/locales/bn.ts b/client/src/i18n/locales/bn.ts index a4883b71..f06badf0 100644 --- a/client/src/i18n/locales/bn.ts +++ b/client/src/i18n/locales/bn.ts @@ -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} খালি", diff --git a/client/src/i18n/locales/de.ts b/client/src/i18n/locales/de.ts index ce19e5df..b04979d8 100644 --- a/client/src/i18n/locales/de.ts +++ b/client/src/i18n/locales/de.ts @@ -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", diff --git a/client/src/i18n/locales/en.ts b/client/src/i18n/locales/en.ts index 5e630b90..351d99d9 100644 --- a/client/src/i18n/locales/en.ts +++ b/client/src/i18n/locales/en.ts @@ -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", diff --git a/client/src/i18n/locales/es.ts b/client/src/i18n/locales/es.ts index 4ace55f6..f7a7fe4d 100644 --- a/client/src/i18n/locales/es.ts +++ b/client/src/i18n/locales/es.ts @@ -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", diff --git a/client/src/i18n/locales/fa.ts b/client/src/i18n/locales/fa.ts index 9d0fba63..70f4adb7 100644 --- a/client/src/i18n/locales/fa.ts +++ b/client/src/i18n/locales/fa.ts @@ -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} آزاد", diff --git a/client/src/i18n/locales/fr.ts b/client/src/i18n/locales/fr.ts index 24a94ba8..44c25bb9 100644 --- a/client/src/i18n/locales/fr.ts +++ b/client/src/i18n/locales/fr.ts @@ -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", diff --git a/client/src/i18n/locales/hi.ts b/client/src/i18n/locales/hi.ts index cb356287..6d312a39 100644 --- a/client/src/i18n/locales/hi.ts +++ b/client/src/i18n/locales/hi.ts @@ -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} खाली", diff --git a/client/src/i18n/locales/hu.ts b/client/src/i18n/locales/hu.ts index 48936ec5..c48007f0 100644 --- a/client/src/i18n/locales/hu.ts +++ b/client/src/i18n/locales/hu.ts @@ -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", diff --git a/client/src/i18n/locales/id.ts b/client/src/i18n/locales/id.ts index b97300f0..7eee5c1d 100644 --- a/client/src/i18n/locales/id.ts +++ b/client/src/i18n/locales/id.ts @@ -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", diff --git a/client/src/i18n/locales/it.ts b/client/src/i18n/locales/it.ts index aad51965..da7c4289 100644 --- a/client/src/i18n/locales/it.ts +++ b/client/src/i18n/locales/it.ts @@ -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", diff --git a/client/src/i18n/locales/ja.ts b/client/src/i18n/locales/ja.ts index 875c6ca1..68fd12ce 100644 --- a/client/src/i18n/locales/ja.ts +++ b/client/src/i18n/locales/ja.ts @@ -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}", diff --git a/client/src/i18n/locales/ko.ts b/client/src/i18n/locales/ko.ts index 116a3602..c832964a 100644 --- a/client/src/i18n/locales/ko.ts +++ b/client/src/i18n/locales/ko.ts @@ -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} 남음", diff --git a/client/src/i18n/locales/pl.ts b/client/src/i18n/locales/pl.ts index 50e345c9..13a205fb 100644 --- a/client/src/i18n/locales/pl.ts +++ b/client/src/i18n/locales/pl.ts @@ -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}", diff --git a/client/src/i18n/locales/pt-BR.ts b/client/src/i18n/locales/pt-BR.ts index 984ca06a..51d9bf0a 100644 --- a/client/src/i18n/locales/pt-BR.ts +++ b/client/src/i18n/locales/pt-BR.ts @@ -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", diff --git a/client/src/i18n/locales/ru.ts b/client/src/i18n/locales/ru.ts index e6f1f1db..7a31b959 100644 --- a/client/src/i18n/locales/ru.ts +++ b/client/src/i18n/locales/ru.ts @@ -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}", diff --git a/client/src/i18n/locales/th.ts b/client/src/i18n/locales/th.ts index d6551b9f..0ee37321 100644 --- a/client/src/i18n/locales/th.ts +++ b/client/src/i18n/locales/th.ts @@ -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}", diff --git a/client/src/i18n/locales/tr.ts b/client/src/i18n/locales/tr.ts index a76ed085..bf577336 100644 --- a/client/src/i18n/locales/tr.ts +++ b/client/src/i18n/locales/tr.ts @@ -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ş", diff --git a/client/src/i18n/locales/vi.ts b/client/src/i18n/locales/vi.ts index 0049e5db..5a2dcb32 100644 --- a/client/src/i18n/locales/vi.ts +++ b/client/src/i18n/locales/vi.ts @@ -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}", diff --git a/client/src/i18n/locales/zh-CN.ts b/client/src/i18n/locales/zh-CN.ts index 30391240..c909a5fc 100644 --- a/client/src/i18n/locales/zh-CN.ts +++ b/client/src/i18n/locales/zh-CN.ts @@ -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}", diff --git a/client/src/i18n/locales/zh-TW.ts b/client/src/i18n/locales/zh-TW.ts index f43facae..bc178ff3 100644 --- a/client/src/i18n/locales/zh-TW.ts +++ b/client/src/i18n/locales/zh-TW.ts @@ -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}",