WIP 81 파일 탐색기 빠른 보기: Space (TC 는 F3) · 길게 누르기 [미리 보기] - 동영상 썸네일 · 길이 · 코덱, 그림, 글 앞 64KB (검토 대기)
- 관리자 지시 (F10): 미리 보기 창 / PC 에서 Space 로 빠른 보기 (썸네일 · 길이 · 코덱). 관리자 판단: 띄우는 창 (옆에 붙는 칸은 넓은 화면용으로 나중에 따로). - 여는 곳: Windows 탐색기 · X-plore 모양은 Space (빠른 필터가 닫혀 있을 때만 - 칸에 치는 중이면 빈칸), TC 모양은 F3 (Space 는 표시 그대로), 길게 누르기 [미리 보기] (파일). - 동영상 · 음악: 큰 썸네일 (가로 640, jj_thumbs 에 따로 기억) · 길이 · 영상 코덱 · 해상도 · 소리 (코덱 · 언어) · 자막 트랙. ffprobe 는 기다리는 동안 "정보를 읽는 중…", 20초 넘으면 "읽지 못했습니다" (창은 멈추지 않음). - 그림: 그림 그대로. 글 파일 (txt · srt · ass · smi · log · json …): 앞 64KB, UTF-8 이 아니면 "미리 볼 수 없음". - WebDAV: 썸네일 · ffprobe 없이 이름 · 크기 · 날짜만 (비밀번호를 명령 줄에 넣지 않게 - 관리자 판단). - ← → (↑ ↓) 로 같은 폴더의 앞 · 다음 (지금 목록 차례 - 필터 · 정렬 반영), Space · Esc 로 닫기, Enter · [열기 · 재생]. - 시험: quick_look_test (글 앞부분 · CP949 · 64KB 경계, ← → · 열기 · Space 닫기), explorer_test 81 (Space · 필터 중 빈칸 · TC F3). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>묶음6
parent
60336d342d
commit
b1f20914a0
|
|
@ -325,6 +325,7 @@
|
|||
"WebDAV 에서 받는 중": "Downloading from WebDAV",
|
||||
"WebDAV 원본에 씁니다 (PC 의 폴더는 바뀌면 바로 맞춤). 짧을수록 빨리 맞추지만 서버 · 배터리를 더 씁니다.": "Used for WebDAV sources (PC folders sync as soon as they change). Shorter means faster syncing but more server and battery use.",
|
||||
"WebDAV 의 동영상은 MKV 목록에 넣을 수 없습니다. 먼저 이 기기로 복사하세요.": "Videos on WebDAV can't be added to the MKV list. Copy them to this device first.",
|
||||
"WebDAV 파일은 이름 · 크기 · 날짜만 봅니다": "WebDAV files show only name, size and date",
|
||||
"WebDAV: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --delete · --remove-source-files 를 따름).": "WebDAV: the app syncs directly instead of rsync (compares size and modified time; follows -u, --delete and --remove-source-files).",
|
||||
"WebDAV: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --remove-source-files 만 따름).": "WebDAV: the app syncs by itself instead of rsync (compares size and modified time; of the options only -u and --remove-source-files apply).",
|
||||
"WebM (동영상)": "WebM (video)",
|
||||
|
|
@ -666,6 +667,7 @@
|
|||
"그림을 만들지 못했습니다: {0}": "Couldn't make the image: {0}",
|
||||
"그림을 바꿔 만들기": "Make from this picture",
|
||||
"그림을 받을 수 없습니다 ({0})": "Couldn't get the image ({0})",
|
||||
"그림을 열 수 없습니다": "Can't open the image",
|
||||
"그림을 올릴 수 없습니다 ({0})": "Couldn't upload the image ({0})",
|
||||
"그림을 읽을 수 없습니다": "Cannot read the image",
|
||||
"그림을 페이지로 넣기 (이 페이지 뒤)": "Add images as pages (after this page)",
|
||||
|
|
@ -701,6 +703,7 @@
|
|||
"기억한 복사가 없습니다. 파일 탐색기에서 복사 · 이동하면 여기에 자동으로 등록됩니다.": "No remembered copies. Copies · moves in the file explorer are added here automatically.",
|
||||
"기타 즐겨찾기": "Other bookmarks",
|
||||
"길게 누르기 · 오른쪽 클릭은 늘 기능 메뉴 (복사 · 이동 · 삭제 · 이름 변경 …). Ctrl + 클릭: 여러 개 고르기": "Long press · right click always opens the action menu (copy · move · delete · rename …). Ctrl + click: select several",
|
||||
"길이": "Length",
|
||||
"길이 {0}": "Length {0}",
|
||||
"끄기": "Off",
|
||||
"끄면 (기본) Tor 화면을 떠날 때 (다른 화면 · 앱이 뒤로) 지웁니다. 일반 브라우저와는 늘 따로입니다.": "When off (default), they are cleared when you leave the Tor screen (another screen, or the app goes to the background). They are always kept separate from the normal browser.",
|
||||
|
|
@ -856,6 +859,7 @@
|
|||
"다음 (N)": "Next (N)",
|
||||
"다음 {0}개 기다림": "{0} more waiting",
|
||||
"다음 페이지": "Next page",
|
||||
"다음 항목 (→)": "Next item (→)",
|
||||
"다음부터 묻지 않기 (환경 설정 > 실행 에서 바꿀 수 있음)": "Don't ask again (can be changed in Settings > Run)",
|
||||
"단계": "Steps",
|
||||
"단어 첫 글자만 크게 (Abc)": "Capitalize words (Abc)",
|
||||
|
|
@ -1071,7 +1075,9 @@
|
|||
"문제 {0}개 - 고치기 전에는 바꾸지 않습니다": "{0} problems - nothing is renamed until they're fixed",
|
||||
"묻기": "Ask",
|
||||
"물어보지 않기": "Never ask",
|
||||
"미리 보기": "Quick look",
|
||||
"미리 볼 수 없습니다: {0}": "Cannot preview: {0}",
|
||||
"미리 볼 수 없음 (글자 코드가 UTF-8 이 아닙니다)": "Can't preview (the text isn't UTF-8)",
|
||||
"미리보기: 목록에서 동영상을 고르세요": "Preview: choose a video in the list",
|
||||
"미얀마어": "Burmese",
|
||||
"미지정": "Unspecified",
|
||||
|
|
@ -1354,6 +1360,7 @@
|
|||
"세로 4:5": "Portrait 4:5",
|
||||
"세로 9:16": "Portrait 9:16",
|
||||
"세로 고정": "Portrait lock",
|
||||
"소리": "Audio",
|
||||
"속도 제한": "Speed limit",
|
||||
"속도 제한 (KB/s)": "Speed limit (KB/s)",
|
||||
"속도 제한 {0} KB/s": "Speed limit {0} KB/s",
|
||||
|
|
@ -1455,6 +1462,7 @@
|
|||
"압축 파일 안에 없습니다": "Not in the archive",
|
||||
"압축 파일 안에 프로그램이 없습니다.": "The program is not in the archive.",
|
||||
"압축을 풀 수 없습니다: {0}": "Cannot extract the archive: {0}",
|
||||
"앞 항목 (←)": "Previous item (←)",
|
||||
"앞으로": "Forward",
|
||||
"앞의 rsync 가 끝난 뒤에 하세요.": "Wait until the previous rsync finishes.",
|
||||
"앞의 버튼 보기": "Previous buttons",
|
||||
|
|
@ -1522,8 +1530,10 @@
|
|||
"연도": "Year",
|
||||
"열 수 없습니다: {0}": "Cannot open: {0}",
|
||||
"열기": "Open",
|
||||
"열기 · 재생": "Open · Play",
|
||||
"영구 삭제": "Delete permanently",
|
||||
"영구 삭제 ({0}개, 되돌릴 수 없음)": "Delete permanently ({0}, cannot be undone)",
|
||||
"영상": "Video",
|
||||
"영상 {0}": "Video {0}",
|
||||
"영상 · 음성 합치는 중": "Merging video · audio",
|
||||
"영상 재생 오류: {0}": "Video playback error: {0}",
|
||||
|
|
@ -1868,6 +1878,7 @@
|
|||
"자막 이동 실패: {0} ({1})": "Failed to move subtitle: {0} ({1})",
|
||||
"자막 저장: {0} ({1}, {2}개 줄){3}": "Subtitles saved: {0} ({1}, {2} lines){3}",
|
||||
"자막 크기": "Subtitle size",
|
||||
"자막 트랙 {0}개: {1}": "{0} subtitle tracks: {1}",
|
||||
"자막 파일 선택": "Choose subtitle files",
|
||||
"자막 파일 추가": "Add subtitle files",
|
||||
"자막 파일을 받을 수 없습니다 ({0})": "Cannot download the subtitle file ({0})",
|
||||
|
|
@ -1969,6 +1980,8 @@
|
|||
"정말 되돌릴까요?": "Really roll back?",
|
||||
"정말 초기화할까요?": "Really reset?",
|
||||
"정보": "Info",
|
||||
"정보를 읽는 중…": "Reading info…",
|
||||
"정보를 읽지 못했습니다": "Couldn't read the info",
|
||||
"정사각 1:1": "Square 1:1",
|
||||
"정지": "Stop",
|
||||
"정하기": "Set",
|
||||
|
|
|
|||
|
|
@ -325,6 +325,7 @@
|
|||
"WebDAV 에서 받는 중": "WebDAV からダウンロード中",
|
||||
"WebDAV 원본에 씁니다 (PC 의 폴더는 바뀌면 바로 맞춤). 짧을수록 빨리 맞추지만 서버 · 배터리를 더 씁니다.": "WebDAV の元に使います (PC のフォルダーは変わるとすぐ同期)。短いほど早く同期しますが、サーバー・バッテリーをより使います。",
|
||||
"WebDAV 의 동영상은 MKV 목록에 넣을 수 없습니다. 먼저 이 기기로 복사하세요.": "WebDAV の動画は MKV リストに追加できません。先にこの端末へコピーしてください。",
|
||||
"WebDAV 파일은 이름 · 크기 · 날짜만 봅니다": "WebDAV のファイルは名前・サイズ・日付のみ表示します",
|
||||
"WebDAV: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --delete · --remove-source-files 를 따름).": "WebDAV: rsync の代わりにアプリが直接同期します (サイズ・更新日時で比較、オプションのうち -u・--delete・--remove-source-files に従う)。",
|
||||
"WebDAV: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --remove-source-files 만 따름).": "WebDAV: rsync の代わりにアプリが直接同期します (サイズ・更新日時で比較、オプションは -u・--remove-source-files のみ反映)。",
|
||||
"WebM (동영상)": "WebM(動画)",
|
||||
|
|
@ -666,6 +667,7 @@
|
|||
"그림을 만들지 못했습니다: {0}": "画像を作成できませんでした: {0}",
|
||||
"그림을 바꿔 만들기": "画像を変えて作成",
|
||||
"그림을 받을 수 없습니다 ({0})": "画像を受け取れません ({0})",
|
||||
"그림을 열 수 없습니다": "画像を開けません",
|
||||
"그림을 올릴 수 없습니다 ({0})": "画像をアップロードできません ({0})",
|
||||
"그림을 읽을 수 없습니다": "画像を読み込めません",
|
||||
"그림을 페이지로 넣기 (이 페이지 뒤)": "画像をページとして追加 (このページの後)",
|
||||
|
|
@ -701,6 +703,7 @@
|
|||
"기억한 복사가 없습니다. 파일 탐색기에서 복사 · 이동하면 여기에 자동으로 등록됩니다.": "記憶したコピーはありません。ファイルエクスプローラーでコピー · 移動すると自動で登録されます。",
|
||||
"기타 즐겨찾기": "その他のブックマーク",
|
||||
"길게 누르기 · 오른쪽 클릭은 늘 기능 메뉴 (복사 · 이동 · 삭제 · 이름 변경 …). Ctrl + 클릭: 여러 개 고르기": "長押し · 右クリックは常に機能メニュー (コピー · 移動 · 削除 · 名前の変更 …)。Ctrl + クリック: 複数選択",
|
||||
"길이": "長さ",
|
||||
"길이 {0}": "長さ {0}",
|
||||
"끄기": "オフ",
|
||||
"끄면 (기본) Tor 화면을 떠날 때 (다른 화면 · 앱이 뒤로) 지웁니다. 일반 브라우저와는 늘 따로입니다.": "オフ (既定) の場合は Tor 画面を離れるとき (別の画面 · アプリがバックグラウンドへ) に消去します。通常のブラウザーとは常に別です。",
|
||||
|
|
@ -856,6 +859,7 @@
|
|||
"다음 (N)": "次へ(N)",
|
||||
"다음 {0}개 기다림": "次の {0} 件が待機中",
|
||||
"다음 페이지": "次のページ",
|
||||
"다음 항목 (→)": "次の項目 (→)",
|
||||
"다음부터 묻지 않기 (환경 설정 > 실행 에서 바꿀 수 있음)": "次回から確認しない(環境設定 > 実行 で変更できます)",
|
||||
"단계": "ステップ",
|
||||
"단어 첫 글자만 크게 (Abc)": "単語の先頭だけ大文字 (Abc)",
|
||||
|
|
@ -1071,7 +1075,9 @@
|
|||
"문제 {0}개 - 고치기 전에는 바꾸지 않습니다": "問題 {0} 件 - 直すまで変更しません",
|
||||
"묻기": "確認する",
|
||||
"물어보지 않기": "尋ねない",
|
||||
"미리 보기": "クイックプレビュー",
|
||||
"미리 볼 수 없습니다: {0}": "プレビューできません: {0}",
|
||||
"미리 볼 수 없음 (글자 코드가 UTF-8 이 아닙니다)": "プレビューできません (文字コードが UTF-8 ではありません)",
|
||||
"미리보기: 목록에서 동영상을 고르세요": "プレビュー: リストで動画を選んでください",
|
||||
"미얀마어": "ミャンマー語",
|
||||
"미지정": "未指定",
|
||||
|
|
@ -1354,6 +1360,7 @@
|
|||
"세로 4:5": "縦 4:5",
|
||||
"세로 9:16": "縦 9:16",
|
||||
"세로 고정": "縦向き固定",
|
||||
"소리": "音声",
|
||||
"속도 제한": "速度制限",
|
||||
"속도 제한 (KB/s)": "速度制限 (KB/s)",
|
||||
"속도 제한 {0} KB/s": "速度制限 {0} KB/s",
|
||||
|
|
@ -1455,6 +1462,7 @@
|
|||
"압축 파일 안에 없습니다": "圧縮ファイル内にありません",
|
||||
"압축 파일 안에 프로그램이 없습니다.": "圧縮ファイル内にプログラムがありません。",
|
||||
"압축을 풀 수 없습니다: {0}": "圧縮ファイルを展開できません: {0}",
|
||||
"앞 항목 (←)": "前の項目 (←)",
|
||||
"앞으로": "進む",
|
||||
"앞의 rsync 가 끝난 뒤에 하세요.": "前の rsync が終わってから行ってください。",
|
||||
"앞의 버튼 보기": "前のボタンを表示",
|
||||
|
|
@ -1522,8 +1530,10 @@
|
|||
"연도": "年",
|
||||
"열 수 없습니다: {0}": "開けません: {0}",
|
||||
"열기": "開く",
|
||||
"열기 · 재생": "開く・再生",
|
||||
"영구 삭제": "完全に削除",
|
||||
"영구 삭제 ({0}개, 되돌릴 수 없음)": "完全に削除 ({0} 件、元に戻せません)",
|
||||
"영상": "映像",
|
||||
"영상 {0}": "映像 {0}",
|
||||
"영상 · 음성 합치는 중": "映像・音声を結合中",
|
||||
"영상 재생 오류: {0}": "動画再生エラー: {0}",
|
||||
|
|
@ -1868,6 +1878,7 @@
|
|||
"자막 이동 실패: {0} ({1})": "字幕の移動失敗: {0}({1})",
|
||||
"자막 저장: {0} ({1}, {2}개 줄){3}": "字幕を保存: {0}({1}、{2}行){3}",
|
||||
"자막 크기": "字幕サイズ",
|
||||
"자막 트랙 {0}개: {1}": "字幕トラック {0} 個: {1}",
|
||||
"자막 파일 선택": "字幕ファイルを選択",
|
||||
"자막 파일 추가": "字幕ファイルを追加",
|
||||
"자막 파일을 받을 수 없습니다 ({0})": "字幕ファイルをダウンロードできません({0})",
|
||||
|
|
@ -1969,6 +1980,8 @@
|
|||
"정말 되돌릴까요?": "本当に戻しますか?",
|
||||
"정말 초기화할까요?": "本当に初期化しますか?",
|
||||
"정보": "情報",
|
||||
"정보를 읽는 중…": "情報を読み込み中…",
|
||||
"정보를 읽지 못했습니다": "情報を読み取れませんでした",
|
||||
"정사각 1:1": "正方形 1:1",
|
||||
"정지": "停止",
|
||||
"정하기": "設定",
|
||||
|
|
|
|||
|
|
@ -325,6 +325,7 @@
|
|||
"WebDAV 에서 받는 중": "正在从 WebDAV 下载",
|
||||
"WebDAV 원본에 씁니다 (PC 의 폴더는 바뀌면 바로 맞춤). 짧을수록 빨리 맞추지만 서버 · 배터리를 더 씁니다.": "用于 WebDAV 源 (电脑上的文件夹一有更改就同步)。间隔越短同步越快,但会更多占用服务器和电池。",
|
||||
"WebDAV 의 동영상은 MKV 목록에 넣을 수 없습니다. 먼저 이 기기로 복사하세요.": "无法将 WebDAV 上的视频加入 MKV 列表。请先复制到本设备。",
|
||||
"WebDAV 파일은 이름 · 크기 · 날짜만 봅니다": "WebDAV 文件仅显示名称、大小和日期",
|
||||
"WebDAV: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --delete · --remove-source-files 를 따름).": "WebDAV: 由应用直接同步而非 rsync (比较大小 · 修改时间,遵循 -u · --delete · --remove-source-files)。",
|
||||
"WebDAV: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --remove-source-files 만 따름).": "WebDAV: 由应用代替 rsync 直接同步 (比较大小和修改时间,选项中仅 -u・--remove-source-files 生效)。",
|
||||
"WebM (동영상)": "WebM(视频)",
|
||||
|
|
@ -666,6 +667,7 @@
|
|||
"그림을 만들지 못했습니다: {0}": "无法生成图片: {0}",
|
||||
"그림을 바꿔 만들기": "基于图片生成",
|
||||
"그림을 받을 수 없습니다 ({0})": "无法获取图片 ({0})",
|
||||
"그림을 열 수 없습니다": "无法打开图片",
|
||||
"그림을 올릴 수 없습니다 ({0})": "无法上传图片 ({0})",
|
||||
"그림을 읽을 수 없습니다": "无法读取图片",
|
||||
"그림을 페이지로 넣기 (이 페이지 뒤)": "将图片作为页面加入 (本页之后)",
|
||||
|
|
@ -701,6 +703,7 @@
|
|||
"기억한 복사가 없습니다. 파일 탐색기에서 복사 · 이동하면 여기에 자동으로 등록됩니다.": "没有记住的复制。在文件浏览器中复制 · 移动后会自动登记到这里。",
|
||||
"기타 즐겨찾기": "其他书签",
|
||||
"길게 누르기 · 오른쪽 클릭은 늘 기능 메뉴 (복사 · 이동 · 삭제 · 이름 변경 …). Ctrl + 클릭: 여러 개 고르기": "长按 · 右键总是打开功能菜单 (复制 · 移动 · 删除 · 重命名 …)。Ctrl + 点击: 多选",
|
||||
"길이": "时长",
|
||||
"길이 {0}": "时长 {0}",
|
||||
"끄기": "关闭",
|
||||
"끄면 (기본) Tor 화면을 떠날 때 (다른 화면 · 앱이 뒤로) 지웁니다. 일반 브라우저와는 늘 따로입니다.": "关闭时(默认)在离开 Tor 页面时(切换到其他页面或应用转到后台)清除。始终与普通浏览器分开。",
|
||||
|
|
@ -856,6 +859,7 @@
|
|||
"다음 (N)": "下一个(N)",
|
||||
"다음 {0}개 기다림": "另有 {0} 个等待中",
|
||||
"다음 페이지": "下一页",
|
||||
"다음 항목 (→)": "下一项 (→)",
|
||||
"다음부터 묻지 않기 (환경 설정 > 실행 에서 바꿀 수 있음)": "下次不再询问(可在 设置 > 运行 中更改)",
|
||||
"단계": "步数",
|
||||
"단어 첫 글자만 크게 (Abc)": "单词首字母大写 (Abc)",
|
||||
|
|
@ -1071,7 +1075,9 @@
|
|||
"문제 {0}개 - 고치기 전에는 바꾸지 않습니다": "{0} 个问题 - 修正前不会重命名",
|
||||
"묻기": "询问",
|
||||
"물어보지 않기": "不询问",
|
||||
"미리 보기": "快速预览",
|
||||
"미리 볼 수 없습니다: {0}": "无法预览: {0}",
|
||||
"미리 볼 수 없음 (글자 코드가 UTF-8 이 아닙니다)": "无法预览 (文本不是 UTF-8)",
|
||||
"미리보기: 목록에서 동영상을 고르세요": "预览: 请在列表中选择视频",
|
||||
"미얀마어": "缅甸语",
|
||||
"미지정": "未指定",
|
||||
|
|
@ -1354,6 +1360,7 @@
|
|||
"세로 4:5": "竖屏 4:5",
|
||||
"세로 9:16": "竖屏 9:16",
|
||||
"세로 고정": "锁定竖屏",
|
||||
"소리": "音频",
|
||||
"속도 제한": "速度限制",
|
||||
"속도 제한 (KB/s)": "速度限制 (KB/s)",
|
||||
"속도 제한 {0} KB/s": "速度限制 {0} KB/s",
|
||||
|
|
@ -1455,6 +1462,7 @@
|
|||
"압축 파일 안에 없습니다": "压缩文件中没有",
|
||||
"압축 파일 안에 프로그램이 없습니다.": "压缩文件中没有程序。",
|
||||
"압축을 풀 수 없습니다: {0}": "无法解压: {0}",
|
||||
"앞 항목 (←)": "上一项 (←)",
|
||||
"앞으로": "前进",
|
||||
"앞의 rsync 가 끝난 뒤에 하세요.": "请等上一个 rsync 结束后再运行。",
|
||||
"앞의 버튼 보기": "前面的按钮",
|
||||
|
|
@ -1522,8 +1530,10 @@
|
|||
"연도": "年份",
|
||||
"열 수 없습니다: {0}": "无法打开: {0}",
|
||||
"열기": "打开",
|
||||
"열기 · 재생": "打开 · 播放",
|
||||
"영구 삭제": "永久删除",
|
||||
"영구 삭제 ({0}개, 되돌릴 수 없음)": "永久删除 ({0} 个,无法撤销)",
|
||||
"영상": "视频",
|
||||
"영상 {0}": "视频 {0}",
|
||||
"영상 · 음성 합치는 중": "正在合并视频 · 音频",
|
||||
"영상 재생 오류: {0}": "视频播放错误: {0}",
|
||||
|
|
@ -1868,6 +1878,7 @@
|
|||
"자막 이동 실패: {0} ({1})": "字幕移动失败: {0}({1})",
|
||||
"자막 저장: {0} ({1}, {2}개 줄){3}": "已保存字幕: {0}({1},{2} 行){3}",
|
||||
"자막 크기": "字幕大小",
|
||||
"자막 트랙 {0}개: {1}": "{0} 个字幕轨道: {1}",
|
||||
"자막 파일 선택": "选择字幕文件",
|
||||
"자막 파일 추가": "添加字幕文件",
|
||||
"자막 파일을 받을 수 없습니다 ({0})": "无法下载字幕文件({0})",
|
||||
|
|
@ -1969,6 +1980,8 @@
|
|||
"정말 되돌릴까요?": "确定要回退吗?",
|
||||
"정말 초기화할까요?": "确定要重置吗?",
|
||||
"정보": "信息",
|
||||
"정보를 읽는 중…": "正在读取信息…",
|
||||
"정보를 읽지 못했습니다": "无法读取信息",
|
||||
"정사각 1:1": "正方形 1:1",
|
||||
"정지": "停止",
|
||||
"정하기": "设置",
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import '../core/reader_sources.dart';
|
|||
import '../l10n/tr.dart';
|
||||
import 'explorer_bookmarks.dart';
|
||||
import 'explorer_search.dart';
|
||||
import 'quick_look.dart';
|
||||
import 'batch_rename_dialog.dart';
|
||||
import '../core/folder_size.dart';
|
||||
import '../core/batch_rename.dart';
|
||||
|
|
@ -1393,6 +1394,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
// 74: 폰에서 범위 고르기 - 첫 항목을 표시한 뒤 마지막 항목을 길게 눌러
|
||||
if (_selecting && _canRange(pane, e)) ('range', Icons.unfold_more, tr('여기까지 범위 고르기')),
|
||||
if (e.isDir) ('open', Icons.folder_open, tr('열기')),
|
||||
// 81: 빠른 보기 (PC 는 Space · TC 는 F3)
|
||||
if (!e.isDir) ('quickLook', Icons.preview_outlined, tr('미리 보기')),
|
||||
// 80: 이 폴더 크기를 지금 계산 (설정이 "안 함" 이어도)
|
||||
if (e.isDir) ('folderSize', Icons.data_usage, _sizes.busy(e.path) ? tr('폴더 크기 계산 멈추기') : tr('폴더 크기 계산')),
|
||||
if (e.isDir && dual) ('openOther', Icons.vertical_split_outlined, tr('다른 창에서 열기')),
|
||||
|
|
@ -1480,6 +1483,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
case 'select':
|
||||
setState(() => _selecting = true);
|
||||
if (!_isMarked(pane, e.path)) await _toggleMark(pane, e);
|
||||
case 'quickLook':
|
||||
await _quickLook(pane, e.path);
|
||||
case 'folderSize':
|
||||
if (_sizes.busy(e.path)) {
|
||||
_sizes.cancel(e.path);
|
||||
|
|
@ -3626,9 +3631,13 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
// 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),
|
||||
|
|
@ -3640,6 +3649,26 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
};
|
||||
}
|
||||
|
||||
/// 81: 빠른 보기 - 지금 줄 (없으면 아무것도 안 함). 같은 폴더에 보이는 항목을 ← → 로
|
||||
Future<void> _quickLook(_Pane pane, [String? path]) async {
|
||||
final f = path ?? pane.focused;
|
||||
if (f == null || samePath(f, pane.root)) return;
|
||||
final list = _shown(pane, vDirname(f));
|
||||
final i = list.indexWhere((x) => samePath(x.path, f));
|
||||
if (i < 0) return;
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => QuickLook(
|
||||
entries: list,
|
||||
index: i,
|
||||
probe: c.services.mediaTool.probe,
|
||||
thumbnail: (e) => _BigThumbs.of(c, e),
|
||||
imageExts: c.settings.imageExts,
|
||||
onOpen: (e) => e.isDir ? unawaited(_goTo(pane, e.path)) : unawaited(_open(e.path, pane: pane)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 83: (TC) 지금 줄을 표시 · 풀기, [next] 면 다음 줄로
|
||||
Future<void> _markFocused({required bool next}) async {
|
||||
final pane = _pane;
|
||||
|
|
@ -3677,8 +3706,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
final ch = ev.character;
|
||||
final kb = HardwareKeyboard.instance;
|
||||
if (ch == null || ch.isEmpty || ch.runes.first < 0x20 || ch.runes.first == 0x7f) return KeyEventResult.ignored;
|
||||
// 83: TC 모양에서 Space 는 표시 (빠른 필터가 아니라)
|
||||
if (ch == ' ' && _tcKeys && !_panes[_active].filterOpen) return KeyEventResult.ignored;
|
||||
// 83 · 81: Space 는 표시 (TC) · 빠른 보기 (그 밖) - 빠른 필터를 열지 않는다 (필터가 열려 있으면 그대로 글자)
|
||||
if (ch == ' ' && !widget.rsync && !_panes[_active].filterOpen) return KeyEventResult.ignored;
|
||||
if (kb.isControlPressed || kb.isAltPressed || kb.isMetaPressed) return KeyEventResult.ignored;
|
||||
// 글자 칸 (필터 · 경로 입력 등) 에 치는 중이면 그 칸이 받는다
|
||||
if (_typing) return KeyEventResult.ignored;
|
||||
|
|
@ -4435,6 +4464,33 @@ class _Thumbs {
|
|||
}
|
||||
}
|
||||
|
||||
/// 81: 빠른 보기의 큰 썸네일 (가로 640, 같은 jj_thumbs 에 따로 저장). 사용자가 연 것이라 작업 중에도 만든다
|
||||
class _BigThumbs {
|
||||
static final _cache = <String, Future<File?>>{};
|
||||
static final _q = _Queue(1);
|
||||
|
||||
static Future<File?> of(AppController c, FileEntry e) {
|
||||
final key = '${e.path}|${e.size}|${e.modified.millisecondsSinceEpoch}|640';
|
||||
return _cache[key] ??= _q.run(() async {
|
||||
try {
|
||||
final dir = Directory(p.join(await c.services.storage.tempDirectory(), 'jj_thumbs'));
|
||||
await dir.create(recursive: true);
|
||||
final out = File(p.join(dir.path, '${md5.convert(utf8.encode(key))}.jpg'));
|
||||
if (await out.exists() && await out.length() > 0) return out;
|
||||
for (final at in ['5', '0']) {
|
||||
await c.services.mediaTool.runFfmpeg([
|
||||
'-hide_banner', '-loglevel', 'error', '-ss', at, '-i', e.path, //
|
||||
'-frames:v', '1', '-vf', 'scale=640:-2', '-y', out.path,
|
||||
]);
|
||||
if (await out.exists() && await out.length() > 0) return out;
|
||||
}
|
||||
} catch (_) {}
|
||||
_cache.remove(key);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _Thumb extends StatelessWidget {
|
||||
final AppController c;
|
||||
final FileEntry entry;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,216 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../core/file_ops.dart';
|
||||
import '../core/models.dart';
|
||||
import '../core/playlist.dart' show isAudioFile, isVideoFile;
|
||||
import '../core/vfs.dart';
|
||||
import '../l10n/tr.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
/// 81: 빠른 보기에서 글로 보는 확장자
|
||||
const textExtensions = ['txt', 'srt', 'ass', 'ssa', 'vtt', 'smi', 'log', 'md', 'json', 'xml', 'csv', 'ini', 'nfo'];
|
||||
|
||||
/// 81: 글 파일의 앞 64KB (UTF-8). 글자 코드가 깨졌으면 null - 끝에서 잘린 한 글자는 깨진 것으로 치지 않는다
|
||||
Future<String?> readTextHead(String path, {int limit = 64 * 1024}) async {
|
||||
final bytes = <int>[];
|
||||
await for (final chunk in File(path).openRead(0, limit)) {
|
||||
bytes.addAll(chunk);
|
||||
}
|
||||
final text = const Utf8Decoder(allowMalformed: true).convert(bytes);
|
||||
final bad = '<EFBFBD>'.allMatches(text).length;
|
||||
final cut = bytes.length >= limit && text.endsWith('<EFBFBD>') ? 1 : 0;
|
||||
return bad - cut > 0 ? null : (cut > 0 ? text.substring(0, text.length - 1) : text);
|
||||
}
|
||||
|
||||
/// 81: 빠른 보기 (Space · TC 는 F3 · 길게 누르기 [미리 보기]) - 동영상은 큰 썸네일 · 길이 · 해상도 · 코덱, 그림은 그림,
|
||||
/// 작은 글 파일은 앞부분, 그 밖은 크기 · 날짜. ← → 로 같은 폴더의 앞 · 다음 항목, Esc · Space 로 닫기, [열기].
|
||||
/// 파일을 바꾸지 않는다 (읽기만).
|
||||
class QuickLook extends StatefulWidget {
|
||||
/// 같은 폴더에 보이는 항목 (차례대로) · 처음 보일 것
|
||||
final List<FileEntry> entries;
|
||||
final int index;
|
||||
|
||||
/// 동영상 정보 (FFprobe) · 큰 썸네일 (없으면 null)
|
||||
final Future<MediaInfo> Function(String path) probe;
|
||||
final Future<File?> Function(FileEntry e) thumbnail;
|
||||
|
||||
/// 그림으로 볼 확장자
|
||||
final List<String> imageExts;
|
||||
|
||||
/// [열기] (닫은 뒤 탐색기가 연다)
|
||||
final void Function(FileEntry e) onOpen;
|
||||
|
||||
const QuickLook({
|
||||
super.key,
|
||||
required this.entries,
|
||||
required this.index,
|
||||
required this.probe,
|
||||
required this.thumbnail,
|
||||
required this.imageExts,
|
||||
required this.onOpen,
|
||||
});
|
||||
|
||||
@override
|
||||
State<QuickLook> createState() => _QuickLookState();
|
||||
}
|
||||
|
||||
class _QuickLookState extends State<QuickLook> {
|
||||
late int _i = widget.index;
|
||||
final _focus = FocusNode(debugLabel: 'quickLook');
|
||||
|
||||
FileEntry get _e => widget.entries[_i];
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _go(int d) {
|
||||
final n = _i + d;
|
||||
if (n < 0 || n >= widget.entries.length) return;
|
||||
setState(() => _i = n);
|
||||
}
|
||||
|
||||
static String _date(DateTime d) {
|
||||
String two(int x) => x.toString().padLeft(2, '0');
|
||||
return '${d.year}-${two(d.month)}-${two(d.day)} ${two(d.hour)}:${two(d.minute)}';
|
||||
}
|
||||
|
||||
static String _dur(Duration d) {
|
||||
final h = d.inHours, m = d.inMinutes % 60, s = d.inSeconds % 60;
|
||||
String two(int x) => x.toString().padLeft(2, '0');
|
||||
return h > 0 ? '$h:${two(m)}:${two(s)}' : '$m:${two(s)}';
|
||||
}
|
||||
|
||||
Widget _body(FileEntry e) {
|
||||
final dav = isDav(e.path);
|
||||
if (e.isDir) return _info([tr('폴더')]);
|
||||
if (!dav && (isVideoFile(e.path) || isAudioFile(e.path))) return _media(e);
|
||||
if (!dav && widget.imageExts.contains(e.ext)) {
|
||||
return Image.file(File(e.path), fit: BoxFit.contain, errorBuilder: (_, _, _) => _info([tr('그림을 열 수 없습니다')]));
|
||||
}
|
||||
// 관리자 판단: 글 파일은 앞 64KB 만, 글자 코드가 깨지면 "미리 볼 수 없음"
|
||||
if (!dav && e.size > 0 && textExtensions.contains(e.ext)) {
|
||||
return FutureBuilder<String?>(
|
||||
future: readTextHead(e.path),
|
||||
builder: (_, s) => s.connectionState != ConnectionState.done
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: s.data == null
|
||||
? _info([tr('미리 볼 수 없음 (글자 코드가 UTF-8 이 아닙니다)')])
|
||||
: SingleChildScrollView(
|
||||
child: SelectableText(s.data!, style: const TextStyle(fontFamily: 'Consolas', fontSize: 12)),
|
||||
),
|
||||
);
|
||||
}
|
||||
return _info([if (dav) tr('WebDAV 파일은 이름 · 크기 · 날짜만 봅니다')]);
|
||||
}
|
||||
|
||||
Widget _info(List<String> lines) => Center(
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Icon(_e.isDir ? Icons.folder : Icons.insert_drive_file_outlined, size: 56, color: JjColors.textDim),
|
||||
for (final l in lines) Text(l, textAlign: TextAlign.center, style: const TextStyle(color: JjColors.textDim)),
|
||||
]),
|
||||
);
|
||||
|
||||
Widget _media(FileEntry e) => Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
|
||||
if (isVideoFile(e.path))
|
||||
Expanded(
|
||||
child: FutureBuilder<File?>(
|
||||
future: widget.thumbnail(e),
|
||||
builder: (_, s) => s.connectionState != ConnectionState.done
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: s.data == null
|
||||
? const Center(child: Icon(Icons.movie_outlined, size: 56, color: JjColors.textDim))
|
||||
: Image.file(s.data!, fit: BoxFit.contain),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
FutureBuilder<MediaInfo>(
|
||||
// 큰 파일 · 느린 디스크에서도 창이 멈추지 않게 (기다리는 동안 "읽는 중", 20초 넘으면 못 읽음)
|
||||
future: widget.probe(e.path).timeout(const Duration(seconds: 20)),
|
||||
builder: (_, s) {
|
||||
if (s.connectionState != ConnectionState.done) return Text(tr('정보를 읽는 중…'), style: const TextStyle(color: JjColors.textDim));
|
||||
final info = s.data;
|
||||
if (info == null) return Text(tr('정보를 읽지 못했습니다'), style: const TextStyle(color: JjColors.textDim));
|
||||
final v = info.ofType('video').where((x) => (x.width ?? 0) > 0).firstOrNull;
|
||||
final audio = info.ofType('audio');
|
||||
final subs = info.ofType('subtitle');
|
||||
return SelectableText(
|
||||
[
|
||||
if (info.duration != null) '${tr('길이')}: ${_dur(info.duration!)}',
|
||||
if (v != null) '${tr('영상')}: ${v.codec} · ${v.width}×${v.height}',
|
||||
if (audio.isNotEmpty)
|
||||
'${tr('소리')}: ${audio.map((a) => [a.codec, if (a.language.isNotEmpty) a.language].join(' ')).join(', ')}',
|
||||
if (subs.isNotEmpty)
|
||||
trf('자막 트랙 {0}개: {1}', [subs.length, subs.map((x) => [x.codec, if (x.language.isNotEmpty) x.language].join(' ')).join(', ')]),
|
||||
].join('\n'),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
);
|
||||
},
|
||||
),
|
||||
]);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final e = _e;
|
||||
return CallbackShortcuts(
|
||||
bindings: {
|
||||
const SingleActivator(LogicalKeyboardKey.arrowLeft): () => _go(-1),
|
||||
const SingleActivator(LogicalKeyboardKey.arrowRight): () => _go(1),
|
||||
const SingleActivator(LogicalKeyboardKey.arrowUp): () => _go(-1),
|
||||
const SingleActivator(LogicalKeyboardKey.arrowDown): () => _go(1),
|
||||
const SingleActivator(LogicalKeyboardKey.space): () => Navigator.pop(context),
|
||||
const SingleActivator(LogicalKeyboardKey.enter): () {
|
||||
Navigator.pop(context);
|
||||
widget.onOpen(e);
|
||||
},
|
||||
},
|
||||
child: Focus(
|
||||
focusNode: _focus,
|
||||
autofocus: true,
|
||||
child: AlertDialog(
|
||||
titlePadding: const EdgeInsets.fromLTRB(16, 12, 8, 0),
|
||||
title: Row(children: [
|
||||
Expanded(child: Text(e.name, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 16))),
|
||||
IconButton(tooltip: tr('앞 항목 (←)'), onPressed: _i > 0 ? () => _go(-1) : null, icon: const Icon(Icons.chevron_left)),
|
||||
Text('${_i + 1}/${widget.entries.length}', style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
IconButton(
|
||||
tooltip: tr('다음 항목 (→)'),
|
||||
onPressed: _i < widget.entries.length - 1 ? () => _go(1) : null,
|
||||
icon: const Icon(Icons.chevron_right)),
|
||||
]),
|
||||
content: SizedBox(
|
||||
width: 640,
|
||||
height: 420,
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
|
||||
Text(
|
||||
[if (!e.isDir) formatSize(e.size), if (e.modified.year > 1) _date(e.modified), vDirname(e.path)].join(' · '),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(child: KeyedSubtree(key: ValueKey(e.path), child: _body(e))),
|
||||
]),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: Text(tr('닫기'))),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
widget.onOpen(e);
|
||||
},
|
||||
child: Text(e.isDir ? tr('열기') : tr('열기 · 재생')),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ import 'package:jj_mkvmaker/services/platform_services.dart';
|
|||
import 'package:jj_mkvmaker/core/file_ops.dart' show formatSize;
|
||||
import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
||||
import 'package:jj_mkvmaker/ui/explorer_search.dart';
|
||||
import 'package:jj_mkvmaker/ui/quick_look.dart';
|
||||
import 'package:jj_mkvmaker/ui/path_label.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'support/fake_recycle_bin.dart';
|
||||
|
|
@ -1013,6 +1014,42 @@ void main() {
|
|||
expect(find.textContaining('담았습니다'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('81: Space 로 빠른 보기 (글 파일 앞부분) · 필터에 치는 중에는 그냥 빈칸 · TC 는 F3', (tester) async {
|
||||
c.settings.explorerStyle = 'windows';
|
||||
await open(tester);
|
||||
await act(tester, () => tester.tap(find.text('doc.txt')));
|
||||
await settle(tester, () => false, rounds: 15);
|
||||
await act(tester, () => tester.sendKeyEvent(LogicalKeyboardKey.space));
|
||||
await settle(tester, () => find.byType(QuickLook).evaluate().isNotEmpty);
|
||||
expect(find.byType(QuickLook), findsOneWidget);
|
||||
await settle(tester, () => find.text('hello').evaluate().isNotEmpty);
|
||||
expect(find.text('hello'), findsOneWidget, reason: '글 파일 앞부분');
|
||||
await act(tester, () => tester.sendKeyEvent(LogicalKeyboardKey.escape));
|
||||
await calm(tester);
|
||||
expect(find.byType(QuickLook), findsNothing);
|
||||
|
||||
// 빠른 필터에 치는 중이면 Space 는 칸의 글자 (빠른 보기를 열지 않음)
|
||||
await act(tester, () => tester.sendKeyEvent(LogicalKeyboardKey.keyD));
|
||||
await calm(tester);
|
||||
expect(find.byType(TextField), findsOneWidget);
|
||||
await act(tester, () => tester.sendKeyEvent(LogicalKeyboardKey.space));
|
||||
await calm(tester);
|
||||
expect(find.byType(QuickLook), findsNothing);
|
||||
await act(tester, () => tester.sendKeyEvent(LogicalKeyboardKey.escape));
|
||||
await calm(tester);
|
||||
|
||||
// TC: F3
|
||||
await c.updateSettings((s) => s.explorerStyle = 'totalcmd');
|
||||
await calm(tester);
|
||||
await act(tester, () => tester.tap(find.text('doc')));
|
||||
await settle(tester, () => false, rounds: 15);
|
||||
await act(tester, () => tester.sendKeyEvent(LogicalKeyboardKey.f3));
|
||||
await settle(tester, () => find.byType(QuickLook).evaluate().isNotEmpty);
|
||||
expect(find.byType(QuickLook), findsOneWidget);
|
||||
await act(tester, () => tester.sendKeyEvent(LogicalKeyboardKey.escape));
|
||||
await calm(tester);
|
||||
});
|
||||
|
||||
// P0 (15 지적 10/17): 글자 칸에 치는 중에는 파일 단축키가 키를 가로채지 않는다 (예전: 필터 칸의 Delete → 파일 지우기 창)
|
||||
for (final style in const ['windows', 'xplore', 'totalcmd']) {
|
||||
testWidgets('P0 $style: 빠른 필터 칸에서 Ctrl+A · C · V · Delete · Space · F8 · Backspace 는 파일에 쓰이지 않는다', (tester) async {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jj_mkvmaker/core/file_ops.dart';
|
||||
import 'package:jj_mkvmaker/core/models.dart';
|
||||
import 'package:jj_mkvmaker/ui/quick_look.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
/// 81: 빠른 보기 - 글 파일은 앞 64KB (깨지면 "미리 볼 수 없음"), ← → 로 앞 · 다음, Space 로 닫기, [열기]
|
||||
void main() {
|
||||
late Directory tmp;
|
||||
setUp(() => tmp = Directory.systemTemp.createTempSync('jj_ql81_'));
|
||||
tearDown(() => tmp.deleteSync(recursive: true));
|
||||
|
||||
test('글 앞부분: UTF-8 은 그대로 · 다른 글자 코드 (CP949) 는 null · 64KB 끝에서 잘린 한 글자는 괜찮음', () async {
|
||||
final a = File(p.join(tmp.path, 'a.txt'))..writeAsStringSync('안녕 hello');
|
||||
expect(await readTextHead(a.path), '안녕 hello');
|
||||
final b = File(p.join(tmp.path, 'b.smi'))..writeAsBytesSync([0xBE, 0xC8, 0xB3, 0xE7]); // CP949 "안녕"
|
||||
expect(await readTextHead(b.path), isNull);
|
||||
// 64KB 경계에 3바이트 글자가 걸리게
|
||||
final c = File(p.join(tmp.path, 'c.txt'))..writeAsStringSync('${'x' * (64 * 1024 - 1)}가나다');
|
||||
final head = await readTextHead(c.path);
|
||||
expect(head, isNotNull);
|
||||
expect(head!.length, 64 * 1024 - 1);
|
||||
});
|
||||
|
||||
testWidgets('← → 로 앞 · 다음 (보이는 차례), Space 로 닫기, [열기]', (tester) async {
|
||||
tester.view.physicalSize = const Size(1000, 800);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final now = DateTime(2026, 10, 17);
|
||||
final entries = [
|
||||
FileEntry(p.join(tmp.path, 'one.bin'), isDir: false, size: 10, modified: now),
|
||||
FileEntry(p.join(tmp.path, 'two.bin'), isDir: false, size: 20, modified: now),
|
||||
];
|
||||
final opened = <String>[];
|
||||
final nav = GlobalKey<NavigatorState>();
|
||||
await tester.pumpWidget(MaterialApp(navigatorKey: nav, home: const Scaffold()));
|
||||
showDialog<void>(
|
||||
context: nav.currentContext!,
|
||||
builder: (_) => QuickLook(
|
||||
entries: entries,
|
||||
index: 0,
|
||||
probe: (_) async => const MediaInfo(streams: []),
|
||||
thumbnail: (_) async => null,
|
||||
imageExts: const ['jpg'],
|
||||
onOpen: (e) => opened.add(e.name),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('one.bin'), findsOneWidget);
|
||||
expect(find.text('1/2'), findsOneWidget);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('two.bin'), findsOneWidget);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('2/2'), findsOneWidget, reason: '끝에서 더 가지 않음');
|
||||
await tester.tap(find.text('열기 · 재생'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(opened, ['two.bin']);
|
||||
expect(find.byType(QuickLook), findsNothing);
|
||||
|
||||
showDialog<void>(
|
||||
context: nav.currentContext!,
|
||||
builder: (_) => QuickLook(
|
||||
entries: entries,
|
||||
index: 1,
|
||||
probe: (_) async => const MediaInfo(streams: []),
|
||||
thumbnail: (_) async => null,
|
||||
imageExts: const ['jpg'],
|
||||
onOpen: (_) {},
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(QuickLook), findsNothing, reason: 'Space 로 닫기');
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue