WIP X-plore 비교 5-1 ⑥ 파일 탐색기 단축키 바꾸기 (관리자 지시 10/21): 환경 설정 > 파일 탐색기 [파일 탐색기 단축키] (키보드가 있는 PC 에서만) - 동작 19개마다 지금 키 · [바꾸기] (다음에 누른 키, Ctrl · Shift · Alt 함께) · [처음 값으로] · [단축키 없음] · [모두 처음 값으로]. 처음 키는 83 의 스타일별 키 그대로 (바꾼 것만 설정 explorerKeyOverrides 에), 바꾼 키는 모든 스타일에 쓰이고 다른 동작과 겹치면 그 동작은 그 키를 잃음 (창에서 먼저 알림), Esc 는 바꿀 수 없음, Rsync 화면은 늘 같은 키 · 시험 (처음 키 = 예전 키 · 바꾸기 · 겹침 · 저장 꼴 · 탐색기에서 바꾼 키 동작 · 설정 화면) (검토 대기)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xplore_p3b
RyuJongJin 2026-10-21 03:33:43 +09:00
parent 7dfbc400d1
commit b5434683f4
9 changed files with 537 additions and 43 deletions

View File

@ -65,6 +65,7 @@
"(받는 쪽이 더 새것 - 건너뜀)": "(receiving side is newer - skipped)",
"(비어 있음)": "(empty)",
"(설명 없음)": "(no description)",
"(없음)": "(none)",
"(지워짐)": "(deleted)",
"(폴더 · 안의 {0}개를 지우고 같은 이름의 파일로)": "(folder and the {0} items inside are deleted and replaced by the file of the same name)",
"(현재 방식은 옵션 없음)": "(no options for the built-in method)",
@ -201,6 +202,7 @@
"Edge 에서 가져옴": "Imported from Edge",
"Edge 엔진으로 바꾸기": "Switch to the Edge engine",
"Edge: Windows 에 들어 있어 따로 설치하지 않습니다. 여기서 YouTube 에 로그인하면 다운로드에도 그 로그인이 쓰입니다.\nChrome: 고르면 약 {0}MB 를 내려받고 다시 시작한 뒤 쓸 수 있습니다 (Google 로그인은 막힐 수 있음).{1}{2}": "Edge: part of Windows, nothing to install. If you sign in to YouTube here, downloads use that sign-in too.\nChrome: if chosen, about {0}MB is downloaded and it can be used after a restart (Google sign-in may be blocked).{1}{2}",
"Esc 는 선택 · 빠른 필터를 닫는 키라 쓸 수 없습니다": "Esc closes selection and the quick filter, so it cannot be used",
"FFmpeg · yt-dlp · aria2 · Deno 가 없으면 내려받아 설치합니다": "Downloads and installs FFmpeg · yt-dlp · aria2 · Deno if missing",
"FFmpeg 준비됨: {0}{1}": "FFmpeg ready: {0}{1}",
"FTP 서버 \"{0}\" 을(를) 지울까요?": "Delete FTP server \"{0}\"?",
@ -498,6 +500,7 @@
"{0} (기본): {1}": "{0} (default): {1}",
"{0} (동영상 {1})": "{0} (videos: {1})",
"{0} - {1} (약 {2}MB)": "{0} - {1} (about {2}MB)",
"{0} - 단축키": "{0} - shortcut",
"{0} / {1} 읽음": "{0} / {1} read",
"{0} / {1} 확인": "{0} / {1} checked",
"{0} / {1}장": "{0} / {1} images",
@ -993,6 +996,7 @@
"다른 창에서 열기": "Open in other pane",
"다른 창으로 끌어 놓았을 때. 어느 쪽이든 복사 · 이동 확인 창 (같은 이름 등) 은 그대로 뜹니다": "When you drop onto the other pane. Either way, the copy/move confirmation (same names etc.) still appears",
"다른 창으로 복사": "Copy to other pane",
"다른 창으로 옮겨 가기": "Switch to the other pane",
"다른 창으로 이동": "Move to other pane",
"다른 폴더": "Other folder",
"다른 폴더로 이동": "Move to another folder",
@ -1050,6 +1054,7 @@
"단계": "Steps",
"단어 첫 글자만 크게 (Abc)": "Capitalize words (Abc)",
"단축키 - Total Commander: F5 복사 · F6 이동 · F7 새 폴더 · F8 지우기 · Insert/Space 표시 · Ctrl+R 새로 고침. 그 밖: Ctrl+C · X · V (복사 · 잘라내기 · 붙여넣기) · Ctrl+A 모두 표시 · F5 새로 고침": "Shortcuts - Total Commander: F5 copy · F6 move · F7 new folder · F8 delete · Insert/Space mark · Ctrl+R refresh. Others: Ctrl+C · X · V (copy · cut · paste) · Ctrl+A mark all · F5 refresh",
"단축키 없음": "No shortcut",
"단축키 해제": "Unregister hotkey",
"닫기": "Close",
"닫힌 문서입니다": "The document is closed",
@ -1262,10 +1267,12 @@
"모두 작게 (abc)": "lowercase (abc)",
"모두 종료": "Quit all",
"모두 지우기": "Clear all",
"모두 처음 값으로": "Reset all",
"모두 초기화": "Reset all",
"모두 취소 ({0}개)": "Cancel all ({0})",
"모두 취소 (대기 {0}개 포함)": "Cancel all (including {0} waiting)",
"모두 크게 (ABC)": "UPPERCASE (ABC)",
"모두 표시": "Mark all",
"모두 풀기": "Extract all",
"모든 작업 취소": "Cancel all jobs",
"목": "Thu",
@ -1329,6 +1336,7 @@
"바꾸면 바로 저장됩니다": "Saved automatically",
"바꾸지 못했습니다: {0}": "Could not change it: {0}",
"바꾼 PDF 를 저장하지 못했습니다 (저장 공간을 확인해 주세요)": "Couldn't save the converted PDF (check storage space)",
"바꾼 것 {0}개": "{0} changed",
"바꾼 것이 없습니다 (원본 그대로)": "Nothing changed (original as is)",
"바꿀 글": "Replace with",
"바꿀 글 (비우면 지움)": "Replace with (empty = remove)",
@ -1543,6 +1551,7 @@
"빈 폴더입니다": "This folder is empty",
"빠르게": "Fast",
"빠른 디코더 (TAESD)": "Fast decoder (TAESD)",
"빠른 보기": "Quick look",
"빠른 필터": "Quick filter",
"빼고 싶은 것 (선택)": "What to avoid (optional)",
"사라지는 것": "What is lost",
@ -1703,6 +1712,7 @@
"쉬는 중": "Idle",
"스웨덴어": "Swedish",
"스타일": "Style",
"스타일대로 (바꾼 것 없음)": "As the style (nothing changed)",
"스페인어": "Spanish",
"시간 지정 (cron)": "Scheduled (cron)",
"시간 형식 오류 {0}곳": "{0} time format errors",
@ -1765,6 +1775,7 @@
"쓸 수 없는 글자 (:)": "Invalid character (:)",
"쓸 수 없는 이름": "Invalid name",
"쓸 수 없는 이름입니다": "This name cannot be used",
"쓸 키를 누르세요 (Ctrl · Shift · Alt 와 함께 눌러도 됩니다)": "Press the key to use (you can combine it with Ctrl, Shift or Alt)",
"아랍어": "Arabic",
"아래 갤러리 줄 보이기": "Show the gallery strip",
"아래 갤러리 줄 숨기기": "Hide the gallery strip",
@ -1897,6 +1908,7 @@
"열어 본 때: {0}": "Accessed: {0}",
"영구 삭제": "Delete permanently",
"영구 삭제 ({0}개, 되돌릴 수 없음)": "Delete permanently ({0}, cannot be undone)",
"영구 삭제 (휴지통을 거치지 않음)": "Delete permanently (skip the trash)",
"영구 삭제하면 되살릴 수 없습니다.": "Permanently deleted items can't be restored.",
"영상": "Video",
"영상 {0}": "Video {0}",
@ -2198,6 +2210,7 @@
"이름을 바꿔 풂 {0}개": "{0} extracted with new names",
"이름을 찾을 수 없습니다: {0} (DNS {1})": "Name not found: {0} (DNS {1})",
"이름이 너무 김 (255자 넘음)": "Name too long (over 255 characters)",
"이미 \"{0}\" 에 쓰는 키입니다 - 저장하면 그 동작의 이 키는 없어집니다": "This key is already used for \"{0}\" - if you save, that action loses this key",
"이미 그리는 중입니다": "Already drawing",
"이미 동영상 목록에 있습니다 ({0}개).": "Already in the video list ({0}).",
"이미 만든 MKV 가 있습니다 ({0}개)": "Some MKVs already exist ({0})",
@ -2498,6 +2511,7 @@
"지금 보고 있는 동영상의 {0} (여러 개는 목록에서 체크)": "{0} for the video you are viewing (check several in the list)",
"지금 설정 그대로": "Keep current settings",
"지금 설치": "Install now",
"지금 스타일 ({0}) 의 키입니다. 바꾼 키는 모든 스타일에 쓰이고, 다른 동작과 겹치면 그 동작의 키는 없어집니다. Esc 는 바꿀 수 없습니다": "Keys of the current style ({0}). Changed keys apply to all styles; if one clashes with another action, that action loses the key. Esc cannot be changed",
"지금 실행 중입니다. 멈추고 lsync (실시간 동기화) 로 옮길까요? 남은 것은 lsync 가 이어서 맞춥니다.": "It is running now. Stop it and move to lsync (live sync)? lsync will sync the rest.",
"지금 쓰는 버전: v{0}": "Current version: v{0}",
"지금 쓰는 버전: v{0} → 새 버전: v{1}": "Current version: v{0} → new version: v{1}",
@ -2592,6 +2606,7 @@
"채우기 (가운데 잘라냄)": "Fill (crop the center)",
"처리 장치": "Processor",
"처음 · 🏠 주소 (기본: Tor 연결 확인 페이지)": "Start / 🏠 address (default: the Tor connection check page)",
"처음 값으로": "Reset",
"처음 보는 서버 인증서입니다": "This server certificate is new",
"처음 보는 서버 인증서입니다 (자체 서명). 지문을 확인한 뒤 믿을지 정하세요": "This server certificate is new (self-signed). Check the fingerprint, then decide whether to trust it",
"처음 사용 시 내려받기 {0}": "Downloads {0} on first use",
@ -2724,6 +2739,7 @@
"파일 탐색기 (지금 여기)": "File explorer (you are here)",
"파일 탐색기 가운데에 [모니터링] 버튼: 복사 · 이동을 기억해 진행 · 대상 디스크 용량 · 옵션을 보여 주고, 옵션을 고쳐 다시 실행 · 재개하거나 lsync 로 옮깁니다 (복사 · rsync 와 lsync 를 따로).": "[Monitor] button in the middle of the file explorer: remembers copies · moves and shows progress · target disk space · options; change options to run again · resume, or move to lsync (copy · rsync and lsync shown separately).",
"파일 탐색기 누르기: 한 번 누르면 선택 → 한 번 누르면 바로 실행. 되돌리기: 환경 설정 > 파일 탐색기 > 누르기": "File explorer tap: single tap selects → single tap opens. To revert: Settings > File explorer > Tap",
"파일 탐색기 단축키": "File explorer shortcuts",
"파일 탐색기 두 창 배치: 화면 모양 따라 → 좌우. 되돌리기: 파일 탐색기의 [좌우 ⇆ / 위아래 ⇅] 버튼 또는 창 배치": "File explorer two-pane layout: follow screen shape → side by side. To revert: the [Columns ⇆ / Rows ⇅] button in the file explorer, or Pane layout",
"파일 탐색기 목록의 이름 · 크기 글자만. 앱 화면 크기 · 기기 글자 크기와 곱해집니다": "Only the name and size text in the file explorer list. Multiplied by the app screen size and the device text size",
"파일 탐색기 목록의 줄 높이. 좁게는 손가락으로 누르기 어려울 수 있습니다": "Row height in the file explorer list. Compact rows may be hard to tap with a finger",
@ -2808,10 +2824,12 @@
"폴더만 lsync 로 옮길 수 있습니다.": "Only folders can be moved to lsync.",
"폴더와 안의 것 전체 (기본)": "The folder and everything inside (default)",
"폴란드어": "Polish",
"표시": "Mark",
"표시 (여러 개 고르기)": "Mark (select several)",
"표시 이름": "Display name",
"표시 이름은 바로 고칠 수 있습니다. 동영상 줄을 세 번 누르면 첫 번째 버튼의 폴더로 옮깁니다": "You can edit display names right here. Pressing a video row three times moves it to the first button's folder",
"표시 지우기": "Clear marks",
"표시하고 다음 줄": "Mark and move down",
"표준": "Standard",
"표준 입력 (-) 은 쓸 수 없습니다": "Standard input (-) cannot be used",
"푸는 중": "Extracting",

View File

@ -65,6 +65,7 @@
"(받는 쪽이 더 새것 - 건너뜀)": "(受け取る側が新しい - スキップ)",
"(비어 있음)": "(空)",
"(설명 없음)": "(説明なし)",
"(없음)": "(なし)",
"(지워짐)": "(削除)",
"(폴더 · 안의 {0}개를 지우고 같은 이름의 파일로)": "(フォルダーと中の {0} 件を削除して同名のファイルに)",
"(현재 방식은 옵션 없음)": "(現在の方式にはオプションなし)",
@ -201,6 +202,7 @@
"Edge 에서 가져옴": "Edge から取り込み",
"Edge 엔진으로 바꾸기": "Edge エンジンに切り替える",
"Edge: Windows 에 들어 있어 따로 설치하지 않습니다. 여기서 YouTube 에 로그인하면 다운로드에도 그 로그인이 쓰입니다.\nChrome: 고르면 약 {0}MB 를 내려받고 다시 시작한 뒤 쓸 수 있습니다 (Google 로그인은 막힐 수 있음).{1}{2}": "Edge: Windows に含まれているので別途インストール不要です。ここで YouTube にログインすると、ダウンロードにもそのログインが使われます。\nChrome: 選ぶと約 {0}MB をダウンロードし、再起動後に使えます(Google ログインはブロックされることがあります)。{1}{2}",
"Esc 는 선택 · 빠른 필터를 닫는 키라 쓸 수 없습니다": "Esc は選択・クイックフィルターを閉じるキーのため使えません",
"FFmpeg · yt-dlp · aria2 · Deno 가 없으면 내려받아 설치합니다": "FFmpeg・yt-dlp・aria2・Deno がなければダウンロードしてインストールします",
"FFmpeg 준비됨: {0}{1}": "FFmpeg 準備完了: {0}{1}",
"FTP 서버 \"{0}\" 을(를) 지울까요?": "FTP サーバー「{0}」を削除しますか?",
@ -498,6 +500,7 @@
"{0} (기본): {1}": "{0}(既定): {1}",
"{0} (동영상 {1})": "{0} (動画 {1})",
"{0} - {1} (약 {2}MB)": "{0} - {1}(約 {2}MB)",
"{0} - 단축키": "{0} - ショートカット",
"{0} / {1} 읽음": "{0} / {1} 読み込み済み",
"{0} / {1} 확인": "{0} / {1} 確認",
"{0} / {1}장": "{0} / {1} 枚",
@ -993,6 +996,7 @@
"다른 창에서 열기": "もう一方のペインで開く",
"다른 창으로 끌어 놓았을 때. 어느 쪽이든 복사 · 이동 확인 창 (같은 이름 등) 은 그대로 뜹니다": "もう一方のウィンドウにドロップしたとき。どちらでもコピー・移動の確認 (同じ名前など) は表示されます",
"다른 창으로 복사": "もう一方のペインへコピー",
"다른 창으로 옮겨 가기": "もう一方のペインへ移動",
"다른 창으로 이동": "もう一方のペインへ移動",
"다른 폴더": "別のフォルダー",
"다른 폴더로 이동": "他のフォルダーに移動",
@ -1050,6 +1054,7 @@
"단계": "ステップ",
"단어 첫 글자만 크게 (Abc)": "単語の先頭だけ大文字 (Abc)",
"단축키 - Total Commander: F5 복사 · F6 이동 · F7 새 폴더 · F8 지우기 · Insert/Space 표시 · Ctrl+R 새로 고침. 그 밖: Ctrl+C · X · V (복사 · 잘라내기 · 붙여넣기) · Ctrl+A 모두 표시 · F5 새로 고침": "ショートカット - Total Commander: F5 コピー · F6 移動 · F7 新しいフォルダー · F8 削除 · Insert/Space マーク · Ctrl+R 更新。その他: Ctrl+C · X · V (コピー · 切り取り · 貼り付け) · Ctrl+A すべてマーク · F5 更新",
"단축키 없음": "ショートカットなし",
"단축키 해제": "ショートカット解除",
"닫기": "閉じる",
"닫힌 문서입니다": "文書は閉じられています",
@ -1262,10 +1267,12 @@
"모두 작게 (abc)": "すべて小文字 (abc)",
"모두 종료": "すべて終了",
"모두 지우기": "すべて削除",
"모두 처음 값으로": "すべて既定に戻す",
"모두 초기화": "すべてリセット",
"모두 취소 ({0}개)": "すべてキャンセル ({0} 件)",
"모두 취소 (대기 {0}개 포함)": "すべてキャンセル (待機 {0} 件を含む)",
"모두 크게 (ABC)": "すべて大文字 (ABC)",
"모두 표시": "すべて選択",
"모두 풀기": "すべて展開",
"모든 작업 취소": "すべての作業をキャンセル",
"목": "木",
@ -1329,6 +1336,7 @@
"바꾸면 바로 저장됩니다": "変更するとすぐ保存されます",
"바꾸지 못했습니다: {0}": "変更できませんでした: {0}",
"바꾼 PDF 를 저장하지 못했습니다 (저장 공간을 확인해 주세요)": "変換した PDF を保存できませんでした (空き容量を確認してください)",
"바꾼 것 {0}개": "{0} 件変更",
"바꾼 것이 없습니다 (원본 그대로)": "変更なし(元のまま)",
"바꿀 글": "置き換える文字",
"바꿀 글 (비우면 지움)": "置き換える文字 (空なら削除)",
@ -1543,6 +1551,7 @@
"빈 폴더입니다": "空のフォルダーです",
"빠르게": "速く",
"빠른 디코더 (TAESD)": "高速デコーダー (TAESD)",
"빠른 보기": "クイックルック",
"빠른 필터": "クイックフィルター",
"빼고 싶은 것 (선택)": "避けたいもの (任意)",
"사라지는 것": "消えるもの",
@ -1703,6 +1712,7 @@
"쉬는 중": "休止中",
"스웨덴어": "スウェーデン語",
"스타일": "スタイル",
"스타일대로 (바꾼 것 없음)": "スタイルどおり (変更なし)",
"스페인어": "スペイン語",
"시간 지정 (cron)": "時間指定 (cron)",
"시간 형식 오류 {0}곳": "時間形式エラー {0}か所",
@ -1765,6 +1775,7 @@
"쓸 수 없는 글자 (:)": "使えない文字 (:)",
"쓸 수 없는 이름": "使えない名前",
"쓸 수 없는 이름입니다": "使用できない名前です",
"쓸 키를 누르세요 (Ctrl · Shift · Alt 와 함께 눌러도 됩니다)": "使うキーを押してください (Ctrl・Shift・Alt と組み合わせても構いません)",
"아랍어": "アラビア語",
"아래 갤러리 줄 보이기": "下のギャラリー列を表示",
"아래 갤러리 줄 숨기기": "下のギャラリー列を隠す",
@ -1897,6 +1908,7 @@
"열어 본 때: {0}": "アクセス日時: {0}",
"영구 삭제": "完全に削除",
"영구 삭제 ({0}개, 되돌릴 수 없음)": "完全に削除 ({0} 件、元に戻せません)",
"영구 삭제 (휴지통을 거치지 않음)": "完全に削除 (ごみ箱を経由しない)",
"영구 삭제하면 되살릴 수 없습니다.": "完全に削除すると元に戻せません。",
"영상": "映像",
"영상 {0}": "映像 {0}",
@ -2198,6 +2210,7 @@
"이름을 바꿔 풂 {0}개": "名前を変えて展開 {0} 件",
"이름을 찾을 수 없습니다: {0} (DNS {1})": "名前が見つかりません: {0} (DNS {1})",
"이름이 너무 김 (255자 넘음)": "名前が長すぎます (255 文字超)",
"이미 \"{0}\" 에 쓰는 키입니다 - 저장하면 그 동작의 이 키는 없어집니다": "このキーはすでに「{0}」に使われています - 保存するとその操作のこのキーはなくなります",
"이미 그리는 중입니다": "すでに描画中です",
"이미 동영상 목록에 있습니다 ({0}개).": "既に動画リストにあります({0}個)。",
"이미 만든 MKV 가 있습니다 ({0}개)": "作成済みの MKV があります ({0}件)",
@ -2498,6 +2511,7 @@
"지금 보고 있는 동영상의 {0} (여러 개는 목록에서 체크)": "今見ている動画の{0}(複数はリストでチェック)",
"지금 설정 그대로": "今の設定のまま",
"지금 설치": "今すぐインストール",
"지금 스타일 ({0}) 의 키입니다. 바꾼 키는 모든 스타일에 쓰이고, 다른 동작과 겹치면 그 동작의 키는 없어집니다. Esc 는 바꿀 수 없습니다": "現在のスタイル ({0}) のキーです。変更したキーはすべてのスタイルに使われ、他の操作と重なるとその操作のキーはなくなります。Esc は変更できません",
"지금 실행 중입니다. 멈추고 lsync (실시간 동기화) 로 옮길까요? 남은 것은 lsync 가 이어서 맞춥니다.": "実行中です。止めて lsync (リアルタイム同期) へ移しますか? 残りは lsync が続けて同期します。",
"지금 쓰는 버전: v{0}": "使用中のバージョン: v{0}",
"지금 쓰는 버전: v{0} → 새 버전: v{1}": "現在のバージョン: v{0} → 新しいバージョン: v{1}",
@ -2592,6 +2606,7 @@
"채우기 (가운데 잘라냄)": "フィル(中央を切り抜き)",
"처리 장치": "処理装置",
"처음 · 🏠 주소 (기본: Tor 연결 확인 페이지)": "最初・🏠 のアドレス (既定: Tor 接続確認ページ)",
"처음 값으로": "既定に戻す",
"처음 보는 서버 인증서입니다": "初めて見るサーバー証明書です",
"처음 보는 서버 인증서입니다 (자체 서명). 지문을 확인한 뒤 믿을지 정하세요": "初めて見るサーバー証明書です (自己署名)。指紋を確認してから信頼するか決めてください",
"처음 사용 시 내려받기 {0}": "初回使用時にダウンロード {0}",
@ -2724,6 +2739,7 @@
"파일 탐색기 (지금 여기)": "ファイルエクスプローラー (現在地)",
"파일 탐색기 가운데에 [모니터링] 버튼: 복사 · 이동을 기억해 진행 · 대상 디스크 용량 · 옵션을 보여 주고, 옵션을 고쳐 다시 실행 · 재개하거나 lsync 로 옮깁니다 (복사 · rsync 와 lsync 를 따로).": "ファイルエクスプローラー中央に [モニタリング] ボタン: コピー · 移動を記憶して進捗 · 対象ディスク容量 · オプションを表示し、オプションを変えて再実行 · 再開、または lsync へ移動します (コピー · rsync と lsync は別々)。",
"파일 탐색기 누르기: 한 번 누르면 선택 → 한 번 누르면 바로 실행. 되돌리기: 환경 설정 > 파일 탐색기 > 누르기": "ファイルエクスプローラーのタップ: 1 回で選択 → 1 回ですぐ開く。元に戻す: 環境設定 > ファイルエクスプローラー > タップ",
"파일 탐색기 단축키": "ファイルエクスプローラーのショートカット",
"파일 탐색기 두 창 배치: 화면 모양 따라 → 좌우. 되돌리기: 파일 탐색기의 [좌우 ⇆ / 위아래 ⇅] 버튼 또는 창 배치": "ファイルエクスプローラーの 2 画面配置: 画面の形に合わせる → 左右。元に戻す: ファイルエクスプローラーの [左右 ⇆ / 上下 ⇅] ボタンまたは画面配置",
"파일 탐색기 목록의 이름 · 크기 글자만. 앱 화면 크기 · 기기 글자 크기와 곱해집니다": "ファイルエクスプローラー一覧の名前・サイズの文字のみ。アプリの画面サイズ・端末の文字サイズと掛け合わされます",
"파일 탐색기 목록의 줄 높이. 좁게는 손가락으로 누르기 어려울 수 있습니다": "ファイルエクスプローラー一覧の行の高さ。狭くすると指でタップしにくい場合があります",
@ -2808,10 +2824,12 @@
"폴더만 lsync 로 옮길 수 있습니다.": "lsync へ移せるのはフォルダーだけです。",
"폴더와 안의 것 전체 (기본)": "フォルダーと中身すべて (既定)",
"폴란드어": "ポーランド語",
"표시": "選択",
"표시 (여러 개 고르기)": "マーク (複数選択)",
"표시 이름": "表示名",
"표시 이름은 바로 고칠 수 있습니다. 동영상 줄을 세 번 누르면 첫 번째 버튼의 폴더로 옮깁니다": "表示名はその場で変更できます。動画の行を 3回押すと最初のボタンのフォルダーに移動します",
"표시 지우기": "マークを解除",
"표시하고 다음 줄": "選択して次の行へ",
"표준": "標準",
"표준 입력 (-) 은 쓸 수 없습니다": "標準入力 (-) は使えません",
"푸는 중": "展開中",

View File

@ -65,6 +65,7 @@
"(받는 쪽이 더 새것 - 건너뜀)": "(接收端较新 - 已跳过)",
"(비어 있음)": "(空)",
"(설명 없음)": "(无说明)",
"(없음)": "(无)",
"(지워짐)": "(将删除)",
"(폴더 · 안의 {0}개를 지우고 같은 이름의 파일로)": "(删除文件夹及其中 {0} 项,换成同名文件)",
"(현재 방식은 옵션 없음)": "(当前方式没有选项)",
@ -201,6 +202,7 @@
"Edge 에서 가져옴": "从 Edge 导入",
"Edge 엔진으로 바꾸기": "切换到 Edge 引擎",
"Edge: Windows 에 들어 있어 따로 설치하지 않습니다. 여기서 YouTube 에 로그인하면 다운로드에도 그 로그인이 쓰입니다.\nChrome: 고르면 약 {0}MB 를 내려받고 다시 시작한 뒤 쓸 수 있습니다 (Google 로그인은 막힐 수 있음).{1}{2}": "Edge: Windows 自带,无需另外安装。在此登录 YouTube 后,下载也会使用该登录。\nChrome: 选择后会下载约 {0}MB,重新启动后可用(Google 登录可能被阻止)。{1}{2}",
"Esc 는 선택 · 빠른 필터를 닫는 키라 쓸 수 없습니다": "Esc 用于关闭选择和快速筛选,不能使用",
"FFmpeg · yt-dlp · aria2 · Deno 가 없으면 내려받아 설치합니다": "如缺少 FFmpeg · yt-dlp · aria2 · Deno,将下载并安装",
"FFmpeg 준비됨: {0}{1}": "FFmpeg 已就绪: {0}{1}",
"FTP 서버 \"{0}\" 을(를) 지울까요?": "删除 FTP 服务器\"{0}\"?",
@ -498,6 +500,7 @@
"{0} (기본): {1}": "{0}(默认): {1}",
"{0} (동영상 {1})": "{0}(视频 {1})",
"{0} - {1} (약 {2}MB)": "{0} - {1}(约 {2}MB)",
"{0} - 단축키": "{0} - 快捷键",
"{0} / {1} 읽음": "已读取 {0} / {1}",
"{0} / {1} 확인": "已检查 {0} / {1}",
"{0} / {1}장": "{0} / {1} 张",
@ -993,6 +996,7 @@
"다른 창에서 열기": "在另一窗格中打开",
"다른 창으로 끌어 놓았을 때. 어느 쪽이든 복사 · 이동 확인 창 (같은 이름 등) 은 그대로 뜹니다": "拖放到另一个窗格时。无论哪种,都会显示复制/移动确认 (同名等)",
"다른 창으로 복사": "复制到另一窗格",
"다른 창으로 옮겨 가기": "切换到另一窗格",
"다른 창으로 이동": "移动到另一窗格",
"다른 폴더": "其他文件夹",
"다른 폴더로 이동": "移动到其他文件夹",
@ -1050,6 +1054,7 @@
"단계": "步数",
"단어 첫 글자만 크게 (Abc)": "单词首字母大写 (Abc)",
"단축키 - Total Commander: F5 복사 · F6 이동 · F7 새 폴더 · F8 지우기 · Insert/Space 표시 · Ctrl+R 새로 고침. 그 밖: Ctrl+C · X · V (복사 · 잘라내기 · 붙여넣기) · Ctrl+A 모두 표시 · F5 새로 고침": "快捷键 - Total Commander: F5 复制 · F6 移动 · F7 新建文件夹 · F8 删除 · Insert/Space 标记 · Ctrl+R 刷新。其他: Ctrl+C · X · V (复制 · 剪切 · 粘贴) · Ctrl+A 全部标记 · F5 刷新",
"단축키 없음": "无快捷键",
"단축키 해제": "注销快捷键",
"닫기": "关闭",
"닫힌 문서입니다": "文档已关闭",
@ -1262,10 +1267,12 @@
"모두 작게 (abc)": "全部小写 (abc)",
"모두 종료": "全部退出",
"모두 지우기": "全部清除",
"모두 처음 값으로": "全部恢复默认",
"모두 초기화": "全部重置",
"모두 취소 ({0}개)": "全部取消({0} 个)",
"모두 취소 (대기 {0}개 포함)": "全部取消 (包括等待中的 {0} 个)",
"모두 크게 (ABC)": "全部大写 (ABC)",
"모두 표시": "全部标记",
"모두 풀기": "全部解压",
"모든 작업 취소": "取消所有任务",
"목": "四",
@ -1329,6 +1336,7 @@
"바꾸면 바로 저장됩니다": "更改后立即保存",
"바꾸지 못했습니다: {0}": "无法更改: {0}",
"바꾼 PDF 를 저장하지 못했습니다 (저장 공간을 확인해 주세요)": "无法保存转换后的 PDF (请检查存储空间)",
"바꾼 것 {0}개": "已更改 {0} 个",
"바꾼 것이 없습니다 (원본 그대로)": "未做更改(保持原样)",
"바꿀 글": "替换为",
"바꿀 글 (비우면 지움)": "替换为 (留空则删除)",
@ -1543,6 +1551,7 @@
"빈 폴더입니다": "文件夹为空",
"빠르게": "快速",
"빠른 디코더 (TAESD)": "快速解码器 (TAESD)",
"빠른 보기": "快速查看",
"빠른 필터": "快速筛选",
"빼고 싶은 것 (선택)": "要排除的内容 (可选)",
"사라지는 것": "丢失的内容",
@ -1703,6 +1712,7 @@
"쉬는 중": "空闲",
"스웨덴어": "瑞典语",
"스타일": "样式",
"스타일대로 (바꾼 것 없음)": "按样式 (未更改)",
"스페인어": "西班牙语",
"시간 지정 (cron)": "指定时间 (cron)",
"시간 형식 오류 {0}곳": "{0} 处时间格式错误",
@ -1765,6 +1775,7 @@
"쓸 수 없는 글자 (:)": "无效字符 (:)",
"쓸 수 없는 이름": "无效名称",
"쓸 수 없는 이름입니다": "不能使用此名称",
"쓸 키를 누르세요 (Ctrl · Shift · Alt 와 함께 눌러도 됩니다)": "请按下要使用的键 (可与 Ctrl、Shift、Alt 组合)",
"아랍어": "阿拉伯语",
"아래 갤러리 줄 보이기": "显示底部图库条",
"아래 갤러리 줄 숨기기": "隐藏底部图库条",
@ -1897,6 +1908,7 @@
"열어 본 때: {0}": "访问时间: {0}",
"영구 삭제": "永久删除",
"영구 삭제 ({0}개, 되돌릴 수 없음)": "永久删除 ({0} 个,无法撤销)",
"영구 삭제 (휴지통을 거치지 않음)": "永久删除 (不经过回收站)",
"영구 삭제하면 되살릴 수 없습니다.": "永久删除后无法还原。",
"영상": "视频",
"영상 {0}": "视频 {0}",
@ -2198,6 +2210,7 @@
"이름을 바꿔 풂 {0}개": "{0} 个以新名称解压",
"이름을 찾을 수 없습니다: {0} (DNS {1})": "找不到名称: {0} (DNS {1})",
"이름이 너무 김 (255자 넘음)": "名称过长 (超过 255 个字符)",
"이미 \"{0}\" 에 쓰는 키입니다 - 저장하면 그 동작의 이 키는 없어집니다": "此键已用于“{0}” - 保存后该操作将失去此键",
"이미 그리는 중입니다": "已在绘制中",
"이미 동영상 목록에 있습니다 ({0}개).": "已在视频列表中({0} 个)。",
"이미 만든 MKV 가 있습니다 ({0}개)": "已有制作好的 MKV({0} 个)",
@ -2498,6 +2511,7 @@
"지금 보고 있는 동영상의 {0} (여러 개는 목록에서 체크)": "当前查看视频的{0}(多个请在列表中勾选)",
"지금 설정 그대로": "保持当前设置",
"지금 설치": "立即安装",
"지금 스타일 ({0}) 의 키입니다. 바꾼 키는 모든 스타일에 쓰이고, 다른 동작과 겹치면 그 동작의 키는 없어집니다. Esc 는 바꿀 수 없습니다": "当前样式 ({0}) 的按键。更改的键适用于所有样式,与其他操作冲突时该操作将失去此键。Esc 不能更改",
"지금 실행 중입니다. 멈추고 lsync (실시간 동기화) 로 옮길까요? 남은 것은 lsync 가 이어서 맞춥니다.": "正在运行。停止并移到 lsync (实时同步) 吗? 剩余部分由 lsync 继续同步。",
"지금 쓰는 버전: v{0}": "当前版本: v{0}",
"지금 쓰는 버전: v{0} → 새 버전: v{1}": "当前版本: v{0} → 新版本: v{1}",
@ -2592,6 +2606,7 @@
"채우기 (가운데 잘라냄)": "填充(裁切中间)",
"처리 장치": "处理器",
"처음 · 🏠 주소 (기본: Tor 연결 확인 페이지)": "起始 · 🏠 地址 (默认: Tor 连接检查页面)",
"처음 값으로": "恢复默认",
"처음 보는 서버 인증서입니다": "这是首次见到的服务器证书",
"처음 보는 서버 인증서입니다 (자체 서명). 지문을 확인한 뒤 믿을지 정하세요": "这是首次见到的服务器证书 (自签名)。请确认指纹后决定是否信任",
"처음 사용 시 내려받기 {0}": "首次使用时下载 {0}",
@ -2724,6 +2739,7 @@
"파일 탐색기 (지금 여기)": "文件浏览器 (当前位置)",
"파일 탐색기 가운데에 [모니터링] 버튼: 복사 · 이동을 기억해 진행 · 대상 디스크 용량 · 옵션을 보여 주고, 옵션을 고쳐 다시 실행 · 재개하거나 lsync 로 옮깁니다 (복사 · rsync 와 lsync 를 따로).": "文件浏览器中间的 [监控] 按钮: 记住复制 · 移动并显示进度 · 目标磁盘容量 · 选项,可修改选项后重新运行 · 继续,或移到 lsync (复制 · rsync 与 lsync 分开显示)。",
"파일 탐색기 누르기: 한 번 누르면 선택 → 한 번 누르면 바로 실행. 되돌리기: 환경 설정 > 파일 탐색기 > 누르기": "文件浏览器点击: 单击选择 → 单击直接打开。恢复: 设置 > 文件浏览器 > 点击",
"파일 탐색기 단축키": "文件管理器快捷键",
"파일 탐색기 두 창 배치: 화면 모양 따라 → 좌우. 되돌리기: 파일 탐색기의 [좌우 ⇆ / 위아래 ⇅] 버튼 또는 창 배치": "文件浏览器双窗格布局: 跟随屏幕形状 → 左右。恢复: 文件浏览器中的 [左右 ⇆ / 上下 ⇅] 按钮或窗格布局",
"파일 탐색기 목록의 이름 · 크기 글자만. 앱 화면 크기 · 기기 글자 크기와 곱해집니다": "仅文件管理器列表中的名称和大小文字。会与应用界面大小及设备文字大小相乘",
"파일 탐색기 목록의 줄 높이. 좁게는 손가락으로 누르기 어려울 수 있습니다": "文件管理器列表的行高。紧凑时可能难以用手指点按",
@ -2808,10 +2824,12 @@
"폴더만 lsync 로 옮길 수 있습니다.": "只有文件夹可以移到 lsync。",
"폴더와 안의 것 전체 (기본)": "文件夹及其所有内容 (默认)",
"폴란드어": "波兰语",
"표시": "标记",
"표시 (여러 개 고르기)": "标记 (多选)",
"표시 이름": "显示名称",
"표시 이름은 바로 고칠 수 있습니다. 동영상 줄을 세 번 누르면 첫 번째 버튼의 폴더로 옮깁니다": "显示名称可直接修改。连续点击视频行三次会移到第一个按钮的文件夹",
"표시 지우기": "清除标记",
"표시하고 다음 줄": "标记并移到下一行",
"표준": "标准",
"표준 입력 (-) 은 쓸 수 없습니다": "不能使用标准输入 (-)",
"푸는 중": "正在解压",

View File

@ -481,6 +481,10 @@ class AppSettings {
int explorerRemoteThumbCacheMb = 200;
static const remoteThumbCacheChoices = [50, 100, 200, 500, 1000];
/// X-plore 비교 5-1 ⑥ (관리자 지시 10/21): 파일 탐색기 단축키 중 사용자가 바꾼 것만 (동작 → 키, '' = 그 동작은 단축키 없음).
/// 비어 있으면 (처음) 스타일대로 (83) - ui/explorer_keys.dart
Map<String, String> explorerKeyOverrides = {};
/// 84: 폴더 → "기준|desc" (예: "size|1"). 오래된 것부터 지워 300개까지
Map<String, String> explorerFolderSorts = {};
@ -881,6 +885,7 @@ class AppSettings {
'explorerSortByKind': explorerSortByKind,
'explorerRemoteThumbs': explorerRemoteThumbs,
'explorerRemoteThumbCacheMb': explorerRemoteThumbCacheMb,
'explorerKeyOverrides': explorerKeyOverrides,
'explorerFolderSorts': explorerFolderSorts,
'explorerShowHidden': explorerShowHidden,
'explorerHiddenItems': explorerHiddenItems,
@ -1125,6 +1130,9 @@ class AppSettings {
..explorerSortPerFolder = j['explorerSortPerFolder'] == true
..explorerSortByKind = j['explorerSortByKind'] == true
..explorerRemoteThumbs = const {'off', 'wifi', 'always'}.contains(j['explorerRemoteThumbs']) ? j['explorerRemoteThumbs'] as String : 'wifi'
..explorerKeyOverrides = j['explorerKeyOverrides'] is Map
? {for (final e in (j['explorerKeyOverrides'] as Map).entries) if (e.key is String && e.value is String) e.key as String: e.value as String}
: {}
..explorerRemoteThumbCacheMb = j['explorerRemoteThumbCacheMb'] is num
? (j['explorerRemoteThumbCacheMb'] as num).round().clamp(10, 10000)
: 200

139
lib/ui/explorer_keys.dart Normal file
View File

@ -0,0 +1,139 @@
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import '../l10n/tr.dart';
/// X-plore 비교 5-1 ⑥ (관리자 지시 10/21): 파일 탐색기 단축키 바꾸기.
/// 동작마다 처음 키는 스타일대로 (83 - Total Commander / 그 밖) 이고, 사용자가 바꾼 것만 설정 (explorerKeyOverrides: 동작 → 키) 에 둔다.
/// 바꾼 키가 다른 동작의 키와 같으면 바꾼 쪽이 이기고 그 동작은 그 키를 잃는다 (설정 화면이 먼저 알린다). Esc 는 바꾸지 않는다.
const explorerKeyActions = <String>[
'copy', 'move', 'newFolder', 'delete', 'deletePermanent', 'rename', 'refresh', 'quickLook', //
'clipCopy', 'clipCut', 'paste', 'selectAll', 'mark', 'markNext', 'filter', 'up', 'back', 'forward', 'switchPane',
];
/// 동작 이름 (화면 글)
String explorerKeyLabel(String id) => switch (id) {
'copy' => tr('다른 창으로 복사'),
'move' => tr('다른 창으로 이동'),
'newFolder' => tr('새 폴더'),
'delete' => tr('삭제'),
'deletePermanent' => tr('영구 삭제 (휴지통을 거치지 않음)'),
'rename' => tr('이름 변경'),
'refresh' => tr('새로 고침'),
'quickLook' => tr('빠른 보기'),
'clipCopy' => tr('복사 (붙여넣기로)'),
'clipCut' => tr('이동 (붙여넣기로)'),
'paste' => tr('붙여넣기'),
'selectAll' => tr('모두 표시'),
'mark' => tr('표시'),
'markNext' => tr('표시하고 다음 줄'),
'filter' => tr('빠른 필터'),
'up' => tr('상위 폴더'),
'back' => tr('뒤로'),
'forward' => tr('앞으로'),
'switchPane' => tr('다른 창으로 옮겨 가기'),
_ => id,
};
/// 스타일의 처음 키 (83 과 같음). [tc] Total Commander 스타일
Map<String, List<SingleActivator>> defaultExplorerKeys({required bool tc}) => {
'delete': [const SingleActivator(LogicalKeyboardKey.delete), if (tc) const SingleActivator(LogicalKeyboardKey.f8)],
'deletePermanent': [const SingleActivator(LogicalKeyboardKey.delete, shift: true)],
'rename': [const SingleActivator(LogicalKeyboardKey.f2)],
'filter': [const SingleActivator(LogicalKeyboardKey.keyF, control: true)],
'up': [const SingleActivator(LogicalKeyboardKey.backspace)],
'back': [const SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true)],
'forward': [const SingleActivator(LogicalKeyboardKey.arrowRight, alt: true)],
'switchPane': [const SingleActivator(LogicalKeyboardKey.tab)],
if (tc) ...{
'copy': [const SingleActivator(LogicalKeyboardKey.f5)],
'move': [const SingleActivator(LogicalKeyboardKey.f6)],
'newFolder': [const SingleActivator(LogicalKeyboardKey.f7)],
'refresh': [const SingleActivator(LogicalKeyboardKey.keyR, control: true)],
'markNext': [const SingleActivator(LogicalKeyboardKey.insert)],
'mark': [const SingleActivator(LogicalKeyboardKey.space)],
'quickLook': [const SingleActivator(LogicalKeyboardKey.f3)],
} else ...{
'quickLook': [const SingleActivator(LogicalKeyboardKey.space)],
'refresh': [const SingleActivator(LogicalKeyboardKey.f5)],
'clipCopy': [const SingleActivator(LogicalKeyboardKey.keyC, control: true)],
'clipCut': [const SingleActivator(LogicalKeyboardKey.keyX, control: true)],
'paste': [const SingleActivator(LogicalKeyboardKey.keyV, control: true)],
'selectAll': [const SingleActivator(LogicalKeyboardKey.keyA, control: true)],
},
};
/// 같은 키 (같은 글쇠 · 같은 Ctrl · Shift · Alt · Meta)
bool sameKey(SingleActivator a, SingleActivator b) =>
a.trigger == b.trigger && a.control == b.control && a.shift == b.shift && a.alt == b.alt && a.meta == b.meta;
/// 설정에 두는 꼴: "c,s,a,m|글쇠 번호" (없으면 '' = 그 동작은 단축키 없음). 틀리면 null
String keySpec(SingleActivator a) =>
'${[if (a.control) 'c', if (a.shift) 's', if (a.alt) 'a', if (a.meta) 'm'].join(',')}|${a.trigger.keyId}';
SingleActivator? parseKeySpec(String spec) {
final bar = spec.lastIndexOf('|');
if (bar < 0) return null;
final id = int.tryParse(spec.substring(bar + 1));
final key = id == null ? null : LogicalKeyboardKey.findKeyByKeyId(id);
if (key == null) return null;
final mods = spec.substring(0, bar).split(',');
return SingleActivator(key,
control: mods.contains('c'), shift: mods.contains('s'), alt: mods.contains('a'), meta: mods.contains('m'));
}
/// 화면에 보일 글: "Ctrl+Shift+K"
String keyText(SingleActivator a) {
final name = a.trigger.keyLabel.isNotEmpty ? a.trigger.keyLabel : (a.trigger.debugName ?? '?');
return [if (a.control) 'Ctrl', if (a.shift) 'Shift', if (a.alt) 'Alt', if (a.meta) 'Meta', name == ' ' ? 'Space' : name].join('+');
}
/// 바꾼 것을 넣은 동작마다의 키. 바꾼 키는 다른 동작에서 뺀다
Map<String, List<SingleActivator>> effectiveExplorerKeys({required bool tc, required Map<String, String> overrides}) {
final out = {for (final e in defaultExplorerKeys(tc: tc).entries) e.key: [...e.value]};
final mine = <String, SingleActivator?>{};
for (final e in overrides.entries) {
if (!explorerKeyActions.contains(e.key)) continue;
mine[e.key] = e.value.isEmpty ? null : parseKeySpec(e.value);
if (e.value.isNotEmpty && mine[e.key] == null) mine.remove(e.key); // 틀린 값은 처음 키 그대로
}
for (final e in mine.entries) {
out[e.key] = [?e.value];
}
for (final e in mine.entries) {
final k = e.value;
if (k == null) continue;
for (final other in out.keys) {
if (other != e.key) out[other]!.removeWhere((x) => sameKey(x, k));
}
}
return out;
}
/// [action] 에 [key] 를 주면 키를 잃는 다른 동작 (없으면 null). Esc 는 쓸 수 없음 ('escape')
String? keyConflict(String action, SingleActivator key, {required bool tc, required Map<String, String> overrides}) {
if (key.trigger == LogicalKeyboardKey.escape) return 'escape';
final now = effectiveExplorerKeys(tc: tc, overrides: overrides);
for (final e in now.entries) {
if (e.key != action && e.value.any((x) => sameKey(x, key))) return e.key;
}
return null;
}
/// 키를 받는 창에서 모자 키 (Ctrl · Shift · Alt · Meta) 만 누른 것은 아직 키가 아니다
bool isModifierOnly(LogicalKeyboardKey k) => {
LogicalKeyboardKey.control, LogicalKeyboardKey.controlLeft, LogicalKeyboardKey.controlRight, //
LogicalKeyboardKey.shift, LogicalKeyboardKey.shiftLeft, LogicalKeyboardKey.shiftRight,
LogicalKeyboardKey.alt, LogicalKeyboardKey.altLeft, LogicalKeyboardKey.altRight,
LogicalKeyboardKey.meta, LogicalKeyboardKey.metaLeft, LogicalKeyboardKey.metaRight,
}.contains(k);
/// 키 이벤트를 받는 쪽 (설정의 키 받기 창) 에서 쓰는 모자 상태
SingleActivator activatorFromEvent(KeyEvent e) {
final hk = HardwareKeyboard.instance;
return SingleActivator(e.logicalKey,
control: hk.isControlPressed, shift: hk.isShiftPressed, alt: hk.isAltPressed, meta: hk.isMetaPressed);
}
/// 표시용 위젯이 키 글을 고정 폭 글로
TextStyle get keyTextStyle => const TextStyle(fontFamily: 'monospace', fontSize: 13);

View File

@ -0,0 +1,146 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../app/app_controller.dart';
import '../l10n/tr.dart';
import 'explorer_keys.dart';
import 'explorer_look.dart' show ExplorerStyle;
import 'theme.dart';
/// X-plore 비교 5-1 ⑥ (관리자 지시 10/21): 파일 탐색기 단축키 바꾸기 - 동작마다 지금 키 · [바꾸기] (다음에 누른 키) · [처음 값으로].
/// 지금 고른 스타일의 처음 키를 보이고, 바꾼 키는 모든 스타일에 쓰인다
class ExplorerKeysPage extends StatelessWidget {
final AppController c;
const ExplorerKeysPage({super.key, required this.c});
static Future<void> open(BuildContext context, AppController c) =>
Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => ExplorerKeysPage(c: c)));
bool get _tc => ExplorerStyle.of(c.settings.explorerStyle) == ExplorerStyle.totalcmd;
Future<void> _change(BuildContext context, String id) async {
final picked = await showDialog<SingleActivator?>(
context: context,
builder: (_) => _KeyCaptureDialog(c: c, action: id, tc: _tc),
);
if (picked == null) return;
await c.updateSettings((x) => x.explorerKeyOverrides = {...x.explorerKeyOverrides, id: _none(picked) ? '' : keySpec(picked)});
}
static bool _none(SingleActivator a) => a.trigger == LogicalKeyboardKey.abort;
@override
Widget build(BuildContext context) => ListenableBuilder(
listenable: c,
builder: (context, _) {
final ov = c.settings.explorerKeyOverrides;
final keys = effectiveExplorerKeys(tc: _tc, overrides: ov);
return Scaffold(
appBar: AppBar(
title: Text(tr('파일 탐색기 단축키')),
actions: [
if (ov.isNotEmpty)
TextButton(
onPressed: () => c.updateSettings((x) => x.explorerKeyOverrides = {}),
child: Text(tr('모두 처음 값으로')),
),
],
),
body: ListView(children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
child: Text(
trf('지금 스타일 ({0}) 의 키입니다. 바꾼 키는 모든 스타일에 쓰이고, 다른 동작과 겹치면 그 동작의 키는 없어집니다. Esc 는 바꿀 수 없습니다',
[ExplorerStyle.of(c.settings.explorerStyle).label]),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
),
for (final id in explorerKeyActions)
ListTile(
dense: true,
title: Text(explorerKeyLabel(id)),
subtitle: Text(
keys[id]?.isNotEmpty == true ? keys[id]!.map(keyText).join(' · ') : tr('(없음)'),
style: keyTextStyle.copyWith(color: ov.containsKey(id) ? JjColors.accent : null),
),
trailing: Wrap(spacing: 4, children: [
if (ov.containsKey(id))
TextButton(
onPressed: () => c.updateSettings((x) => x.explorerKeyOverrides = {...x.explorerKeyOverrides}..remove(id)),
child: Text(tr('처음 값으로')),
),
OutlinedButton(onPressed: () => _change(context, id), child: Text(tr('바꾸기'))),
]),
),
]),
);
},
);
}
/// 다음에 누른 키를 받는 창 - 겹치면 알리고 [저장] 은 그래도 됨. [단축키 없음] 은 abort 글쇠로 돌려준다 (부르는 쪽이 '' 로 저장)
class _KeyCaptureDialog extends StatefulWidget {
final AppController c;
final String action;
final bool tc;
const _KeyCaptureDialog({required this.c, required this.action, required this.tc});
@override
State<_KeyCaptureDialog> createState() => _KeyCaptureDialogState();
}
class _KeyCaptureDialogState extends State<_KeyCaptureDialog> {
SingleActivator? _key;
final _focus = FocusNode();
@override
void dispose() {
_focus.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final k = _key;
final clash = k == null ? null : keyConflict(widget.action, k, tc: widget.tc, overrides: widget.c.settings.explorerKeyOverrides);
return AlertDialog(
title: Text(trf('{0} - 단축키', [explorerKeyLabel(widget.action)])),
content: Focus(
focusNode: _focus,
autofocus: true,
onKeyEvent: (_, e) {
if (e is! KeyDownEvent || isModifierOnly(e.logicalKey)) return KeyEventResult.handled;
setState(() => _key = activatorFromEvent(e));
return KeyEventResult.handled;
},
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(tr('쓸 키를 누르세요 (Ctrl · Shift · Alt 와 함께 눌러도 됩니다)')),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(border: Border.all(color: JjColors.border), borderRadius: BorderRadius.circular(6)),
child: Text(k == null ? '…' : keyText(k), style: keyTextStyle.copyWith(fontSize: 16)),
),
if (clash == 'escape')
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(tr('Esc 는 선택 · 빠른 필터를 닫는 키라 쓸 수 없습니다'), style: const TextStyle(color: JjColors.danger, fontSize: 12)),
)
else if (clash != null)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(trf('이미 "{0}" 에 쓰는 키입니다 - 저장하면 그 동작의 이 키는 없어집니다', [explorerKeyLabel(clash)]),
style: const TextStyle(color: Colors.orangeAccent, fontSize: 12)),
),
]),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, const SingleActivator(LogicalKeyboardKey.abort)),
child: Text(tr('단축키 없음')),
),
TextButton(onPressed: () => Navigator.pop(context), child: Text(tr('취소'))),
FilledButton(onPressed: k == null || clash == 'escape' ? null : () => Navigator.pop(context, k), child: Text(tr('저장'))),
],
);
}
}

View File

@ -58,6 +58,7 @@ import '../l10n/tr.dart';
import 'explorer_bookmarks.dart';
import 'explorer_search.dart';
import 'remote_thumb_view.dart';
import 'explorer_keys.dart' show defaultExplorerKeys, effectiveExplorerKeys;
import 'sqlite_viewer.dart' show openSqliteViewer, sqliteExtensions;
import 'quick_look.dart';
import 'file_tools.dart' show showFileHash, showFolderProperties;
@ -4460,17 +4461,9 @@ class _ExplorerPageState extends State<ExplorerPage> {
// P0 (15 지적 10/17): 글자 칸 (빠른 필터 · 경로 등) 에 치는 중이면 파일 단축키를 쓰지 않는다 - 예전에는 CallbackShortcuts 가
// 글자 칸보다 먼저 키를 받아, 칸에서 Delete 를 누르면 파일 지우기 창이 뜨고 Ctrl+A · C · V · Space 도 파일에 쓰였다
final bindings = <ShortcutActivator, VoidCallback>{
// 83: 스타일에 맞는 단축키 (Total Commander: F5 복사 · F6 이동 · F7 새 폴더 · F8 지우기 · Insert/Space 표시 ·
// Ctrl+R 새로 고침 / Windows 탐색기 · X-plore: Ctrl+C · X · V · A · F5 새로 고침)
..._styleKeys(),
const SingleActivator(LogicalKeyboardKey.delete): () => _button(ExplorerButton.delete),
// 65: Shift+Delete = 휴지통을 거치지 않고 (창에서 한 번 더 확인)
const SingleActivator(LogicalKeyboardKey.delete, shift: true): () {
_shiftDelete = true;
_button(ExplorerButton.delete);
},
const SingleActivator(LogicalKeyboardKey.f2): () => _button(ExplorerButton.rename),
// 180: 선택 모드 · 표시 풀기 (복사 · 이동이 끝나도 선택 모드는 그대로이므로 바로 나올 수 있게)
// 83 · X-plore 비교 5-1 ⑥: 스타일에 맞는 처음 키 + 사용자가 바꾼 키 ([explorer_keys.dart], 환경 설정 > 파일 탐색기 > 단축키)
..._keyBindings(),
// 180: 선택 모드 · 표시 풀기 (복사 · 이동이 끝나도 선택 모드는 그대로이므로 바로 나올 수 있게) - Esc 는 바꾸지 않는다
const SingleActivator(LogicalKeyboardKey.escape): () {
// 76: 열린 빠른 필터부터 닫는다
if (_panes[_active].filterOpen) {
@ -4479,13 +4472,6 @@ class _ExplorerPageState extends State<ExplorerPage> {
_setSelecting(false);
}
},
const SingleActivator(LogicalKeyboardKey.keyF, control: true): () => _openFilter(_panes[_active]),
const SingleActivator(LogicalKeyboardKey.backspace): () => _button(ExplorerButton.up),
// 75: 지금 창의 뒤로 · 앞으로
const SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true): () => _travel(_panes[_active], forward: false),
const SingleActivator(LogicalKeyboardKey.arrowRight, alt: true): () => _travel(_panes[_active], forward: true),
const SingleActivator(LogicalKeyboardKey.tab): () =>
setState(() => _active = _dual ? 1 - _active : 0),
};
return Focus(
canRequestFocus: false,
@ -5123,34 +5109,52 @@ class _ExplorerPageState extends State<ExplorerPage> {
/// 83: 지금 스타일이 Total Commander 인지 (Rsync 화면은 스타일 단축키를 쓰지 않는다 - 늘 같은 버튼)
bool get _tcKeys => !widget.rsync && _look == ExplorerStyle.totalcmd;
/// 83: 스타일에 맞는 단축키
Map<ShortcutActivator, VoidCallback> _styleKeys() {
if (widget.rsync) return {const SingleActivator(LogicalKeyboardKey.f5): () => _button(ExplorerButton.refresh)};
if (_tcKeys) {
return {
const SingleActivator(LogicalKeyboardKey.f5): () => _button(ExplorerButton.copy),
const SingleActivator(LogicalKeyboardKey.f6): () => _button(ExplorerButton.move),
const SingleActivator(LogicalKeyboardKey.f7): () => _button(ExplorerButton.newFolder),
const SingleActivator(LogicalKeyboardKey.f8): () => _button(ExplorerButton.delete),
const SingleActivator(LogicalKeyboardKey.keyR, control: true): () => _button(ExplorerButton.refresh),
// Insert: 표시하고 다음 줄로 · Space: 표시 (TC 처럼)
const SingleActivator(LogicalKeyboardKey.insert): () => _markFocused(next: true),
const SingleActivator(LogicalKeyboardKey.space): () => _markFocused(next: false),
// 81: 빠른 보기 (TC 의 보기 키)
const SingleActivator(LogicalKeyboardKey.f3): () => _quickLook(_pane),
};
}
return {
// 81: 빠른 보기 (Windows · macOS 처럼 Space)
const SingleActivator(LogicalKeyboardKey.space): () => _quickLook(_pane),
const SingleActivator(LogicalKeyboardKey.f5): () => _button(ExplorerButton.refresh),
const SingleActivator(LogicalKeyboardKey.keyC, control: true): () => _toClipboard(_targets(_pane), move: false),
const SingleActivator(LogicalKeyboardKey.keyX, control: true): () => _toClipboard(_targets(_pane), move: true),
const SingleActivator(LogicalKeyboardKey.keyV, control: true): () => _paste(_pane, _pane.current),
const SingleActivator(LogicalKeyboardKey.keyA, control: true): () {
/// 83 · X-plore 비교 5-1 ⑥ (관리자 지시 10/21): 동작마다의 키 → 할 일. 처음 키는 스타일대로 (Total Commander: F5 복사 · F6 이동 ·
/// F7 새 폴더 · F8 지우기 · Insert/Space 표시 · Ctrl+R 새로 고침 / Windows 탐색기 · X-plore: Ctrl+C · X · V · A · F5 새로 고침),
/// 사용자가 바꾼 것은 설정 explorerKeyOverrides. Rsync 화면은 늘 같은 키 (바꾼 것을 쓰지 않음)
Map<ShortcutActivator, VoidCallback> _keyBindings() {
final cb = <String, VoidCallback>{
'copy': () => _button(ExplorerButton.copy),
'move': () => _button(ExplorerButton.move),
'newFolder': () => _button(ExplorerButton.newFolder),
'delete': () => _button(ExplorerButton.delete),
// 65: Shift+Delete = 휴지통을 거치지 않고 (창에서 한 번 더 확인)
'deletePermanent': () {
_shiftDelete = true;
_button(ExplorerButton.delete);
},
'rename': () => _button(ExplorerButton.rename),
'refresh': () => _button(ExplorerButton.refresh),
// 81: 빠른 보기
'quickLook': () => _quickLook(_pane),
'clipCopy': () => _toClipboard(_targets(_pane), move: false),
'clipCut': () => _toClipboard(_targets(_pane), move: true),
'paste': () => _paste(_pane, _pane.current),
'selectAll': () {
if (!_selecting) setState(() => _selecting = true);
_markWhere(_pane, _pane.current, (_) => true);
},
// TC: Space 표시 · Insert 표시하고 다음 줄로
'mark': () => _markFocused(next: false),
'markNext': () => _markFocused(next: true),
'filter': () => _openFilter(_panes[_active]),
'up': () => _button(ExplorerButton.up),
// 75: 지금 창의 뒤로 · 앞으로
'back': () => _travel(_panes[_active], forward: false),
'forward': () => _travel(_panes[_active], forward: true),
'switchPane': () => setState(() => _active = _dual ? 1 - _active : 0),
};
final keys = widget.rsync
? {
for (final e in defaultExplorerKeys(tc: false).entries)
if (const {'delete', 'deletePermanent', 'rename', 'refresh', 'filter', 'up', 'back', 'forward', 'switchPane'}.contains(e.key))
e.key: e.value,
}
: effectiveExplorerKeys(tc: _tcKeys, overrides: c.settings.explorerKeyOverrides);
return {
for (final e in keys.entries)
if (cb[e.key] != null)
for (final a in e.value) a: cb[e.key]!,
};
}

View File

@ -33,6 +33,7 @@ import 'ftp_settings.dart';
import 'smb_settings.dart';
import 'webdav_settings.dart';
import 'explorer_look.dart' show ExplorerStyle;
import 'explorer_keys_page.dart';
import 'theme.dart';
import '../l10n/tr.dart';
import 'setting_tile.dart';
@ -888,6 +889,16 @@ class _SettingsPageState extends State<SettingsPage> {
onChanged: (v) => c.updateSettings((x) => x.explorerFolderSize = v ?? 'sort'),
),
),
// X-plore 비교 5-1 ⑥ (관리자 지시 10/21): 단축키 바꾸기 (키보드가 있는 PC 에서만 보임)
if (desk)
ListTile(
title: Text(tr('파일 탐색기 단축키')),
subtitle: Text(s.explorerKeyOverrides.isEmpty
? tr('스타일대로 (바꾼 것 없음)')
: trf('바꾼 것 {0}개', [s.explorerKeyOverrides.length])),
trailing: const Icon(Icons.chevron_right),
onTap: () => ExplorerKeysPage.open(context, c),
),
SettingTile(
title: Text(tr('스타일')),
// 83: 스타일마다 단축키 - PC 에서만 (194: 키보드가 없는 폰 · 태블릿에는 쓸모없는 긴 글, 15 지적)

View File

@ -0,0 +1,132 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:jj_mkvmaker/app/app_controller.dart';
import 'package:jj_mkvmaker/app/settings.dart';
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
import 'package:jj_mkvmaker/services/platform_services.dart';
import 'package:jj_mkvmaker/ui/explorer_keys.dart';
import 'package:jj_mkvmaker/ui/explorer_keys_page.dart';
import 'package:jj_mkvmaker/ui/explorer_page.dart';
/// X-plore 비교 5-1 ⑥ (관리자 지시 10/21): 파일 탐색기 단축키 바꾸기 - 처음은 83 과 같은 키 그대로, 바꾼 것만 설정에
void main() {
const k = LogicalKeyboardKey.keyK;
List<String> texts(List<SingleActivator> l) => l.map(keyText).toList();
test('바꾼 것이 없으면 (처음) 83 의 키 그대로 - Total Commander / 그 밖', () {
final tc = effectiveExplorerKeys(tc: true, overrides: {});
expect(texts(tc['copy']!), ['F5']);
expect(texts(tc['move']!), ['F6']);
expect(texts(tc['newFolder']!), ['F7']);
expect(texts(tc['delete']!), ['Delete', 'F8']);
expect(texts(tc['refresh']!), ['Ctrl+R']);
expect(texts(tc['mark']!), ['Space']);
expect(texts(tc['markNext']!), ['Insert']);
expect(texts(tc['quickLook']!), ['F3']);
final win = effectiveExplorerKeys(tc: false, overrides: {});
expect(texts(win['refresh']!), ['F5']);
expect(texts(win['clipCopy']!), ['Ctrl+C']);
expect(texts(win['paste']!), ['Ctrl+V']);
expect(texts(win['selectAll']!), ['Ctrl+A']);
expect(texts(win['quickLook']!), ['Space']);
expect(win['copy'], isNull, reason: 'Windows 모양은 F5 가 새로 고침 (복사 키 없음)');
expect(texts(win['deletePermanent']!), ['Shift+Delete']);
expect(texts(win['back']!), ['Alt+Arrow Left']);
});
test('바꾸기: 그 동작은 새 키만 · 겹치는 다른 동작은 그 키를 잃음 · \'\' 는 없음 · 틀린 값은 처음 키 · 저장 꼴 왕복', () {
final spec = keySpec(const SingleActivator(k, control: true, shift: true));
expect(parseKeySpec(spec), isA<SingleActivator>().having((a) => keyText(a), 'text', 'Ctrl+Shift+K'));
final a = effectiveExplorerKeys(tc: false, overrides: {'newFolder': spec, 'paste': '', 'rename': 'zzz'});
expect(texts(a['newFolder']!), ['Ctrl+Shift+K']);
expect(a['paste'], isEmpty);
expect(texts(a['rename']!), ['F2'], reason: '틀린 값은 처음 키');
// F5 를 복사에 주면 새로 고침은 F5 를 잃음
final b = effectiveExplorerKeys(tc: false, overrides: {'copy': keySpec(const SingleActivator(LogicalKeyboardKey.f5))});
expect(texts(b['copy']!), ['F5']);
expect(b['refresh'], isEmpty);
expect(keyConflict('copy', const SingleActivator(LogicalKeyboardKey.f5), tc: false, overrides: {}), 'refresh');
expect(keyConflict('copy', const SingleActivator(LogicalKeyboardKey.escape), tc: false, overrides: {}), 'escape');
expect(keyConflict('copy', const SingleActivator(k, control: true), tc: false, overrides: {}), isNull);
// 설정 저장 · 읽기 · 틀린 꼴은 비움
final back = AppSettings.fromJson((AppSettings()..explorerKeyOverrides = {'copy': spec}).toJson());
expect(back.explorerKeyOverrides, {'copy': spec});
expect(AppSettings.fromJson({'explorerKeyOverrides': 'x'}).explorerKeyOverrides, isEmpty);
expect(AppSettings().explorerKeyOverrides, isEmpty);
});
late Directory tmp;
setUp(() => tmp = Directory.systemTemp.createTempSync('jj_keys_'));
tearDown(() => tmp.deleteSync(recursive: true));
AppController ctl() => AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
testWidgets('탐색기: 바꾼 키 (새 폴더 = Ctrl+Shift+K) 를 누르면 그 동작 · 처음 키도 그대로 (F2 이름 변경)', (tester) async {
tester.view.physicalSize = const Size(1400, 900);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
final c = ctl();
c.settings
..explorerPaths = [tmp.path, tmp.path]
..explorerStyle = 'windows'
..explorerKeyOverrides = {'newFolder': keySpec(const SingleActivator(k, control: true, shift: true))};
await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c))));
for (var i = 0; i < 20; i++) {
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 30)));
await tester.pump();
}
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
await tester.sendKeyEvent(k);
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.pumpAndSettle();
expect(find.widgetWithText(AlertDialog, '새 폴더'), findsOneWidget, reason: '바꾼 키로 새 폴더 창');
await tester.tap(find.text('취소').last);
await tester.pumpAndSettle();
await tester.pumpWidget(const SizedBox());
await tester.pump(const Duration(seconds: 1));
});
testWidgets('설정 화면: [바꾸기] 에서 누른 키 저장 · 겹치면 알림 · [처음 값으로] · [단축키 없음]', (tester) async {
tester.view.physicalSize = const Size(1000, 1600);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
final c = ctl()..settings.explorerStyle = 'windows';
await tester.pumpWidget(MaterialApp(home: ExplorerKeysPage(c: c)));
Future<void> change(String label) async {
await tester.tap(find.descendant(of: find.widgetWithText(ListTile, label), matching: find.text('바꾸기')));
await tester.pumpAndSettle();
}
await change('다른 창으로 복사');
await tester.sendKeyEvent(LogicalKeyboardKey.f5);
await tester.pump();
expect(find.textContaining('이미 "새로 고침" 에 쓰는 키입니다'), findsOneWidget);
await tester.tap(find.text('저장'));
await tester.pumpAndSettle();
expect(c.settings.explorerKeyOverrides['copy'], keySpec(const SingleActivator(LogicalKeyboardKey.f5)));
expect(find.descendant(of: find.widgetWithText(ListTile, '새로 고침'), matching: find.text('(없음)')), findsOneWidget,
reason: '겹친 동작은 키를 잃음');
// 처음 값으로
await tester.tap(find.descendant(of: find.widgetWithText(ListTile, '다른 창으로 복사'), matching: find.text('처음 값으로')));
await tester.pumpAndSettle();
expect(c.settings.explorerKeyOverrides, isEmpty);
// 단축키 없음
await change('붙여넣기');
await tester.tap(find.text('단축키 없음'));
await tester.pumpAndSettle();
expect(c.settings.explorerKeyOverrides['paste'], '');
// Esc 는 저장할 수 없음
await change('삭제');
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pump();
if (find.byType(AlertDialog).evaluate().isNotEmpty) {
expect(tester.widget<FilledButton>(find.widgetWithText(FilledButton, '저장')).onPressed, isNull);
}
});
}