WIP X-plore g · h: 격자 보기 · 글 뷰어 (검토 대기)

- g: 폴더 길게 누르기 메뉴 [격자로 보기 (그림 · 동영상)] - 썸네일 격자 (크게 · 작게), 누르면 열기 (보기 · 재생),
  칸을 길게 누르면 탐색기에서 그 자리로. WebDAV · SFTP 폴더는 메뉴에 없음 (썸네일을 받아야 해서).
- h: 파일 메뉴 [글 뷰어로 보기] (동영상 · 소리 빼고 어떤 파일이든, 읽기 전용) - 글자 코드 스스로 판별
  (UTF-8 · UTF-16 · CP949 · Shift-JIS · GBK, 위에서 바꿈), 큰 파일은 256KB 씩 [더 읽기], 줄 번호 · 글자 크기 · 전체 복사 · 고르기.
  나눠 읽은 끝에서 두 바이트 글자가 잘려 CP949 풀이가 실패하던 것은 끝 바이트를 다음 조각까지 미뤄 고침.
- 내 판단: 글 파일 (txt · srt · log 등) 을 열 앱이 없을 때만 글 뷰어로 연다 (예전에는 "열 앱이 없습니다").
  기본 앱이 있으면 예전처럼 그 앱으로 (사용자가 보는 동작은 바꾸지 않음).
- 내 판단 (캡처에서 발견): 좁은 화면 (폭 600 미만) 의 글 뷰어는 줄 번호 · 글자 크기를 ⋮ 메뉴로 (412 에서 파일 이름이 다 가려졌다).
- 번역: 이 커밋 뒤의 i · 저장 공간 그림 글도 같이 넣음 (en · ja · zh-Hans).
- 시험: xplore_tools_test (글 뷰어 CP949 나눠 읽기 · 격자), explorer_test "X-plore g · h".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xplore
RyuJongJin 2026-10-18 00:55:27 +09:00
parent a80d8bcafe
commit 78486be27a
8 changed files with 483 additions and 1 deletions

View File

@ -381,6 +381,7 @@
"ZIP 이 있는 폴더로": "To the ZIP's folder", "ZIP 이 있는 폴더로": "To the ZIP's folder",
"ZIP 이름": "ZIP name", "ZIP 이름": "ZIP name",
"[N] 원래 이름 · [C] 번호 예: Show S01E[C]": "[N] original name · [C] counter e.g. Show S01E[C]", "[N] 원래 이름 · [C] 번호 예: Show S01E[C]": "[N] original name · [C] counter e.g. Show S01E[C]",
"[N] 원래 이름 · [C] 번호 · [D] 날짜 · [T] 시각 · 확장자는 그대로 예: Show S01E[C]": "[N] original name · [C] counter · [D] date · [T] time · extension kept e.g. Show S01E[C]",
"[N] 원래 이름 · [C] 번호 · 확장자는 그대로 예: Show S01E[C]": "[N] original name · [C] counter · extension kept e.g. Show S01E[C]", "[N] 원래 이름 · [C] 번호 · 확장자는 그대로 예: Show S01E[C]": "[N] original name · [C] counter · extension kept e.g. Show S01E[C]",
"[서버 설정 고치기] 에서 비밀번호를 넣으면 이 기기의 안전 저장소에 남아 다음부터는 다시 넣지 않아도 됩니다.": "Enter the password in [Edit server settings]. It is kept in this device's secure storage, so you won't need to enter it again.", "[서버 설정 고치기] 에서 비밀번호를 넣으면 이 기기의 안전 저장소에 남아 다음부터는 다시 넣지 않아도 됩니다.": "Enter the password in [Edit server settings]. It is kept in this device's secure storage, so you won't need to enter it again.",
"[서버 설정 고치기] 에서 아이디 · 비밀번호를 확인하세요.": "Check the user name and password in [Edit server settings].", "[서버 설정 고치기] 에서 아이디 · 비밀번호를 확인하세요.": "Check the user name and password in [Edit server settings].",
@ -444,6 +445,7 @@
"{0} / {1}장": "{0} / {1} images", "{0} / {1}장": "{0} / {1} images",
"{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2} (change at the top of the main screen)", "{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2} (change at the top of the main screen)",
"{0} · {1}배 · {2}": "{0} · {1}× · {2}", "{0} · {1}배 · {2}": "{0} · {1}× · {2}",
"{0} · 그림 · 동영상 {1}개": "{0} · {1} pictures / videos",
"{0} · 링크 {1}개 제외": "{0} · {1} links excluded", "{0} · 링크 {1}개 제외": "{0} · {1} links excluded",
"{0} · 파일 {1}개": "{0} · {1} files", "{0} · 파일 {1}개": "{0} · {1} files",
"{0} → {1} 번역 {2}%": "{0} → {1} translation {2}%", "{0} → {1} 번역 {2}%": "{0} → {1} translation {2}%",
@ -630,6 +632,7 @@
"검색": "Search", "검색": "Search",
"검색 지우기": "Clear search", "검색 지우기": "Clear search",
"검정": "Black", "검정": "Black",
"격자로 보기 (그림 · 동영상)": "Grid view (pictures / videos)",
"결과 폴더": "Output folder", "결과 폴더": "Output folder",
"결과 폴더 열기": "Open output folder", "결과 폴더 열기": "Open output folder",
"결과 폴더 열기 (동영상을 고르세요)": "Open output folder (choose a video)", "결과 폴더 열기 (동영상을 고르세요)": "Open output folder (choose a video)",
@ -716,8 +719,11 @@
"그중 {0}개는 받는 쪽이 더 새것입니다 - 더 새 파일을 예전 것으로 덮어씁니다": "{0} of them are newer on the receiving side - the newer files will be overwritten with older ones", "그중 {0}개는 받는 쪽이 더 새것입니다 - 더 새 파일을 예전 것으로 덮어씁니다": "{0} of them are newer on the receiving side - the newer files will be overwritten with older ones",
"그중 폴더 {0}개 - 안의 파일 · 폴더도 모두 함께": "Including {0} folders - everything inside them too", "그중 폴더 {0}개 - 안의 파일 · 폴더도 모두 함께": "Including {0} folders - everything inside them too",
"글 → 그림 · 그림 → 그림 (기기 안에서, 모델은 환경 설정에서 받기)": "Text → image · image → image (on this device; download models in Settings)", "글 → 그림 · 그림 → 그림 (기기 안에서, 모델은 환경 설정에서 받기)": "Text → image · image → image (on this device; download models in Settings)",
"글 뷰어로 보기": "View as text",
"글을 격자에 반영": "Apply text to grid", "글을 격자에 반영": "Apply text to grid",
"글자 · 버튼 크기입니다. 위쪽 막대의 − · + 로 그때그때 바꿀 수 있고, 가운데 숫자를 누르면 이 크기로 돌아갑니다 (지금 {0}%)": "Text · button size. Change it any time with − · + on the top bar; press the number in the middle to return to this size (now {0}%)", "글자 · 버튼 크기입니다. 위쪽 막대의 − · + 로 그때그때 바꿀 수 있고, 가운데 숫자를 누르면 이 크기로 돌아갑니다 (지금 {0}%)": "Text · button size. Change it any time with − · + on the top bar; press the number in the middle to return to this size (now {0}%)",
"글자 작게": "Smaller text",
"글자 크게": "Larger text",
"금": "Fri", "금": "Fri",
"기계 번역": "Machine translation", "기계 번역": "Machine translation",
"기기": "device", "기기": "device",
@ -772,10 +778,13 @@
"끝내기 (작업 멈춤)": "Exit (stop tasks)", "끝내기 (작업 멈춤)": "Exit (stop tasks)",
"끝에서 처음으로 돌기": "Wrap around at the end", "끝에서 처음으로 돌기": "Wrap around at the end",
"나가기": "Exit", "나가기": "Exit",
"나머지 폴더 {0}개": "{0} other folders",
"나머지는 대기했다가 차례로 받습니다": "The rest wait and download in turn", "나머지는 대기했다가 차례로 받습니다": "The rest wait and download in turn",
"나머지는 지웠습니다.": "The rest were deleted.", "나머지는 지웠습니다.": "The rest were deleted.",
"나중에": "Later", "나중에": "Later",
"날짜": "Date", "날짜": "Date",
"날짜: 바뀐 때": "Date: modified",
"날짜: 찍은 때 (사진, 없으면 바뀐 때)": "Date: taken (photos; otherwise modified)",
"남는 것": "What stays", "남는 것": "What stays",
"남은 {0}장": "{0} left", "남은 {0}장": "{0} left",
"남은 것 모두 취소 ({0}개)": "Cancel all remaining ({0})", "남은 것 모두 취소 ({0}개)": "Cancel all remaining ({0})",
@ -1335,6 +1344,7 @@
"비어 있는 압축 파일입니다.": "The archive is empty.", "비어 있는 압축 파일입니다.": "The archive is empty.",
"비어 있는 폴더": "Empty folder", "비어 있는 폴더": "Empty folder",
"비어 있는 폴더입니다.\n위의 [즐겨찾기 추가] · [새 폴더] 를 누르거나, 즐겨찾기를 왼쪽 폴더로 끌어다 놓으세요.": "This folder is empty.\nPress [Add bookmark] · [New folder] above, or drag bookmarks onto a folder on the left.", "비어 있는 폴더입니다.\n위의 [즐겨찾기 추가] · [새 폴더] 를 누르거나, 즐겨찾기를 왼쪽 폴더로 끌어다 놓으세요.": "This folder is empty.\nPress [Add bookmark] · [New folder] above, or drag bookmarks onto a folder on the left.",
"비어 있습니다": "Empty",
"비어 있음": "Empty", "비어 있음": "Empty",
"비우기": "Empty", "비우기": "Empty",
"비우면 지움": "Empty = remove", "비우면 지움": "Empty = remove",
@ -1863,8 +1873,10 @@
"이 폴더에 새 폴더": "New folder in this folder", "이 폴더에 새 폴더": "New folder in this folder",
"이 폴더에 현재 페이지 추가": "Add the current page to this folder", "이 폴더에 현재 페이지 추가": "Add the current page to this folder",
"이 폴더에는 고를 파일이 없습니다": "There are no files to choose in this folder", "이 폴더에는 고를 파일이 없습니다": "There are no files to choose in this folder",
"이 폴더에는 그림 · 동영상이 없습니다": "No pictures or videos in this folder",
"이 폴더에만: {0}": "This folder only: {0}", "이 폴더에만: {0}": "This folder only: {0}",
"이 폴더의 동영상 재생": "Play videos in this folder", "이 폴더의 동영상 재생": "Play videos in this folder",
"이 폴더의 파일": "Files in this folder",
"이 프로그램이 관리자 권한으로 실행 중입니다. 권장하지 않습니다.\n\n· 내장 Chrome 엔진은 샌드박스 (웹 페이지를 가두는 보호) 가 꺼져 있어, 웹 페이지가 관리자 권한 프로세스 안에서 돕니다. 페이지의 취약점이 곧 이 PC 의 관리자 권한이 될 수 있습니다.\n· Edge 엔진은 이 영향이 없습니다.\n· 관리자 권한 없이 프로그램을 실행하면 이 설정 없이도 Chrome 엔진을 쓸 수 있습니다.": "This app is running as administrator. This is not recommended.\n\n· The built-in Chrome engine has its sandbox (the protection that confines web pages) turned off, so web pages run inside an administrator process. A flaw in a page could become administrator access to this PC.\n· The Edge engine is not affected.\n· If you run the app without administrator rights, you can use the Chrome engine without this setting.", "이 프로그램이 관리자 권한으로 실행 중입니다. 권장하지 않습니다.\n\n· 내장 Chrome 엔진은 샌드박스 (웹 페이지를 가두는 보호) 가 꺼져 있어, 웹 페이지가 관리자 권한 프로세스 안에서 돕니다. 페이지의 취약점이 곧 이 PC 의 관리자 권한이 될 수 있습니다.\n· Edge 엔진은 이 영향이 없습니다.\n· 관리자 권한 없이 프로그램을 실행하면 이 설정 없이도 Chrome 엔진을 쓸 수 있습니다.": "This app is running as administrator. This is not recommended.\n\n· The built-in Chrome engine has its sandbox (the protection that confines web pages) turned off, so web pages run inside an administrator process. A flaw in a page could become administrator access to this PC.\n· The Edge engine is not affected.\n· If you run the app without administrator rights, you can use the Chrome engine without this setting.",
"이 형식을 여는 앱이 없습니다. 길게 누르거나 오른쪽 클릭 → [내장 플레이어로 재생] 으로 들어 보세요.": "No app opens this format. Long-press or right-click → [Play in built-in player] to try it.", "이 형식을 여는 앱이 없습니다. 길게 누르거나 오른쪽 클릭 → [내장 플레이어로 재생] 으로 들어 보세요.": "No app opens this format. Long-press or right-click → [Play in built-in player] to try it.",
"이 화면은 앱 안 웹 뷰의 연결을 Tor 로 보내는 것이며, 공식 Tor Browser 가 아닙니다.\n\n· 연결 (이름 찾기 포함) 은 모두 Tor 로 나가고, 쿠키 · 기록 · 캐시는 일반 브라우저와 따로 두며 닫으면 지웁니다 (환경 설정에서 남기기 가능).\n· 하지만 브라우저 지문 (화면 크기 · 글꼴 · 기기 정보 등) 을 감추는 보호는 공식 Tor Browser 보다 약합니다. 사이트가 이 기기를 알아볼 수 있습니다.\n· 강한 익명성이 필요하면 공식 Tor Browser (https://www.torproject.org) 를 쓰세요.": "This screen sends the in-app web view's connections through Tor; it is not the official Tor Browser.\n\n· All connections (including name lookups) go out through Tor, and cookies, history and cache are kept separate from the normal browser and cleared when closed (can be kept in Settings).\n· However, protection against browser fingerprinting (screen size, fonts, device information, etc.) is weaker than in the official Tor Browser. Sites may recognize this device.\n· If you need strong anonymity, use the official Tor Browser (https://www.torproject.org).", "이 화면은 앱 안 웹 뷰의 연결을 Tor 로 보내는 것이며, 공식 Tor Browser 가 아닙니다.\n\n· 연결 (이름 찾기 포함) 은 모두 Tor 로 나가고, 쿠키 · 기록 · 캐시는 일반 브라우저와 따로 두며 닫으면 지웁니다 (환경 설정에서 남기기 가능).\n· 하지만 브라우저 지문 (화면 크기 · 글꼴 · 기기 정보 등) 을 감추는 보호는 공식 Tor Browser 보다 약합니다. 사이트가 이 기기를 알아볼 수 있습니다.\n· 강한 익명성이 필요하면 공식 Tor Browser (https://www.torproject.org) 를 쓰세요.": "This screen sends the in-app web view's connections through Tor; it is not the official Tor Browser.\n\n· All connections (including name lookups) go out through Tor, and cookies, history and cache are kept separate from the normal browser and cleared when closed (can be kept in Settings).\n· However, protection against browser fingerprinting (screen size, fonts, device information, etc.) is weaker than in the official Tor Browser. Sites may recognize this device.\n· If you need strong anonymity, use the official Tor Browser (https://www.torproject.org).",
@ -2081,6 +2093,7 @@
"전체 {0}개 중 {1}번째": "Item {1} of {0}", "전체 {0}개 중 {1}번째": "Item {1} of {0}",
"전체 만들기": "Make all", "전체 만들기": "Make all",
"전체 목록": "Full list", "전체 목록": "Full list",
"전체 복사": "Copy all",
"전체 선택": "Select all", "전체 선택": "Select all",
"전체 이동": "Move all", "전체 이동": "Move all",
"전체 화면": "Full screen", "전체 화면": "Full screen",
@ -2142,6 +2155,7 @@
"준비 중": "Preparing", "준비 중": "Preparing",
"준비 중…": "Preparing…", "준비 중…": "Preparing…",
"준비하는 중": "Preparing", "준비하는 중": "Preparing",
"줄 번호": "Line numbers",
"줄 삭제": "Delete line", "줄 삭제": "Delete line",
"중국어": "Chinese", "중국어": "Chinese",
"중국어 간체": "Chinese (Simplified)", "중국어 간체": "Chinese (Simplified)",
@ -2308,6 +2322,7 @@
"취소했습니다. 풀던 파일은 남지 않습니다 (다 푼 파일은 그대로).": "Cancelled. The file being extracted is not left behind (finished files remain).", "취소했습니다. 풀던 파일은 남지 않습니다 (다 푼 파일은 그대로).": "Cancelled. The file being extracted is not left behind (finished files remain).",
"칠한 시간에만 동기화하고, 그 밖의 시간에는 바뀐 것을 모아 두었다가 다음 시작 시간에 맞춥니다. 칸을 누르거나 끌어서 칠합니다 (요일 이름 · 시 숫자를 누르면 그 줄 전체).": "Syncs only in the painted hours; changes outside are collected and synced at the next start. Tap or drag cells to paint (tap a weekday name · hour number for the whole row · column).", "칠한 시간에만 동기화하고, 그 밖의 시간에는 바뀐 것을 모아 두었다가 다음 시작 시간에 맞춥니다. 칸을 누르거나 끌어서 칠합니다 (요일 이름 · 시 숫자를 누르면 그 줄 전체).": "Syncs only in the painted hours; changes outside are collected and synced at the next start. Tap or drag cells to paint (tap a weekday name · hour number for the whole row · column).",
"칠한 시간이 없어 동기화하지 않습니다.": "No hours painted, so nothing will sync.", "칠한 시간이 없어 동기화하지 않습니다.": "No hours painted, so nothing will sync.",
"칸 그림": "Treemap",
"칸에 보이는 옵션: {0}\n저장된 옵션: {1}\n\n고친 옵션을 저장하고 실행할까요?": "Options in the field: {0}\nSaved options: {1}\n\nSave the edited options and run?", "칸에 보이는 옵션: {0}\n저장된 옵션: {1}\n\n고친 옵션을 저장하고 실행할까요?": "Options in the field: {0}\nSaved options: {1}\n\nSave the edited options and run?",
"컴포넌트": "Components", "컴포넌트": "Components",
"컴포넌트 목록을 받을 수 없습니다 ({0})": "Could not get the component list ({0})", "컴포넌트 목록을 받을 수 없습니다 ({0})": "Could not get the component list ({0})",

View File

@ -381,6 +381,7 @@
"ZIP 이 있는 폴더로": "ZIP のあるフォルダーへ", "ZIP 이 있는 폴더로": "ZIP のあるフォルダーへ",
"ZIP 이름": "ZIP の名前", "ZIP 이름": "ZIP の名前",
"[N] 원래 이름 · [C] 번호 예: Show S01E[C]": "[N] 元の名前 · [C] 番号 例: Show S01E[C]", "[N] 원래 이름 · [C] 번호 예: Show S01E[C]": "[N] 元の名前 · [C] 番号 例: Show S01E[C]",
"[N] 원래 이름 · [C] 번호 · [D] 날짜 · [T] 시각 · 확장자는 그대로 예: Show S01E[C]": "[N] 元の名前・[C] 番号・[D] 日付・[T] 時刻・拡張子はそのまま 例: Show S01E[C]",
"[N] 원래 이름 · [C] 번호 · 확장자는 그대로 예: Show S01E[C]": "[N] 元の名前 · [C] 番号 · 拡張子はそのまま 例: Show S01E[C]", "[N] 원래 이름 · [C] 번호 · 확장자는 그대로 예: Show S01E[C]": "[N] 元の名前 · [C] 番号 · 拡張子はそのまま 例: Show S01E[C]",
"[서버 설정 고치기] 에서 비밀번호를 넣으면 이 기기의 안전 저장소에 남아 다음부터는 다시 넣지 않아도 됩니다.": "[サーバー設定を修正] でパスワードを入力すると、この端末の安全な保存領域に残り、次回からは入力不要です。", "[서버 설정 고치기] 에서 비밀번호를 넣으면 이 기기의 안전 저장소에 남아 다음부터는 다시 넣지 않아도 됩니다.": "[サーバー設定を修正] でパスワードを入力すると、この端末の安全な保存領域に残り、次回からは入力不要です。",
"[서버 설정 고치기] 에서 아이디 · 비밀번호를 확인하세요.": "[サーバー設定を修正] でユーザー名・パスワードを確認してください。", "[서버 설정 고치기] 에서 아이디 · 비밀번호를 확인하세요.": "[サーバー設定を修正] でユーザー名・パスワードを確認してください。",
@ -444,6 +445,7 @@
"{0} / {1}장": "{0} / {1} 枚", "{0} / {1}장": "{0} / {1} 枚",
"{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2}(メイン画面の上部で変更)", "{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2}(メイン画面の上部で変更)",
"{0} · {1}배 · {2}": "{0}・{1} 倍・{2}", "{0} · {1}배 · {2}": "{0}・{1} 倍・{2}",
"{0} · 그림 · 동영상 {1}개": "{0} · 画像・動画 {1} 個",
"{0} · 링크 {1}개 제외": "{0} · リンク {1} 件を除く", "{0} · 링크 {1}개 제외": "{0} · リンク {1} 件を除く",
"{0} · 파일 {1}개": "{0} · ファイル {1} 件", "{0} · 파일 {1}개": "{0} · ファイル {1} 件",
"{0} → {1} 번역 {2}%": "{0} → {1} 翻訳 {2}%", "{0} → {1} 번역 {2}%": "{0} → {1} 翻訳 {2}%",
@ -630,6 +632,7 @@
"검색": "検索", "검색": "検索",
"검색 지우기": "検索をクリア", "검색 지우기": "検索をクリア",
"검정": "黒", "검정": "黒",
"격자로 보기 (그림 · 동영상)": "グリッド表示 (画像・動画)",
"결과 폴더": "出力フォルダー", "결과 폴더": "出力フォルダー",
"결과 폴더 열기": "出力フォルダーを開く", "결과 폴더 열기": "出力フォルダーを開く",
"결과 폴더 열기 (동영상을 고르세요)": "出力フォルダーを開く(動画を選んでください)", "결과 폴더 열기 (동영상을 고르세요)": "出力フォルダーを開く(動画を選んでください)",
@ -716,8 +719,11 @@
"그중 {0}개는 받는 쪽이 더 새것입니다 - 더 새 파일을 예전 것으로 덮어씁니다": "そのうち {0} 件は受け取る側の方が新しいです - 新しいファイルを古いもので上書きします", "그중 {0}개는 받는 쪽이 더 새것입니다 - 더 새 파일을 예전 것으로 덮어씁니다": "そのうち {0} 件は受け取る側の方が新しいです - 新しいファイルを古いもので上書きします",
"그중 폴더 {0}개 - 안의 파일 · 폴더도 모두 함께": "うちフォルダー {0} 件 - 中のファイル・フォルダーもすべて一緒に", "그중 폴더 {0}개 - 안의 파일 · 폴더도 모두 함께": "うちフォルダー {0} 件 - 中のファイル・フォルダーもすべて一緒に",
"글 → 그림 · 그림 → 그림 (기기 안에서, 모델은 환경 설정에서 받기)": "テキスト → 画像 · 画像 → 画像 (端末内で。モデルは設定でダウンロード)", "글 → 그림 · 그림 → 그림 (기기 안에서, 모델은 환경 설정에서 받기)": "テキスト → 画像 · 画像 → 画像 (端末内で。モデルは設定でダウンロード)",
"글 뷰어로 보기": "テキストビューアで表示",
"글을 격자에 반영": "テキストをグリッドに反映", "글을 격자에 반영": "テキストをグリッドに反映",
"글자 · 버튼 크기입니다. 위쪽 막대의 − · + 로 그때그때 바꿀 수 있고, 가운데 숫자를 누르면 이 크기로 돌아갑니다 (지금 {0}%)": "文字・ボタンの大きさです。上部バーの − ・ + でその都度変更でき、中央の数字を押すとこの大きさに戻ります(現在 {0}%)", "글자 · 버튼 크기입니다. 위쪽 막대의 − · + 로 그때그때 바꿀 수 있고, 가운데 숫자를 누르면 이 크기로 돌아갑니다 (지금 {0}%)": "文字・ボタンの大きさです。上部バーの − ・ + でその都度変更でき、中央の数字を押すとこの大きさに戻ります(現在 {0}%)",
"글자 작게": "文字を小さく",
"글자 크게": "文字を大きく",
"금": "金", "금": "金",
"기계 번역": "機械翻訳", "기계 번역": "機械翻訳",
"기기": "端末", "기기": "端末",
@ -772,10 +778,13 @@
"끝내기 (작업 멈춤)": "終了 (作業を止める)", "끝내기 (작업 멈춤)": "終了 (作業を止める)",
"끝에서 처음으로 돌기": "最後から最初に戻る", "끝에서 처음으로 돌기": "最後から最初に戻る",
"나가기": "終了する", "나가기": "終了する",
"나머지 폴더 {0}개": "その他のフォルダー {0} 個",
"나머지는 대기했다가 차례로 받습니다": "残りは待機して順番にダウンロードします", "나머지는 대기했다가 차례로 받습니다": "残りは待機して順番にダウンロードします",
"나머지는 지웠습니다.": "残りは削除しました。", "나머지는 지웠습니다.": "残りは削除しました。",
"나중에": "後で", "나중에": "後で",
"날짜": "日付", "날짜": "日付",
"날짜: 바뀐 때": "日付: 更新日時",
"날짜: 찍은 때 (사진, 없으면 바뀐 때)": "日付: 撮影日時 (写真、なければ更新日時)",
"남는 것": "残るもの", "남는 것": "残るもの",
"남은 {0}장": "残り {0} 枚", "남은 {0}장": "残り {0} 枚",
"남은 것 모두 취소 ({0}개)": "残りをすべて取り消し ({0} 件)", "남은 것 모두 취소 ({0}개)": "残りをすべて取り消し ({0} 件)",
@ -1335,6 +1344,7 @@
"비어 있는 압축 파일입니다.": "空の圧縮ファイルです。", "비어 있는 압축 파일입니다.": "空の圧縮ファイルです。",
"비어 있는 폴더": "空のフォルダー", "비어 있는 폴더": "空のフォルダー",
"비어 있는 폴더입니다.\n위의 [즐겨찾기 추가] · [새 폴더] 를 누르거나, 즐겨찾기를 왼쪽 폴더로 끌어다 놓으세요.": "空のフォルダーです。\n上の[ブックマーク追加]・[新しいフォルダー]を押すか、ブックマークを左のフォルダーにドラッグしてください。", "비어 있는 폴더입니다.\n위의 [즐겨찾기 추가] · [새 폴더] 를 누르거나, 즐겨찾기를 왼쪽 폴더로 끌어다 놓으세요.": "空のフォルダーです。\n上の[ブックマーク追加]・[新しいフォルダー]を押すか、ブックマークを左のフォルダーにドラッグしてください。",
"비어 있습니다": "空です",
"비어 있음": "空", "비어 있음": "空",
"비우기": "空にする", "비우기": "空にする",
"비우면 지움": "空なら削除", "비우면 지움": "空なら削除",
@ -1863,8 +1873,10 @@
"이 폴더에 새 폴더": "このフォルダーに新しいフォルダー", "이 폴더에 새 폴더": "このフォルダーに新しいフォルダー",
"이 폴더에 현재 페이지 추가": "このフォルダーに現在のページを追加", "이 폴더에 현재 페이지 추가": "このフォルダーに現在のページを追加",
"이 폴더에는 고를 파일이 없습니다": "このフォルダーには選べるファイルがありません", "이 폴더에는 고를 파일이 없습니다": "このフォルダーには選べるファイルがありません",
"이 폴더에는 그림 · 동영상이 없습니다": "このフォルダーには画像・動画がありません",
"이 폴더에만: {0}": "このフォルダーだけ: {0}", "이 폴더에만: {0}": "このフォルダーだけ: {0}",
"이 폴더의 동영상 재생": "このフォルダーの動画を再生", "이 폴더의 동영상 재생": "このフォルダーの動画を再生",
"이 폴더의 파일": "このフォルダーのファイル",
"이 프로그램이 관리자 권한으로 실행 중입니다. 권장하지 않습니다.\n\n· 내장 Chrome 엔진은 샌드박스 (웹 페이지를 가두는 보호) 가 꺼져 있어, 웹 페이지가 관리자 권한 프로세스 안에서 돕니다. 페이지의 취약점이 곧 이 PC 의 관리자 권한이 될 수 있습니다.\n· Edge 엔진은 이 영향이 없습니다.\n· 관리자 권한 없이 프로그램을 실행하면 이 설정 없이도 Chrome 엔진을 쓸 수 있습니다.": "このアプリは管理者権限で実行されています。おすすめしません。\n\n· 内蔵 Chrome エンジンはサンドボックス (Web ページを閉じ込める保護) がオフのため、Web ページが管理者権限のプロセス内で動きます。ページの脆弱性がそのままこの PC の管理者権限になるおそれがあります。\n· Edge エンジンにはこの影響がありません。\n· 管理者権限なしでアプリを実行すれば、この設定なしで Chrome エンジンを使えます。", "이 프로그램이 관리자 권한으로 실행 중입니다. 권장하지 않습니다.\n\n· 내장 Chrome 엔진은 샌드박스 (웹 페이지를 가두는 보호) 가 꺼져 있어, 웹 페이지가 관리자 권한 프로세스 안에서 돕니다. 페이지의 취약점이 곧 이 PC 의 관리자 권한이 될 수 있습니다.\n· Edge 엔진은 이 영향이 없습니다.\n· 관리자 권한 없이 프로그램을 실행하면 이 설정 없이도 Chrome 엔진을 쓸 수 있습니다.": "このアプリは管理者権限で実行されています。おすすめしません。\n\n· 内蔵 Chrome エンジンはサンドボックス (Web ページを閉じ込める保護) がオフのため、Web ページが管理者権限のプロセス内で動きます。ページの脆弱性がそのままこの PC の管理者権限になるおそれがあります。\n· Edge エンジンにはこの影響がありません。\n· 管理者権限なしでアプリを実行すれば、この設定なしで Chrome エンジンを使えます。",
"이 형식을 여는 앱이 없습니다. 길게 누르거나 오른쪽 클릭 → [내장 플레이어로 재생] 으로 들어 보세요.": "この形式を開くアプリがありません。長押しまたは右クリック → [内蔵プレーヤーで再生] で試してください。", "이 형식을 여는 앱이 없습니다. 길게 누르거나 오른쪽 클릭 → [내장 플레이어로 재생] 으로 들어 보세요.": "この形式を開くアプリがありません。長押しまたは右クリック → [内蔵プレーヤーで再生] で試してください。",
"이 화면은 앱 안 웹 뷰의 연결을 Tor 로 보내는 것이며, 공식 Tor Browser 가 아닙니다.\n\n· 연결 (이름 찾기 포함) 은 모두 Tor 로 나가고, 쿠키 · 기록 · 캐시는 일반 브라우저와 따로 두며 닫으면 지웁니다 (환경 설정에서 남기기 가능).\n· 하지만 브라우저 지문 (화면 크기 · 글꼴 · 기기 정보 등) 을 감추는 보호는 공식 Tor Browser 보다 약합니다. 사이트가 이 기기를 알아볼 수 있습니다.\n· 강한 익명성이 필요하면 공식 Tor Browser (https://www.torproject.org) 를 쓰세요.": "この画面はアプリ内 Web ビューの接続を Tor 経由にするもので、公式の Tor Browser ではありません。\n\n・接続 (名前解決を含む) はすべて Tor 経由になり、Cookie・履歴・キャッシュは通常のブラウザーとは別に保存し、閉じると消去します (環境設定で残すことも可能)。\n・ただし、ブラウザーのフィンガープリント (画面サイズ・フォント・端末情報など) を隠す保護は公式 Tor Browser より弱いです。サイトがこの端末を識別できる場合があります。\n・強い匿名性が必要な場合は公式 Tor Browser (https://www.torproject.org) を使ってください。", "이 화면은 앱 안 웹 뷰의 연결을 Tor 로 보내는 것이며, 공식 Tor Browser 가 아닙니다.\n\n· 연결 (이름 찾기 포함) 은 모두 Tor 로 나가고, 쿠키 · 기록 · 캐시는 일반 브라우저와 따로 두며 닫으면 지웁니다 (환경 설정에서 남기기 가능).\n· 하지만 브라우저 지문 (화면 크기 · 글꼴 · 기기 정보 등) 을 감추는 보호는 공식 Tor Browser 보다 약합니다. 사이트가 이 기기를 알아볼 수 있습니다.\n· 강한 익명성이 필요하면 공식 Tor Browser (https://www.torproject.org) 를 쓰세요.": "この画面はアプリ内 Web ビューの接続を Tor 経由にするもので、公式の Tor Browser ではありません。\n\n・接続 (名前解決を含む) はすべて Tor 経由になり、Cookie・履歴・キャッシュは通常のブラウザーとは別に保存し、閉じると消去します (環境設定で残すことも可能)。\n・ただし、ブラウザーのフィンガープリント (画面サイズ・フォント・端末情報など) を隠す保護は公式 Tor Browser より弱いです。サイトがこの端末を識別できる場合があります。\n・強い匿名性が必要な場合は公式 Tor Browser (https://www.torproject.org) を使ってください。",
@ -2081,6 +2093,7 @@
"전체 {0}개 중 {1}번째": "全 {0} 件中 {1} 件目", "전체 {0}개 중 {1}번째": "全 {0} 件中 {1} 件目",
"전체 만들기": "すべて作成", "전체 만들기": "すべて作成",
"전체 목록": "全体リスト", "전체 목록": "全体リスト",
"전체 복사": "すべてコピー",
"전체 선택": "すべて選択", "전체 선택": "すべて選択",
"전체 이동": "すべて移動", "전체 이동": "すべて移動",
"전체 화면": "全画面", "전체 화면": "全画面",
@ -2142,6 +2155,7 @@
"준비 중": "準備中", "준비 중": "準備中",
"준비 중…": "準備中…", "준비 중…": "準備中…",
"준비하는 중": "準備中", "준비하는 중": "準備中",
"줄 번호": "行番号",
"줄 삭제": "行を削除", "줄 삭제": "行を削除",
"중국어": "中国語", "중국어": "中国語",
"중국어 간체": "中国語(簡体字)", "중국어 간체": "中国語(簡体字)",
@ -2308,6 +2322,7 @@
"취소했습니다. 풀던 파일은 남지 않습니다 (다 푼 파일은 그대로).": "キャンセルしました。展開中のファイルは残りません (展開済みのファイルはそのまま)。", "취소했습니다. 풀던 파일은 남지 않습니다 (다 푼 파일은 그대로).": "キャンセルしました。展開中のファイルは残りません (展開済みのファイルはそのまま)。",
"칠한 시간에만 동기화하고, 그 밖의 시간에는 바뀐 것을 모아 두었다가 다음 시작 시간에 맞춥니다. 칸을 누르거나 끌어서 칠합니다 (요일 이름 · 시 숫자를 누르면 그 줄 전체).": "塗った時間だけ同期し、それ以外は変更を貯めて次の開始時刻に同期します。マスをタップまたはドラッグして塗ります (曜日名 · 時の数字をタップするとその行 · 列全体)。", "칠한 시간에만 동기화하고, 그 밖의 시간에는 바뀐 것을 모아 두었다가 다음 시작 시간에 맞춥니다. 칸을 누르거나 끌어서 칠합니다 (요일 이름 · 시 숫자를 누르면 그 줄 전체).": "塗った時間だけ同期し、それ以外は変更を貯めて次の開始時刻に同期します。マスをタップまたはドラッグして塗ります (曜日名 · 時の数字をタップするとその行 · 列全体)。",
"칠한 시간이 없어 동기화하지 않습니다.": "塗った時間がないため同期しません。", "칠한 시간이 없어 동기화하지 않습니다.": "塗った時間がないため同期しません。",
"칸 그림": "ツリーマップ",
"칸에 보이는 옵션: {0}\n저장된 옵션: {1}\n\n고친 옵션을 저장하고 실행할까요?": "欄に表示中のオプション: {0}\n保存済みのオプション: {1}\n\n編集したオプションを保存して実行しますか?", "칸에 보이는 옵션: {0}\n저장된 옵션: {1}\n\n고친 옵션을 저장하고 실행할까요?": "欄に表示中のオプション: {0}\n保存済みのオプション: {1}\n\n編集したオプションを保存して実行しますか?",
"컴포넌트": "コンポーネント", "컴포넌트": "コンポーネント",
"컴포넌트 목록을 받을 수 없습니다 ({0})": "コンポーネント一覧を取得できません ({0})", "컴포넌트 목록을 받을 수 없습니다 ({0})": "コンポーネント一覧を取得できません ({0})",

View File

@ -381,6 +381,7 @@
"ZIP 이 있는 폴더로": "到 ZIP 所在文件夹", "ZIP 이 있는 폴더로": "到 ZIP 所在文件夹",
"ZIP 이름": "ZIP 名称", "ZIP 이름": "ZIP 名称",
"[N] 원래 이름 · [C] 번호 예: Show S01E[C]": "[N] 原名称 · [C] 序号 例如: Show S01E[C]", "[N] 원래 이름 · [C] 번호 예: Show S01E[C]": "[N] 原名称 · [C] 序号 例如: Show S01E[C]",
"[N] 원래 이름 · [C] 번호 · [D] 날짜 · [T] 시각 · 확장자는 그대로 예: Show S01E[C]": "[N] 原名 · [C] 编号 · [D] 日期 · [T] 时间 · 扩展名不变 例: Show S01E[C]",
"[N] 원래 이름 · [C] 번호 · 확장자는 그대로 예: Show S01E[C]": "[N] 原名称 · [C] 序号 · 保留扩展名 例如: Show S01E[C]", "[N] 원래 이름 · [C] 번호 · 확장자는 그대로 예: Show S01E[C]": "[N] 原名称 · [C] 序号 · 保留扩展名 例如: Show S01E[C]",
"[서버 설정 고치기] 에서 비밀번호를 넣으면 이 기기의 안전 저장소에 남아 다음부터는 다시 넣지 않아도 됩니다.": "在 [修改服务器设置] 中输入密码后会保存在本设备的安全存储中,以后无需再次输入。", "[서버 설정 고치기] 에서 비밀번호를 넣으면 이 기기의 안전 저장소에 남아 다음부터는 다시 넣지 않아도 됩니다.": "在 [修改服务器设置] 中输入密码后会保存在本设备的安全存储中,以后无需再次输入。",
"[서버 설정 고치기] 에서 아이디 · 비밀번호를 확인하세요.": "请在 [修改服务器设置] 中检查用户名和密码。", "[서버 설정 고치기] 에서 아이디 · 비밀번호를 확인하세요.": "请在 [修改服务器设置] 中检查用户名和密码。",
@ -444,6 +445,7 @@
"{0} / {1}장": "{0} / {1} 张", "{0} / {1}장": "{0} / {1} 张",
"{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2}(在主界面顶部更改)", "{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2}(在主界面顶部更改)",
"{0} · {1}배 · {2}": "{0} · {1} 倍 · {2}", "{0} · {1}배 · {2}": "{0} · {1} 倍 · {2}",
"{0} · 그림 · 동영상 {1}개": "{0} · {1} 个图片 / 视频",
"{0} · 링크 {1}개 제외": "{0} · 已排除 {1} 个链接", "{0} · 링크 {1}개 제외": "{0} · 已排除 {1} 个链接",
"{0} · 파일 {1}개": "{0} · {1} 个文件", "{0} · 파일 {1}개": "{0} · {1} 个文件",
"{0} → {1} 번역 {2}%": "{0} → {1} 翻译 {2}%", "{0} → {1} 번역 {2}%": "{0} → {1} 翻译 {2}%",
@ -630,6 +632,7 @@
"검색": "搜索", "검색": "搜索",
"검색 지우기": "清除搜索", "검색 지우기": "清除搜索",
"검정": "黑色", "검정": "黑色",
"격자로 보기 (그림 · 동영상)": "网格查看 (图片 / 视频)",
"결과 폴더": "输出文件夹", "결과 폴더": "输出文件夹",
"결과 폴더 열기": "打开输出文件夹", "결과 폴더 열기": "打开输出文件夹",
"결과 폴더 열기 (동영상을 고르세요)": "打开输出文件夹(请选择视频)", "결과 폴더 열기 (동영상을 고르세요)": "打开输出文件夹(请选择视频)",
@ -716,8 +719,11 @@
"그중 {0}개는 받는 쪽이 더 새것입니다 - 더 새 파일을 예전 것으로 덮어씁니다": "其中 {0} 个在接收端较新 - 将用旧文件覆盖较新的文件", "그중 {0}개는 받는 쪽이 더 새것입니다 - 더 새 파일을 예전 것으로 덮어씁니다": "其中 {0} 个在接收端较新 - 将用旧文件覆盖较新的文件",
"그중 폴더 {0}개 - 안의 파일 · 폴더도 모두 함께": "其中 {0} 个文件夹 - 其中的文件和文件夹也一并处理", "그중 폴더 {0}개 - 안의 파일 · 폴더도 모두 함께": "其中 {0} 个文件夹 - 其中的文件和文件夹也一并处理",
"글 → 그림 · 그림 → 그림 (기기 안에서, 모델은 환경 설정에서 받기)": "文字 → 图片 · 图片 → 图片(在本设备上;模型在设置中下载)", "글 → 그림 · 그림 → 그림 (기기 안에서, 모델은 환경 설정에서 받기)": "文字 → 图片 · 图片 → 图片(在本设备上;模型在设置中下载)",
"글 뷰어로 보기": "以文本查看",
"글을 격자에 반영": "将文本应用到网格", "글을 격자에 반영": "将文本应用到网格",
"글자 · 버튼 크기입니다. 위쪽 막대의 − · + 로 그때그때 바꿀 수 있고, 가운데 숫자를 누르면 이 크기로 돌아갑니다 (지금 {0}%)": "文字 · 按钮大小。可随时用顶部栏的 − · + 调整,点击中间的数字恢复为此大小(当前 {0}%)", "글자 · 버튼 크기입니다. 위쪽 막대의 − · + 로 그때그때 바꿀 수 있고, 가운데 숫자를 누르면 이 크기로 돌아갑니다 (지금 {0}%)": "文字 · 按钮大小。可随时用顶部栏的 − · + 调整,点击中间的数字恢复为此大小(当前 {0}%)",
"글자 작게": "缩小文字",
"글자 크게": "放大文字",
"금": "五", "금": "五",
"기계 번역": "机器翻译", "기계 번역": "机器翻译",
"기기": "设备", "기기": "设备",
@ -772,10 +778,13 @@
"끝내기 (작업 멈춤)": "退出 (停止任务)", "끝내기 (작업 멈춤)": "退出 (停止任务)",
"끝에서 처음으로 돌기": "到末尾后回到开头", "끝에서 처음으로 돌기": "到末尾后回到开头",
"나가기": "退出", "나가기": "退出",
"나머지 폴더 {0}개": "其余 {0} 个文件夹",
"나머지는 대기했다가 차례로 받습니다": "其余的等待并依次下载", "나머지는 대기했다가 차례로 받습니다": "其余的等待并依次下载",
"나머지는 지웠습니다.": "其余已删除。", "나머지는 지웠습니다.": "其余已删除。",
"나중에": "以后再说", "나중에": "以后再说",
"날짜": "日期", "날짜": "日期",
"날짜: 바뀐 때": "日期: 修改时间",
"날짜: 찍은 때 (사진, 없으면 바뀐 때)": "日期: 拍摄时间 (照片; 否则为修改时间)",
"남는 것": "保留的内容", "남는 것": "保留的内容",
"남은 {0}장": "剩余 {0} 张", "남은 {0}장": "剩余 {0} 张",
"남은 것 모두 취소 ({0}개)": "取消全部剩余项({0} 个)", "남은 것 모두 취소 ({0}개)": "取消全部剩余项({0} 个)",
@ -1335,6 +1344,7 @@
"비어 있는 압축 파일입니다.": "压缩文件为空。", "비어 있는 압축 파일입니다.": "压缩文件为空。",
"비어 있는 폴더": "空文件夹", "비어 있는 폴더": "空文件夹",
"비어 있는 폴더입니다.\n위의 [즐겨찾기 추가] · [새 폴더] 를 누르거나, 즐겨찾기를 왼쪽 폴더로 끌어다 놓으세요.": "这是空文件夹。\n请点击上方的[添加书签] · [新建文件夹],或将书签拖到左侧文件夹。", "비어 있는 폴더입니다.\n위의 [즐겨찾기 추가] · [새 폴더] 를 누르거나, 즐겨찾기를 왼쪽 폴더로 끌어다 놓으세요.": "这是空文件夹。\n请点击上方的[添加书签] · [新建文件夹],或将书签拖到左侧文件夹。",
"비어 있습니다": "空",
"비어 있음": "空", "비어 있음": "空",
"비우기": "清空", "비우기": "清空",
"비우면 지움": "留空则删除", "비우면 지움": "留空则删除",
@ -1863,8 +1873,10 @@
"이 폴더에 새 폴더": "在此文件夹中新建文件夹", "이 폴더에 새 폴더": "在此文件夹中新建文件夹",
"이 폴더에 현재 페이지 추가": "将当前页面添加到此文件夹", "이 폴더에 현재 페이지 추가": "将当前页面添加到此文件夹",
"이 폴더에는 고를 파일이 없습니다": "此文件夹中没有可选择的文件", "이 폴더에는 고를 파일이 없습니다": "此文件夹中没有可选择的文件",
"이 폴더에는 그림 · 동영상이 없습니다": "此文件夹中没有图片或视频",
"이 폴더에만: {0}": "仅此文件夹: {0}", "이 폴더에만: {0}": "仅此文件夹: {0}",
"이 폴더의 동영상 재생": "播放此文件夹中的视频", "이 폴더의 동영상 재생": "播放此文件夹中的视频",
"이 폴더의 파일": "此文件夹中的文件",
"이 프로그램이 관리자 권한으로 실행 중입니다. 권장하지 않습니다.\n\n· 내장 Chrome 엔진은 샌드박스 (웹 페이지를 가두는 보호) 가 꺼져 있어, 웹 페이지가 관리자 권한 프로세스 안에서 돕니다. 페이지의 취약점이 곧 이 PC 의 관리자 권한이 될 수 있습니다.\n· Edge 엔진은 이 영향이 없습니다.\n· 관리자 권한 없이 프로그램을 실행하면 이 설정 없이도 Chrome 엔진을 쓸 수 있습니다.": "本应用正以管理员权限运行,不推荐这样做。\n\n· 内置 Chrome 引擎的沙盒(限制网页的保护)已关闭,网页会在管理员权限进程中运行。网页中的漏洞可能直接变成此电脑的管理员权限。\n· Edge 引擎不受此影响。\n· 不以管理员权限运行本应用时,无需此设置即可使用 Chrome 引擎。", "이 프로그램이 관리자 권한으로 실행 중입니다. 권장하지 않습니다.\n\n· 내장 Chrome 엔진은 샌드박스 (웹 페이지를 가두는 보호) 가 꺼져 있어, 웹 페이지가 관리자 권한 프로세스 안에서 돕니다. 페이지의 취약점이 곧 이 PC 의 관리자 권한이 될 수 있습니다.\n· Edge 엔진은 이 영향이 없습니다.\n· 관리자 권한 없이 프로그램을 실행하면 이 설정 없이도 Chrome 엔진을 쓸 수 있습니다.": "本应用正以管理员权限运行,不推荐这样做。\n\n· 内置 Chrome 引擎的沙盒(限制网页的保护)已关闭,网页会在管理员权限进程中运行。网页中的漏洞可能直接变成此电脑的管理员权限。\n· Edge 引擎不受此影响。\n· 不以管理员权限运行本应用时,无需此设置即可使用 Chrome 引擎。",
"이 형식을 여는 앱이 없습니다. 길게 누르거나 오른쪽 클릭 → [내장 플레이어로 재생] 으로 들어 보세요.": "没有可打开此格式的应用。请长按或右键 → [用内置播放器播放] 试试。", "이 형식을 여는 앱이 없습니다. 길게 누르거나 오른쪽 클릭 → [내장 플레이어로 재생] 으로 들어 보세요.": "没有可打开此格式的应用。请长按或右键 → [用内置播放器播放] 试试。",
"이 화면은 앱 안 웹 뷰의 연결을 Tor 로 보내는 것이며, 공식 Tor Browser 가 아닙니다.\n\n· 연결 (이름 찾기 포함) 은 모두 Tor 로 나가고, 쿠키 · 기록 · 캐시는 일반 브라우저와 따로 두며 닫으면 지웁니다 (환경 설정에서 남기기 가능).\n· 하지만 브라우저 지문 (화면 크기 · 글꼴 · 기기 정보 등) 을 감추는 보호는 공식 Tor Browser 보다 약합니다. 사이트가 이 기기를 알아볼 수 있습니다.\n· 강한 익명성이 필요하면 공식 Tor Browser (https://www.torproject.org) 를 쓰세요.": "此画面将应用内网页视图的连接经由 Tor 发送,并非官方 Tor Browser。\n\n· 所有连接 (包括名称解析) 都经由 Tor,Cookie · 记录 · 缓存与普通浏览器分开保存,关闭时清除 (可在设置中保留)。\n· 但隐藏浏览器指纹 (屏幕尺寸 · 字体 · 设备信息等) 的保护比官方 Tor Browser 弱。网站可能识别出本设备。\n· 如需强匿名性,请使用官方 Tor Browser (https://www.torproject.org)。", "이 화면은 앱 안 웹 뷰의 연결을 Tor 로 보내는 것이며, 공식 Tor Browser 가 아닙니다.\n\n· 연결 (이름 찾기 포함) 은 모두 Tor 로 나가고, 쿠키 · 기록 · 캐시는 일반 브라우저와 따로 두며 닫으면 지웁니다 (환경 설정에서 남기기 가능).\n· 하지만 브라우저 지문 (화면 크기 · 글꼴 · 기기 정보 등) 을 감추는 보호는 공식 Tor Browser 보다 약합니다. 사이트가 이 기기를 알아볼 수 있습니다.\n· 강한 익명성이 필요하면 공식 Tor Browser (https://www.torproject.org) 를 쓰세요.": "此画面将应用内网页视图的连接经由 Tor 发送,并非官方 Tor Browser。\n\n· 所有连接 (包括名称解析) 都经由 Tor,Cookie · 记录 · 缓存与普通浏览器分开保存,关闭时清除 (可在设置中保留)。\n· 但隐藏浏览器指纹 (屏幕尺寸 · 字体 · 设备信息等) 的保护比官方 Tor Browser 弱。网站可能识别出本设备。\n· 如需强匿名性,请使用官方 Tor Browser (https://www.torproject.org)。",
@ -2081,6 +2093,7 @@
"전체 {0}개 중 {1}번째": "共 {0} 个中的第 {1} 个", "전체 {0}개 중 {1}번째": "共 {0} 个中的第 {1} 个",
"전체 만들기": "全部制作", "전체 만들기": "全部制作",
"전체 목록": "完整列表", "전체 목록": "完整列表",
"전체 복사": "全部复制",
"전체 선택": "全选", "전체 선택": "全选",
"전체 이동": "全部移动", "전체 이동": "全部移动",
"전체 화면": "全屏", "전체 화면": "全屏",
@ -2142,6 +2155,7 @@
"준비 중": "准备中", "준비 중": "准备中",
"준비 중…": "准备中…", "준비 중…": "准备中…",
"준비하는 중": "正在准备", "준비하는 중": "正在准备",
"줄 번호": "行号",
"줄 삭제": "删除行", "줄 삭제": "删除行",
"중국어": "中文", "중국어": "中文",
"중국어 간체": "简体中文", "중국어 간체": "简体中文",
@ -2308,6 +2322,7 @@
"취소했습니다. 풀던 파일은 남지 않습니다 (다 푼 파일은 그대로).": "已取消。正在解压的文件不会留下 (已解压的文件保留)。", "취소했습니다. 풀던 파일은 남지 않습니다 (다 푼 파일은 그대로).": "已取消。正在解压的文件不会留下 (已解压的文件保留)。",
"칠한 시간에만 동기화하고, 그 밖의 시간에는 바뀐 것을 모아 두었다가 다음 시작 시간에 맞춥니다. 칸을 누르거나 끌어서 칠합니다 (요일 이름 · 시 숫자를 누르면 그 줄 전체).": "只在涂色的时间同步,其他时间的变化累积到下次开始再同步。点按或拖动格子涂色 (点按星期名 · 小时数字可选整行 · 整列)。", "칠한 시간에만 동기화하고, 그 밖의 시간에는 바뀐 것을 모아 두었다가 다음 시작 시간에 맞춥니다. 칸을 누르거나 끌어서 칠합니다 (요일 이름 · 시 숫자를 누르면 그 줄 전체).": "只在涂色的时间同步,其他时间的变化累积到下次开始再同步。点按或拖动格子涂色 (点按星期名 · 小时数字可选整行 · 整列)。",
"칠한 시간이 없어 동기화하지 않습니다.": "没有涂色的时间,因此不会同步。", "칠한 시간이 없어 동기화하지 않습니다.": "没有涂色的时间,因此不会同步。",
"칸 그림": "矩形树图",
"칸에 보이는 옵션: {0}\n저장된 옵션: {1}\n\n고친 옵션을 저장하고 실행할까요?": "输入框中的选项: {0}\n已保存的选项: {1}\n\n保存修改的选项并执行吗?", "칸에 보이는 옵션: {0}\n저장된 옵션: {1}\n\n고친 옵션을 저장하고 실행할까요?": "输入框中的选项: {0}\n已保存的选项: {1}\n\n保存修改的选项并执行吗?",
"컴포넌트": "组件", "컴포넌트": "组件",
"컴포넌트 목록을 받을 수 없습니다 ({0})": "无法获取组件列表({0})", "컴포넌트 목록을 받을 수 없습니다 ({0})": "无法获取组件列表({0})",

118
lib/ui/explorer_grid.dart Normal file
View File

@ -0,0 +1,118 @@
import 'dart:io';
import 'package:flutter/material.dart';
import '../core/file_ops.dart';
import '../core/playlist.dart' show isVideoFile;
import '../l10n/tr.dart';
import 'theme.dart';
/// X-plore 비교 g: 격자 보기 - 지금 폴더의 그림 · 동영상을 썸네일 격자로 (X-plore 의 그림 보기 방식).
/// 누르면 열기 (그림 보기 · 재생), 길게 누르면 탐색기에서 그 자리로. 돌려주는 값: "그 자리로" 고른 경로 (없으면 null)
class ExplorerGridPage extends StatefulWidget {
final String title;
final List<FileEntry> entries;
/// 그림 확장자 (환경 설정 > 보기)
final List<String> imageExts;
/// 동영상 썸네일 (없으면 아이콘)
final Future<File?> Function(FileEntry e) videoThumb;
final Future<void> Function(FileEntry e) onOpen;
const ExplorerGridPage({
super.key,
required this.title,
required this.entries,
required this.imageExts,
required this.videoThumb,
required this.onOpen,
});
/// 격자에 넣을 것 (그림 · 동영상)
static List<FileEntry> pick(List<FileEntry> all, List<String> imageExts) =>
[for (final e in all) if (!e.isDir && (imageExts.contains(e.ext) || isVideoFile(e.path))) e];
@override
State<ExplorerGridPage> createState() => _ExplorerGridPageState();
}
class _ExplorerGridPageState extends State<ExplorerGridPage> {
/// 칸 크기 (밀어서 키우기 · 줄이기 대신 단추)
double _cell = 140;
@override
Widget build(BuildContext context) {
final items = ExplorerGridPage.pick(widget.entries, widget.imageExts);
return Scaffold(
appBar: AppBar(
title: Text(trf('{0} · 그림 · 동영상 {1}개', [widget.title, items.length]), maxLines: 1, overflow: TextOverflow.ellipsis),
actions: [
IconButton(
tooltip: tr('작게'),
icon: const Icon(Icons.zoom_out),
onPressed: _cell > 80 ? () => setState(() => _cell -= 30) : null,
),
IconButton(
tooltip: tr('크게'),
icon: const Icon(Icons.zoom_in),
onPressed: _cell < 320 ? () => setState(() => _cell += 30) : null,
),
],
),
body: items.isEmpty
? Center(child: Text(tr('이 폴더에는 그림 · 동영상이 없습니다')))
: GridView.builder(
padding: const EdgeInsets.all(6),
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: _cell, mainAxisSpacing: 6, crossAxisSpacing: 6),
itemCount: items.length,
itemBuilder: (_, i) => _tile(items[i]),
),
);
}
Widget _tile(FileEntry e) {
final video = isVideoFile(e.path);
final dpr = MediaQuery.devicePixelRatioOf(context);
final px = (_cell * dpr).round();
Widget picture;
if (video) {
picture = FutureBuilder<File?>(
future: widget.videoThumb(e),
builder: (_, s) => s.data == null
? const Center(child: Icon(Icons.movie_outlined, size: 40, color: JjColors.textDim))
: Image.file(s.data!, fit: BoxFit.cover, cacheWidth: px),
);
} else {
picture = Image.file(File(e.path),
fit: BoxFit.cover,
cacheWidth: px,
errorBuilder: (_, _, _) => const Center(child: Icon(Icons.broken_image_outlined, size: 40, color: JjColors.textDim)));
}
return Tooltip(
message: e.name,
child: Material(
color: JjColors.panelHigh,
borderRadius: BorderRadius.circular(6),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () => widget.onOpen(e),
onLongPress: () => Navigator.pop(context, e.path),
child: Stack(fit: StackFit.expand, children: [
picture,
if (video) const Align(alignment: Alignment.center, child: Icon(Icons.play_circle_outline, color: Colors.white70, size: 32)),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
color: Colors.black54,
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
child: Text(e.name, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 11, color: Colors.white)),
),
),
]),
),
),
);
}
}

View File

@ -49,6 +49,8 @@ import 'explorer_search.dart';
import 'quick_look.dart'; import 'quick_look.dart';
import 'explorer_drag.dart'; import 'explorer_drag.dart';
import 'archive_ui.dart'; import 'archive_ui.dart';
import 'explorer_grid.dart';
import 'text_viewer.dart';
import 'storage_page.dart'; import 'storage_page.dart';
import '../core/archive_ops.dart' import '../core/archive_ops.dart'
show ArchiveEntry, ArchiveKind, ArchivePasswordException, archiveKindOf, extract7zPlan, extractZipPlan, list7z, listZip, planExtract, unsafeArchivePath; show ArchiveEntry, ArchiveKind, ArchivePasswordException, archiveKindOf, extract7zPlan, extractZipPlan, list7z, listZip, planExtract, unsafeArchivePath;
@ -1430,7 +1432,37 @@ class _ExplorerPageState extends State<ExplorerPage> {
final local = await _fetch(path); final local = await _fetch(path);
if (local == null) return; if (local == null) return;
final ok = await c.services.shell.openWith(local); final ok = await c.services.shell.openWith(local);
if (!ok) _snack(_noAppMessage(path)); if (ok || !mounted) return;
// X-plore 비교 h: 글 파일을 열 앱이 없으면 앱 안의 글 뷰어로
if (textExtensions.contains(extOf(path))) return _viewText(local);
_snack(_noAppMessage(path));
}
/// X-plore 비교 h: 글 뷰어 (WebDAV 등은 받은 뒤)
Future<void> _viewText(String path) async {
final local = await _fetch(path);
if (local == null || !mounted) return;
await Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => TextViewerPage(path: local)));
}
/// X-plore 비교 g: 폴더의 그림 · 동영상을 격자로. 길게 누른 것은 탐색기에서 그 자리로
Future<void> _grid(_Pane pane, String dir) async {
await _load(pane, dir);
if (!mounted) return;
final list = _shown(pane, dir);
final at = await Navigator.of(context).push<String>(MaterialPageRoute(
builder: (_) => ExplorerGridPage(
title: vBasename(dir).isEmpty ? dir : vBasename(dir),
entries: list,
imageExts: c.settings.imageExts,
videoThumb: (e) => _BigThumbs.of(c, e),
onOpen: (e) => _open(e.path, pane: pane),
),
));
if (at == null || !mounted) return;
await _goTo(pane, dir);
pane.focused = at;
pane.changed();
} }
/// WebDAV 파일은 열거나 재생하려면 임시 폴더로 받는다 (dav_external.dart) /// WebDAV 파일은 열거나 재생하려면 임시 폴더로 받는다 (dav_external.dart)
@ -1629,8 +1661,12 @@ class _ExplorerPageState extends State<ExplorerPage> {
if (e.isDir) ('folderSize', Icons.data_usage, _sizes.busy(e.path) ? tr('폴더 크기 계산 멈추기') : tr('폴더 크기 계산')), if (e.isDir) ('folderSize', Icons.data_usage, _sizes.busy(e.path) ? tr('폴더 크기 계산 멈추기') : tr('폴더 크기 계산')),
if (e.isDir && dual) ('openOther', Icons.vertical_split_outlined, tr('다른 창에서 열기')), if (e.isDir && dual) ('openOther', Icons.vertical_split_outlined, tr('다른 창에서 열기')),
if (e.isDir) ('playFolder', Icons.play_circle_outline, tr('이 폴더의 동영상 재생')), if (e.isDir) ('playFolder', Icons.play_circle_outline, tr('이 폴더의 동영상 재생')),
// X-plore 비교 g: 그림 · 동영상을 썸네일 격자로
if (e.isDir && !dav) ('grid', Icons.grid_view, tr('격자로 보기 (그림 · 동영상)')),
if (video || audio) ('playInternal', Icons.play_circle_outline, tr('내장 플레이어로 재생')), if (video || audio) ('playInternal', Icons.play_circle_outline, tr('내장 플레이어로 재생')),
if (viewable) ('view', Icons.visibility_outlined, tr('보기')), if (viewable) ('view', Icons.visibility_outlined, tr('보기')),
// X-plore 비교 h: 어떤 파일이든 글로 열어 보기 (읽기 전용)
if (!e.isDir && !video && !audio) ('text', Icons.article_outlined, tr('글 뷰어로 보기')),
if (!e.isDir) ('openWith', Icons.open_in_new, tr('다른 앱으로 열기')), if (!e.isDir) ('openWith', Icons.open_in_new, tr('다른 앱으로 열기')),
if (!e.isDir && !video) ('openDefault', Icons.launch, tr('기본 앱으로 열기')), if (!e.isDir && !video) ('openDefault', Icons.launch, tr('기본 앱으로 열기')),
if (video && !dav) ('addMkv', Icons.playlist_add, tr('MKV 목록에 추가')), if (video && !dav) ('addMkv', Icons.playlist_add, tr('MKV 목록에 추가')),
@ -1699,6 +1735,10 @@ class _ExplorerPageState extends State<ExplorerPage> {
await _playFolder(e.path); await _playFolder(e.path);
case 'playInternal': case 'playInternal':
await _play([e.path], internal: true); await _play([e.path], internal: true);
case 'grid':
await _grid(pane, e.path);
case 'text':
await _viewText(e.path);
case 'openWith': case 'openWith':
final local = await _fetch(e.path); final local = await _fetch(e.path);
if (local != null && !await c.services.shell.openWith(local, choose: true)) _snack(_noAppMessage(e.path)); if (local != null && !await c.services.shell.openWith(local, choose: true)) _snack(_noAppMessage(e.path));

189
lib/ui/text_viewer.dart Normal file
View File

@ -0,0 +1,189 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:path/path.dart' as p;
import '../core/charset_detector.dart';
import '../core/file_ops.dart' show formatSize;
import '../core/text_codec.dart';
import '../l10n/tr.dart';
import 'theme.dart';
/// X-plore 비교 h: 글 뷰어 - 글 파일 전체를 읽기 전용으로. 큰 파일은 앞에서부터 나눠 읽고 [더 읽기],
/// 글자 코드는 스스로 판별 (UTF-8 · UTF-16 · CP949 · Shift-JIS · GBK) 하고 위에서 바꿀 수 있다. 줄 번호 · 글자 크기 · 복사.
class TextViewerPage extends StatefulWidget {
final String path;
/// 한 번에 읽는 양 (시험에서 줄인다)
final int chunk;
const TextViewerPage({super.key, required this.path, this.chunk = 256 * 1024});
@override
State<TextViewerPage> createState() => _TextViewerPageState();
}
class _TextViewerPageState extends State<TextViewerPage> {
final _bytes = BytesBuilder(copy: false);
int _size = 0;
String? _charset;
/// 사용자가 위에서 고른 글자 코드 (고르기 전에는 더 읽을 때마다 다시 판별 - 앞 조각만으로는 틀릴 수 있다)
bool _picked = false;
String _text = '';
Object? _error;
double _font = 13;
bool _lineNumbers = true;
@override
void initState() {
super.initState();
_more();
}
bool get _complete => _bytes.length >= _size;
Future<void> _more() async {
try {
final f = File(widget.path);
_size = await f.length();
final from = _bytes.length;
final to = (from + widget.chunk).clamp(0, _size);
await for (final x in f.openRead(from, to)) {
_bytes.add(x);
}
final all = _bytes.toBytes();
if (!_picked) _charset = detectCharset(Uint8List.sublistView(all, 0, all.length.clamp(0, 256 * 1024)));
_decode(all);
} catch (e) {
_error = e;
}
if (mounted) setState(() {});
}
void _decode(Uint8List all) {
String t;
// 나눠 읽은 끝에서 두 바이트 글자 (CP949 등) 가 잘리면 글자 코드 풀이가 실패한다 - 끝 바이트를 다음 조각까지 미룬다
var cut = 0;
while (true) {
try {
t = decodeText(Uint8List.sublistView(all, 0, all.length - cut), _charset!);
break;
} on FormatException {
if (_complete || cut >= 3 || cut >= all.length) rethrow;
cut++;
}
}
// 나눠 읽은 끝에서 글자가 반쯤 잘렸으면 다음 조각과 함께 읽을 때까지 뺀다
if (!_complete) {
while (t.endsWith('<EFBFBD>')) {
t = t.substring(0, t.length - 1);
}
}
_text = t;
}
@override
Widget build(BuildContext context) {
final lines = _text.split('\n');
// 좁은 화면 (폰 세로) 에서는 줄 번호 · 글자 크기를 ⋮ 메뉴로 (412 에서 파일 이름이 다 가려졌다)
final narrow = MediaQuery.sizeOf(context).width < 600;
return Scaffold(
appBar: AppBar(
title: Text(p.basename(widget.path), maxLines: 1, overflow: TextOverflow.ellipsis),
actions: [
if (_charset != null)
DropdownButton<String>(
value: supportedCharsets.containsKey(_charset) ? _charset : 'UTF-8',
underline: const SizedBox(),
items: [for (final e in supportedCharsets.entries) DropdownMenuItem(value: e.key, child: Text(e.value))],
onChanged: (v) => setState(() {
_charset = v;
_picked = true;
_decode(_bytes.toBytes());
}),
),
if (!narrow) ...[
IconButton(
tooltip: tr('줄 번호'),
icon: Icon(_lineNumbers ? Icons.format_list_numbered : Icons.notes),
onPressed: () => setState(() => _lineNumbers = !_lineNumbers),
),
IconButton(tooltip: tr('글자 작게'), icon: const Icon(Icons.text_decrease), onPressed: () => setState(() => _font = (_font - 1).clamp(9, 30))),
IconButton(tooltip: tr('글자 크게'), icon: const Icon(Icons.text_increase), onPressed: () => setState(() => _font = (_font + 1).clamp(9, 30))),
],
IconButton(
tooltip: tr('전체 복사'),
icon: const Icon(Icons.copy),
onPressed: () {
Clipboard.setData(ClipboardData(text: _text));
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(tr('복사했습니다'))));
},
),
if (narrow)
PopupMenuButton<String>(
tooltip: tr('더 보기'),
onSelected: (v) => setState(() {
switch (v) {
case 'lines':
_lineNumbers = !_lineNumbers;
case 'smaller':
_font = (_font - 1).clamp(9, 30);
case 'bigger':
_font = (_font + 1).clamp(9, 30);
}
}),
itemBuilder: (_) => [
CheckedPopupMenuItem(value: 'lines', checked: _lineNumbers, child: Text(tr('줄 번호'))),
PopupMenuItem(value: 'smaller', child: Text(tr('글자 작게'))),
PopupMenuItem(value: 'bigger', child: Text(tr('글자 크게'))),
],
),
],
),
body: _error != null
? Center(child: Text(trf('읽지 못했습니다: {0}', [_error])))
: _charset == null
? const Center(child: CircularProgressIndicator())
: Column(children: [
Expanded(
// 고르기 · 복사는 화면 전체에서 (줄마다 고르는 칸을 두면 큰 파일에서 무겁다)
child: SelectionArea(
child: Scrollbar(
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
itemCount: lines.length,
itemBuilder: (_, i) => Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
if (_lineNumbers)
SizedBox(
width: 48,
child: Text('${i + 1}',
textAlign: TextAlign.right,
style: TextStyle(fontSize: _font - 2, color: JjColors.textDim, fontFamily: 'monospace')),
),
if (_lineNumbers) const SizedBox(width: 8),
Expanded(child: Text(lines[i], style: TextStyle(fontSize: _font, fontFamily: 'monospace'))),
]),
),
),
),
),
if (!_complete)
SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.all(6),
child: Row(children: [
Expanded(
child: Text(trf('{0} / {1} 읽음', [formatSize(_bytes.length), formatSize(_size)]),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
),
FilledButton.tonal(onPressed: _more, child: Text(tr('더 읽기'))),
]),
),
),
]),
);
}
}

View File

@ -18,6 +18,8 @@ import 'package:jj_mkvmaker/ui/explorer_page.dart';
import 'package:jj_mkvmaker/ui/explorer_search.dart'; import 'package:jj_mkvmaker/ui/explorer_search.dart';
import 'package:jj_mkvmaker/ui/quick_look.dart'; import 'package:jj_mkvmaker/ui/quick_look.dart';
import 'package:jj_mkvmaker/ui/explorer_drag.dart'; import 'package:jj_mkvmaker/ui/explorer_drag.dart';
import 'package:jj_mkvmaker/ui/explorer_grid.dart';
import 'package:jj_mkvmaker/ui/text_viewer.dart';
import 'package:jj_mkvmaker/ui/storage_page.dart'; import 'package:jj_mkvmaker/ui/storage_page.dart';
import 'package:jj_mkvmaker/ui/path_label.dart'; import 'package:jj_mkvmaker/ui/path_label.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
@ -291,6 +293,31 @@ void main() {
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
}); });
testWidgets('X-plore g · h: 폴더 메뉴 [격자로 보기] (길게 = 그 자리로) · 파일 메뉴 [글 뷰어로 보기]', (tester) async {
File(p.join(left, 'sub', 'pic.png')).writeAsBytesSync([0x89, 0x50]);
await open(tester);
// g: 폴더를 길게 → 격자로 보기 → 그림만 (글 파일은 빠짐)
await tester.longPress(find.text('sub'));
await calm(tester);
await act(tester, () => tester.tap(find.text('격자로 보기 (그림 · 동영상)')));
await settle(tester, () => find.byType(ExplorerGridPage).evaluate().isNotEmpty);
expect(find.descendant(of: find.byType(ExplorerGridPage), matching: find.text('pic.png')), findsOneWidget);
expect(find.descendant(of: find.byType(ExplorerGridPage), matching: find.text('inner.txt')), findsNothing);
// 칸을 길게 누르면 탐색기에서 그 자리로 (격자 닫힘)
await tester.longPress(find.descendant(of: find.byType(ExplorerGridPage), matching: find.text('pic.png')));
await settle(tester, () => find.byType(ExplorerGridPage).evaluate().isEmpty && find.descendant(of: find.byType(ExplorerPage), matching: find.text('pic.png')).evaluate().isNotEmpty);
expect(find.descendant(of: find.byType(ExplorerPage), matching: find.text('pic.png')), findsOneWidget, reason: '탐색기에 그 폴더가 펼쳐져 보임 (길게 누른 칸의 이름 풍선은 빼고)');
// h: 파일을 길게 → 글 뷰어로 보기 (읽기 전용)
await tester.longPress(find.text('doc.txt'));
await calm(tester);
await act(tester, () => tester.tap(find.text('글 뷰어로 보기')));
await settle(tester, () => find.text('hello').evaluate().isNotEmpty);
expect(find.byType(TextViewerPage), findsOneWidget);
expect(shell.opened, isEmpty, reason: '다른 앱으로 열지 않음');
await act(tester, () async => Navigator.of(tester.element(find.byType(TextViewerPage))).pop());
await settle(tester, () => find.byType(TextViewerPage).evaluate().isEmpty);
});
testWidgets('X-plore e: Android 앱 휴지통 - 지우기 = 휴지통으로 · [되돌리기] · [휴지통] 화면에서 되살리기', (tester) async { testWidgets('X-plore e: Android 앱 휴지통 - 지우기 = 휴지통으로 · [되돌리기] · [휴지통] 화면에서 되살리기', (tester) async {
// 저장 장치는 시험 폴더 (실제 C:\ 에 .jj_trash 를 만들지 않게) // 저장 장치는 시험 폴더 (실제 C:\ 에 .jj_trash 를 만들지 않게)
ExplorerPage.debugAndroidTrash = true; ExplorerPage.debugAndroidTrash = true;

View File

@ -0,0 +1,63 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:jj_mkvmaker/core/file_ops.dart';
import 'package:jj_mkvmaker/core/text_codec.dart';
import 'package:jj_mkvmaker/ui/explorer_grid.dart';
import 'package:jj_mkvmaker/ui/text_viewer.dart';
import 'package:path/path.dart' as p;
/// X-plore 비교 g · h: 격자 보기 · 글 뷰어
void main() {
late Directory tmp;
setUp(() => tmp = Directory.systemTemp.createTempSync('jj_xptools_'));
tearDown(() => tmp.deleteSync(recursive: true));
testWidgets('h 글 뷰어: CP949 를 알아보고 나눠 읽기 [더 읽기]', (t) async {
final text = List.generate(40, (i) => '줄 $i 한글').join('\n');
final f = File(p.join(tmp.path, 'k.txt'))..writeAsBytesSync(encodeText(text, 'CP949').bytes);
await t.pumpWidget(MaterialApp(home: TextViewerPage(path: f.path, chunk: 120)));
for (var i = 0; i < 20 && find.textContaining('줄 0').evaluate().isEmpty; i++) {
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 30)));
await t.pump();
}
expect(find.text('줄 0 한글'), findsOneWidget);
expect(find.text('더 읽기'), findsOneWidget, reason: '큰 파일은 나눠 읽음');
for (var k = 0; k < 10 && find.text('더 읽기').evaluate().isNotEmpty; k++) {
await t.tap(find.text('더 읽기'));
for (var i = 0; i < 5; i++) {
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 30)));
await t.pump();
}
}
expect(find.text('더 읽기'), findsNothing, reason: '다 읽음');
expect(find.text('CP949 (EUC-KR)'), findsOneWidget);
});
testWidgets('g 격자 보기: 그림 · 동영상만, 누르면 열기', (t) async {
final png = File(p.join(tmp.path, 'a.png'))..writeAsBytesSync([0x89, 0x50]);
final opened = <String>[];
final entries = [
FileEntry(png.path, isDir: false, size: 2, modified: DateTime(2026)),
FileEntry(p.join(tmp.path, 'b.mkv'), isDir: false, size: 2, modified: DateTime(2026)),
FileEntry(p.join(tmp.path, 'c.txt'), isDir: false, size: 2, modified: DateTime(2026)),
FileEntry(p.join(tmp.path, 'dir'), isDir: true, modified: DateTime(2026)),
];
expect(ExplorerGridPage.pick(entries, const ['png']).map((e) => e.name), ['a.png', 'b.mkv']);
await t.pumpWidget(MaterialApp(
home: ExplorerGridPage(
title: 'T',
entries: entries,
imageExts: const ['png'],
videoThumb: (_) async => null,
onOpen: (e) async => opened.add(e.name),
),
));
await t.pump();
expect(find.text('a.png'), findsOneWidget);
expect(find.text('c.txt'), findsNothing);
await t.tap(find.text('b.mkv'));
expect(opened, ['b.mkv']);
});
}