파일 탐색기: 폰 한 창 · 한 번 누르면 실행 · 경로 이름 · 권한 · WebDAV 오류 안내 · 첫 화면 바로

근거: 감독 세션 지시 (폰 Z Fold6 · S10 Ultra 를 사용자처럼 써 본 결과) 1 · 5 · 6 · 10 · 11 · 12 · 14번.
감사 A-13 (지시와 반대였던 기본값) 되돌림 포함.

- 1: "모든 파일 접근" 권한이 없으면 목록 위에 이유 · 할 일 · [권한 허용 화면 열기] · [다시 확인].
     듀얼 앱 (복제한 앱) 이면 그 사실과 원래 앱으로 여는 방법을 안내. [동영상 추가] 권한 화면에도.
     허용하고 돌아오면 다시 확인해 목록을 새로 읽음.
- 5 · 14: 경로를 "SD 카드 › JJ_sdtest › jj_mkv" · "디바이스 저장공간 › Download" · "☁ ds1815 › 폴더" 로 (Android · WebDAV).
- 6: Rsync 창이 위아래면 버튼도 "위 → 아래 · 위 ← 아래 · 위 ⇄ 아래" (화살표도).
- 10 · 5: 기본값을 사용자 입장으로 - 창 배치 기본 '화면 크기 따라' (넓으면 두 창, 폰 세로는 한 창),
     Android 는 한 번 누르면 바로 실행. 예전 기본값 (사용자가 고른 것 아님) 은 한 번 옮김, 고른 값은 그대로.
- 11: 창마다 따로 읽음 - WebDAV 서버를 기다리는 동안에도 이 기기 파일이 바로 보임 (읽는 중 막대).
- 12: 읽을 수 없으면 이유 (서버에 닿지 않음 → 같은 네트워크 · Tailscale, 비밀번호, 인증서, 권한, 없음) ·
     [다시 시도] · [서버 설정 고치기].
- 감사 A-13: ZIP 은 기본 목록 보기 (사용자 지시 10-08 "zip 은 기본 목록 보기, 만화 보기는 설정에서"). 예전 기본 (만화) 은 한 번 끔.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
묶음3
RyuJongJin 2026-10-08 23:23:48 +09:00
parent b39678f8f6
commit fead23ce2f
7 changed files with 310 additions and 19 deletions

View File

@ -62,6 +62,7 @@
"API 키": "API key",
"API 키 (필수, 무료)": "API key (required, free)",
"AV1 영상은 이 기기에서 다시 인코딩할 수 없습니다. 코덱을 \"원본 유지\" 로 바꿔 MKV 를 만드세요. (PC 판은 됩니다)": "AV1 video cannot be re-encoded on this device. Change the codec to \"Keep original\" to make the MKV. (The PC version can.)",
"Android 가 막은 폴더 (Android/data 등) 이거나 \"모든 파일에 대한 접근\" 권한이 없습니다.": "This folder is blocked by Android (such as Android/data), or \"All files access\" is not allowed.",
"Android 는 rsync 를 내려받을 수 없습니다. 직접 지정하세요.": "rsync cannot be downloaded on Android. Specify it yourself.",
"Android 는 버전이 낮은 앱을 지금 앱 위에 설치하지 못합니다. 그래서:\n1. v{0} 설치 파일과 지금 설정 (동영상 목록 · 즐겨찾기 포함) 을 Download/JJ_MKVMaker 에 저장합니다.\n2. 이 앱을 지웁니다 (Android 확인 창).\n3. 파일 앱에서 Download/JJ_MKVMaker 의 APK 를 눌러 설치합니다.\n4. 이 기능이 있는 버전이면 시작할 때 저장해 둔 설정을 되살릴지 묻습니다. 더 예전 버전은 설정을 다시 해야 합니다.\n※ 자막 API 키 · 비밀번호는 저장하지 않으므로 다시 넣어야 합니다.": "Android cannot install a lower version over the current app. So:\n1. Save the v{0} install file and the current settings (including the video list and bookmarks) to Download/JJ_MKVMaker.\n2. Uninstall this app (Android confirmation).\n3. Tap the APK in Download/JJ_MKVMaker in a file manager to install it.\n4. Versions with this feature ask at startup whether to restore the saved settings. Older versions need to be set up again.\n* Subtitle API keys and passwords are not saved, so enter them again.",
"Android 설치 화면에서 [설치] 를 누르세요. 설정 · 받은 파일 · AI 모델은 그대로 남습니다.\n(처음이면 \"이 출처의 앱 설치 허용\" 을 켜야 할 수 있습니다)": "Press [Install] on the Android install screen. Settings, downloaded files and AI models stay as they are.\n(The first time, you may need to turn on \"Allow from this source\")",
@ -162,6 +163,7 @@
"ZIP · CBZ 를 만화로 보기": "Read ZIP · CBZ as comics",
"ZIP 옆 새 폴더 \"{0}\" 로": "To a new folder \"{0}\" next to the ZIP",
"ZIP 이 있는 폴더로": "To the ZIP's folder",
"[서버 설정 고치기] 에서 아이디 · 비밀번호를 확인하세요.": "Check the user name and password in [Edit server settings].",
"aria2 RPC 에 연결할 수 없습니다.": "Cannot connect to aria2 RPC.",
"aria2 오류: {0}": "aria2 error: {0}",
"cookies.txt (Netscape 형식)": "cookies.txt (Netscape format)",
@ -378,6 +380,7 @@
"누르면 일정 바꾸기": "Tap to change the schedule",
"다 받는 대로 MKV 만들기의 동영상 목록에 넣고, 다운로드 목록에서는 뺍니다 (다운로드 화면의 체크 상자와 같음)": "As soon as they finish, put them in the Make MKV video list and remove them from the download list (same as the checkbox on the download screen)",
"다 받으면 MKV 만들기의 동영상 목록에 자동으로 넣고, 이 다운로드 목록에서는 뺍니다 (받은 파일은 그대로)": "When finished, automatically add to the Make MKV video list and remove from this download list (the downloaded file stays)",
"다른 곳에서 지웠거나 옮겼을 수 있습니다. 위 폴더로 가 보세요.": "It may have been deleted or moved elsewhere. Try the parent folder.",
"다른 버전 ({0}) 을 쓰다가 v{1} 로 돌아왔습니다. 그 버전은 이 버전의 설정 일부를 모를 수 있습니다.\nv{1} 를 마지막으로 쓸 때 보관해 둔 설정 (동영상 목록 · 즐겨찾기 포함) 으로 되돌릴까요?": "You came back to v{1} after using another version ({0}). That version may not know some of this version's settings.\nRestore the settings kept when you last used v{1} (including the video list and bookmarks)?",
"다른 버전 고르기": "Choose another version",
"다른 브라우저의 즐겨찾기 가져오기": "Import bookmarks from another browser",
@ -396,6 +399,7 @@
"다시 만들기 대기로": "Back to waiting for remake",
"다시 설치": "Reinstall",
"다시 세기": "Recount",
"다시 시도": "Try again",
"다시 시작": "Restart",
"다시 켤 때 동기화": "Sync on restart",
"다운로드": "Downloads",
@ -641,6 +645,11 @@
"새로고침 (F5)": "Reload (F5)",
"색 보정": "Color correction",
"색온도": "Temperature",
"서버 설정 고치기": "Edit server settings",
"서버 인증서를 확인할 수 없습니다": "Cannot verify the server certificate",
"서버에 닿지 않습니다": "Cannot reach the server",
"서버에서 이 계정의 권한을 확인하세요.": "Check this account's permissions on the server.",
"서버와 같은 네트워크에 있거나 VPN (예: Tailscale) 이 켜져 있어야 합니다. 켠 뒤 [다시 시도] 를 누르세요.": "You need to be on the same network as the server or have a VPN (e.g. Tailscale) on. Turn it on, then tap [Try again].",
"선택": "Select",
"선택 {0} / {1}개": "Selected {0} / {1}",
"선택 끝": "Done",
@ -731,6 +740,7 @@
"아래에 줄 추가": "Add line below",
"아이디": "User name",
"아이디 (선택)": "Username (optional)",
"아이디 또는 비밀번호가 맞지 않습니다": "The user name or password is wrong",
"아이디·비밀번호는 환경 설정에서 넣을 수 있습니다 (선택, 하루 받기 횟수가 늘어남).": "You can enter the username · password in Settings (optional, raises the daily download limit).",
"아이콘 · 목록 모양: X-plore (썸네일 · 두 줄) · Windows 탐색기 (컬러 아이콘 · 열) · Total Commander (촘촘한 목록 · [폴더])": "Icons · list look: X-plore (thumbnails · two lines) · Windows Explorer (color icons · columns) · Total Commander (dense list · [folders])",
"아직 만든 MKV · 자막이 없습니다: {0}": "No MKV · subtitles made yet: {0}",
@ -891,6 +901,8 @@
"이 폴더 선택": "Choose this folder",
"이 폴더 전체": "This whole folder",
"이 폴더를 rsync 원본 · 대상으로 고르기": "Choose this folder as rsync source · target",
"이 폴더를 볼 권한이 없습니다": "No permission to view this folder",
"이 폴더를 읽을 권한이 없습니다": "No permission to read this folder",
"이 폴더에 동영상이 없습니다.": "There are no videos in this folder.",
"이 폴더에 새 폴더": "New folder in this folder",
"이 폴더에 현재 페이지 추가": "Add the current page to this folder",
@ -942,6 +954,7 @@
"일반": "General",
"일본어": "Japanese",
"일시정지": "Pause",
"읽을 수 없습니다": "Cannot read",
"읽을 수 없음": "Cannot read",
"임시 파일 · 남은 조각 정리": "Clean temporary files · leftovers",
"자동": "Auto",
@ -1096,6 +1109,7 @@
"지금 쓰는 버전: v{0}": "Current version: v{0}",
"지금 쓰는 버전: v{0} → 새 버전: v{1}": "Current version: v{0} → new version: v{1}",
"지금: {0}": "Now: {0}",
"지금은 듀얼 앱 (복제한 앱) 입니다. Android 가 듀얼 앱에는 이 권한을 주지 않을 수 있습니다. 허용 화면에서 켤 수 없으면 배지 없는 원래 JJ_MKVMaker 아이콘으로 여세요.": "This is the Dual App (cloned app). Android may not grant this permission to dual apps. If you can't turn it on, open the original JJ_MKVMaker icon without the badge.",
"지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "The last run did not end normally (last check {0}:{1}). It has been noted in the job log.",
"지난 작업 기록": "Old job logs",
"지우기": "Delete",
@ -1110,6 +1124,7 @@
"직접 지정": "Specify",
"진행 중인 작업 없음": "No jobs in progress",
"진행 중인 작업이 있습니다: {0}\n중지하고 끝낼까요?": "A job is in progress: {0}\nStop it and quit?",
"집 NAS 처럼 자체 서명 인증서면 [서버 설정 고치기] 에서 \"인증서 확인 안 함\" 을 켜세요.": "If it uses a self-signed certificate (like a home NAS), turn on \"Don't verify certificate\" in [Edit server settings].",
"창": "Panes",
"창 · 작업 표시줄 · 트레이와 앱 안 왼쪽 위 버튼의 아이콘입니다 (실행 파일 · 바탕 화면 바로 가기 아이콘은 그대로)": "Icon of the window, taskbar, tray and the top-left button in the app (the program file and desktop shortcut icons stay the same)",
"창 배치": "Pane layout",
@ -1188,6 +1203,7 @@
"파일 확인 중…": "Checking files…",
"파일만 골랐을 때": "When only files are selected",
"파일을 분석할 수 없습니다: {0}": "Cannot analyze the file: {0}",
"파일이 보이지 않습니다: \"모든 파일에 대한 접근\" 권한이 없습니다": "Files are not shown: \"All files access\" is not allowed",
"팝업 끄기": "Close pop-up",
"팝업 보기": "Pop-up view",
"패키지 라이선스": "Package licenses",
@ -1217,6 +1233,8 @@
"폴더: {0}": "Folder: {0}",
"폴더가 들어 있을 때": "When folders are included",
"폴더가 아닙니다: {0}": "Not a folder: {0}",
"폴더가 없습니다": "The folder does not exist",
"폴더는 보여도 그 안의 동영상 · 파일은 이 권한이 있어야 보입니다. 허용 화면에서 JJ_MKVMaker 를 켜고 돌아오세요.": "Folders are shown, but the videos and files inside need this permission. Turn on JJ_MKVMaker on the permission screen and come back.",
"폴더를 만들 수 없습니다: {0}": "Cannot create the folder: {0}",
"폴더를 자기 자신 안으로 {0} 수 없습니다: {1}": "Cannot {0} a folder into itself: {1}",
"폴더만 lsync 로 옮길 수 있습니다.": "Only folders can be moved to lsync.",
@ -1247,6 +1265,8 @@
"필수 프로그램이 모두 설치되어 있습니다.": "All required programs are installed.",
"하위 폴더가 없습니다": "No subfolders",
"한 번: 바로 실행": "Single: open right away",
"한 번: 바로 실행 (기본)": "Single tap: open (default)",
"한 번: 선택 · 두 번: 실행": "Single tap: select · double tap: open",
"한 번: 선택 · 두 번: 실행 (기본)": "Single: select · Double: open (default)",
"한 번에": "All at once",
"한 쪽 맞추기": "Fit page",
@ -1282,6 +1302,7 @@
"화면 작게": "Smaller",
"화면 크게": "Larger",
"화면 크기": "Size",
"화면 크기 따라 (기본)": "By screen size (default)",
"화면이 있는 컴포넌트는 하나 이상 남아 있어야 합니다.": "At least one component with a screen must stay installed.",
"화질": "Quality",
"확인": "OK",

View File

@ -62,6 +62,7 @@
"API 키": "API キー",
"API 키 (필수, 무료)": "API キー(必須・無料)",
"AV1 영상은 이 기기에서 다시 인코딩할 수 없습니다. 코덱을 \"원본 유지\" 로 바꿔 MKV 를 만드세요. (PC 판은 됩니다)": "AV1 動画はこの端末では再エンコードできません。コーデックを「元のまま」に変えて MKV を作成してください。(PC 版はできます)",
"Android 가 막은 폴더 (Android/data 등) 이거나 \"모든 파일에 대한 접근\" 권한이 없습니다.": "Android が制限しているフォルダー (Android/data など) か、「すべてのファイルへのアクセス」権限がありません。",
"Android 는 rsync 를 내려받을 수 없습니다. 직접 지정하세요.": "Android では rsync をダウンロードできません。直接指定してください。",
"Android 는 버전이 낮은 앱을 지금 앱 위에 설치하지 못합니다. 그래서:\n1. v{0} 설치 파일과 지금 설정 (동영상 목록 · 즐겨찾기 포함) 을 Download/JJ_MKVMaker 에 저장합니다.\n2. 이 앱을 지웁니다 (Android 확인 창).\n3. 파일 앱에서 Download/JJ_MKVMaker 의 APK 를 눌러 설치합니다.\n4. 이 기능이 있는 버전이면 시작할 때 저장해 둔 설정을 되살릴지 묻습니다. 더 예전 버전은 설정을 다시 해야 합니다.\n※ 자막 API 키 · 비밀번호는 저장하지 않으므로 다시 넣어야 합니다.": "Android はバージョンの低いアプリを今のアプリの上にインストールできません。そこで:\n1. v{0} のインストールファイルと今の設定 (動画リスト · ブックマークを含む) を Download/JJ_MKVMaker に保存します。\n2. このアプリを削除します (Android の確認画面)。\n3. ファイルアプリで Download/JJ_MKVMaker の APK をタップしてインストールします。\n4. この機能があるバージョンなら起動時に保存した設定を復元するか尋ねます。それより古いバージョンは設定し直す必要があります。\n※ 字幕 API キー · パスワードは保存しないので再入力が必要です。",
"Android 설치 화면에서 [설치] 를 누르세요. 설정 · 받은 파일 · AI 모델은 그대로 남습니다.\n(처음이면 \"이 출처의 앱 설치 허용\" 을 켜야 할 수 있습니다)": "Android のインストール画面で[インストール]を押してください。設定・ダウンロードしたファイル・AI モデルはそのまま残ります。\n(初回は「この提供元のアプリを許可」をオンにする必要があるかもしれません)",
@ -162,6 +163,7 @@
"ZIP · CBZ 를 만화로 보기": "ZIP・CBZ をマンガとして表示",
"ZIP 옆 새 폴더 \"{0}\" 로": "ZIP の隣の新しいフォルダー「{0}」へ",
"ZIP 이 있는 폴더로": "ZIP のあるフォルダーへ",
"[서버 설정 고치기] 에서 아이디 · 비밀번호를 확인하세요.": "[サーバー設定を修正] でユーザー名・パスワードを確認してください。",
"aria2 RPC 에 연결할 수 없습니다.": "aria2 RPC に接続できません。",
"aria2 오류: {0}": "aria2 エラー: {0}",
"cookies.txt (Netscape 형식)": "cookies.txt(Netscape 形式)",
@ -378,6 +380,7 @@
"누르면 일정 바꾸기": "タップで予定を変更",
"다 받는 대로 MKV 만들기의 동영상 목록에 넣고, 다운로드 목록에서는 뺍니다 (다운로드 화면의 체크 상자와 같음)": "ダウンロードが終わり次第 MKV 作成の動画リストに入れ、ダウンロードリストからは外します(ダウンロード画面のチェックボックスと同じ)",
"다 받으면 MKV 만들기의 동영상 목록에 자동으로 넣고, 이 다운로드 목록에서는 뺍니다 (받은 파일은 그대로)": "ダウンロードが終わると MKV 作成の動画リストに自動で入れ、このダウンロードリストからは外します(ダウンロードしたファイルはそのまま)",
"다른 곳에서 지웠거나 옮겼을 수 있습니다. 위 폴더로 가 보세요.": "別の場所で削除・移動された可能性があります。上のフォルダーへ移動してみてください。",
"다른 버전 ({0}) 을 쓰다가 v{1} 로 돌아왔습니다. 그 버전은 이 버전의 설정 일부를 모를 수 있습니다.\nv{1} 를 마지막으로 쓸 때 보관해 둔 설정 (동영상 목록 · 즐겨찾기 포함) 으로 되돌릴까요?": "別のバージョン ({0}) を使ったあと v{1} に戻りました。そのバージョンはこのバージョンの設定の一部を知らない可能性があります。\nv{1} を最後に使ったときに保管した設定 (動画リスト · ブックマークを含む) に戻しますか?",
"다른 버전 고르기": "別のバージョンを選ぶ",
"다른 브라우저의 즐겨찾기 가져오기": "他のブラウザーのブックマークを取り込む",
@ -396,6 +399,7 @@
"다시 만들기 대기로": "作り直し待ちに戻す",
"다시 설치": "再インストール",
"다시 세기": "再カウント",
"다시 시도": "再試行",
"다시 시작": "再起動",
"다시 켤 때 동기화": "再起動時の同期",
"다운로드": "ダウンロード",
@ -641,6 +645,11 @@
"새로고침 (F5)": "再読み込み(F5)",
"색 보정": "カラー補正",
"색온도": "色温度",
"서버 설정 고치기": "サーバー設定を修正",
"서버 인증서를 확인할 수 없습니다": "サーバー証明書を確認できません",
"서버에 닿지 않습니다": "サーバーに接続できません",
"서버에서 이 계정의 권한을 확인하세요.": "サーバーでこのアカウントの権限を確認してください。",
"서버와 같은 네트워크에 있거나 VPN (예: Tailscale) 이 켜져 있어야 합니다. 켠 뒤 [다시 시도] 를 누르세요.": "サーバーと同じネットワークにいるか、VPN (例: Tailscale) をオンにする必要があります。オンにしてから [再試行] を押してください。",
"선택": "選択",
"선택 {0} / {1}개": "選択 {0} / {1}個",
"선택 끝": "選択終了",
@ -731,6 +740,7 @@
"아래에 줄 추가": "下に行を追加",
"아이디": "ユーザー名",
"아이디 (선택)": "ユーザー名(任意)",
"아이디 또는 비밀번호가 맞지 않습니다": "ユーザー名またはパスワードが正しくありません",
"아이디·비밀번호는 환경 설정에서 넣을 수 있습니다 (선택, 하루 받기 횟수가 늘어남).": "ユーザー名・パスワードは環境設定で入力できます(任意、1日のダウンロード回数が増えます)。",
"아이콘 · 목록 모양: X-plore (썸네일 · 두 줄) · Windows 탐색기 (컬러 아이콘 · 열) · Total Commander (촘촘한 목록 · [폴더])": "アイコン · 一覧の見た目: X-plore (サムネイル · 2 行) · Windows エクスプローラー (カラーアイコン · 列) · Total Commander (詰めた一覧 · [フォルダー])",
"아직 만든 MKV · 자막이 없습니다: {0}": "まだ作成した MKV・字幕がありません: {0}",
@ -891,6 +901,8 @@
"이 폴더 선택": "このフォルダーを選択",
"이 폴더 전체": "このフォルダー全体",
"이 폴더를 rsync 원본 · 대상으로 고르기": "このフォルダーを rsync の元 · 対象に選ぶ",
"이 폴더를 볼 권한이 없습니다": "このフォルダーを表示する権限がありません",
"이 폴더를 읽을 권한이 없습니다": "このフォルダーを読む権限がありません",
"이 폴더에 동영상이 없습니다.": "このフォルダーに動画がありません。",
"이 폴더에 새 폴더": "このフォルダーに新しいフォルダー",
"이 폴더에 현재 페이지 추가": "このフォルダーに現在のページを追加",
@ -942,6 +954,7 @@
"일반": "一般",
"일본어": "日本語",
"일시정지": "一時停止",
"읽을 수 없습니다": "読み込めません",
"읽을 수 없음": "読み取れません",
"임시 파일 · 남은 조각 정리": "一時ファイル・残った断片を整理",
"자동": "自動",
@ -1096,6 +1109,7 @@
"지금 쓰는 버전: v{0}": "使用中のバージョン: v{0}",
"지금 쓰는 버전: v{0} → 새 버전: v{1}": "現在のバージョン: v{0} → 新しいバージョン: v{1}",
"지금: {0}": "現在: {0}",
"지금은 듀얼 앱 (복제한 앱) 입니다. Android 가 듀얼 앱에는 이 권한을 주지 않을 수 있습니다. 허용 화면에서 켤 수 없으면 배지 없는 원래 JJ_MKVMaker 아이콘으로 여세요.": "これはデュアルアプリ (複製したアプリ) です。Android はデュアルアプリにこの権限を与えない場合があります。許可画面でオンにできない場合は、バッジのない元の JJ_MKVMaker アイコンから開いてください。",
"지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "前回の実行が正常に終了しませんでした(最終確認 {0}:{1})。作業記録に残しました。",
"지난 작업 기록": "過去の作業記録",
"지우기": "削除",
@ -1110,6 +1124,7 @@
"직접 지정": "直接指定",
"진행 중인 작업 없음": "進行中の作業なし",
"진행 중인 작업이 있습니다: {0}\n중지하고 끝낼까요?": "進行中の作業があります: {0}\n中止して終了しますか?",
"집 NAS 처럼 자체 서명 인증서면 [서버 설정 고치기] 에서 \"인증서 확인 안 함\" 을 켜세요.": "自宅 NAS のように自己署名証明書なら、[サーバー設定を修正] で「証明書を検証しない」をオンにしてください。",
"창": "ペイン",
"창 · 작업 표시줄 · 트레이와 앱 안 왼쪽 위 버튼의 아이콘입니다 (실행 파일 · 바탕 화면 바로 가기 아이콘은 그대로)": "ウィンドウ・タスクバー・トレイとアプリ内左上のボタンのアイコンです(実行ファイル・デスクトップのショートカットのアイコンはそのまま)",
"창 배치": "ペインの配置",
@ -1188,6 +1203,7 @@
"파일 확인 중…": "ファイルを確認中…",
"파일만 골랐을 때": "ファイルだけを選んだとき",
"파일을 분석할 수 없습니다: {0}": "ファイルを分析できません: {0}",
"파일이 보이지 않습니다: \"모든 파일에 대한 접근\" 권한이 없습니다": "ファイルが表示されません: 「すべてのファイルへのアクセス」権限がありません",
"팝업 끄기": "ポップアップを閉じる",
"팝업 보기": "ポップアップ表示",
"패키지 라이선스": "パッケージのライセンス",
@ -1217,6 +1233,8 @@
"폴더: {0}": "フォルダー: {0}",
"폴더가 들어 있을 때": "フォルダーを含むとき",
"폴더가 아닙니다: {0}": "フォルダーではありません: {0}",
"폴더가 없습니다": "フォルダーがありません",
"폴더는 보여도 그 안의 동영상 · 파일은 이 권한이 있어야 보입니다. 허용 화면에서 JJ_MKVMaker 를 켜고 돌아오세요.": "フォルダーは見えても、中の動画・ファイルはこの権限が必要です。許可画面で JJ_MKVMaker をオンにして戻ってください。",
"폴더를 만들 수 없습니다: {0}": "フォルダーを作成できません: {0}",
"폴더를 자기 자신 안으로 {0} 수 없습니다: {1}": "フォルダーを自分自身の中へ{0}ことはできません: {1}",
"폴더만 lsync 로 옮길 수 있습니다.": "lsync へ移せるのはフォルダーだけです。",
@ -1247,6 +1265,8 @@
"필수 프로그램이 모두 설치되어 있습니다.": "必須プログラムはすべてインストールされています。",
"하위 폴더가 없습니다": "サブフォルダーがありません",
"한 번: 바로 실행": "1 回: すぐに開く",
"한 번: 바로 실행 (기본)": "1 回: すぐに開く (既定)",
"한 번: 선택 · 두 번: 실행": "1 回: 選択・2 回: 開く",
"한 번: 선택 · 두 번: 실행 (기본)": "1 回: 選択 · 2 回: 開く (既定)",
"한 번에": "まとめて 1 回",
"한 쪽 맞추기": "1 ページに合わせる",
@ -1282,6 +1302,7 @@
"화면 작게": "画面を小さく",
"화면 크게": "画面を大きく",
"화면 크기": "画面サイズ",
"화면 크기 따라 (기본)": "画面サイズに合わせる (既定)",
"화면이 있는 컴포넌트는 하나 이상 남아 있어야 합니다.": "画面のあるコンポーネントを少なくとも1つ残す必要があります。",
"화질": "画質",
"확인": "OK",

View File

@ -62,6 +62,7 @@
"API 키": "API 密钥",
"API 키 (필수, 무료)": "API 密钥(必填,免费)",
"AV1 영상은 이 기기에서 다시 인코딩할 수 없습니다. 코덱을 \"원본 유지\" 로 바꿔 MKV 를 만드세요. (PC 판은 됩니다)": "此设备无法重新编码 AV1 视频。请将编解码器改为“保持原样”后制作 MKV。(PC 版可以)",
"Android 가 막은 폴더 (Android/data 등) 이거나 \"모든 파일에 대한 접근\" 권한이 없습니다.": "这是 Android 限制的文件夹 (如 Android/data),或没有“所有文件访问”权限。",
"Android 는 rsync 를 내려받을 수 없습니다. 직접 지정하세요.": "Android 无法下载 rsync,请自行指定。",
"Android 는 버전이 낮은 앱을 지금 앱 위에 설치하지 못합니다. 그래서:\n1. v{0} 설치 파일과 지금 설정 (동영상 목록 · 즐겨찾기 포함) 을 Download/JJ_MKVMaker 에 저장합니다.\n2. 이 앱을 지웁니다 (Android 확인 창).\n3. 파일 앱에서 Download/JJ_MKVMaker 의 APK 를 눌러 설치합니다.\n4. 이 기능이 있는 버전이면 시작할 때 저장해 둔 설정을 되살릴지 묻습니다. 더 예전 버전은 설정을 다시 해야 합니다.\n※ 자막 API 키 · 비밀번호는 저장하지 않으므로 다시 넣어야 합니다.": "Android 无法在当前应用上安装版本更低的应用。因此:\n1. 将 v{0} 安装文件和当前设置 (包括视频列表 · 书签) 保存到 Download/JJ_MKVMaker。\n2. 卸载此应用 (Android 确认窗口)。\n3. 在文件应用中点按 Download/JJ_MKVMaker 中的 APK 进行安装。\n4. 带有此功能的版本会在启动时询问是否恢复保存的设置。更早的版本需要重新设置。\n※ 字幕 API 密钥 · 密码不会保存,需要重新输入。",
"Android 설치 화면에서 [설치] 를 누르세요. 설정 · 받은 파일 · AI 모델은 그대로 남습니다.\n(처음이면 \"이 출처의 앱 설치 허용\" 을 켜야 할 수 있습니다)": "请在 Android 安装界面点击[安装]。设置、已下载的文件和 AI 模型都会保留。\n(首次可能需要开启“允许安装此来源的应用”)",
@ -162,6 +163,7 @@
"ZIP · CBZ 를 만화로 보기": "将 ZIP · CBZ 作为漫画阅读",
"ZIP 옆 새 폴더 \"{0}\" 로": "到 ZIP 旁的新文件夹“{0}”",
"ZIP 이 있는 폴더로": "到 ZIP 所在文件夹",
"[서버 설정 고치기] 에서 아이디 · 비밀번호를 확인하세요.": "请在 [修改服务器设置] 中检查用户名和密码。",
"aria2 RPC 에 연결할 수 없습니다.": "无法连接到 aria2 RPC。",
"aria2 오류: {0}": "aria2 错误: {0}",
"cookies.txt (Netscape 형식)": "cookies.txt(Netscape 格式)",
@ -378,6 +380,7 @@
"누르면 일정 바꾸기": "点按以更改计划",
"다 받는 대로 MKV 만들기의 동영상 목록에 넣고, 다운로드 목록에서는 뺍니다 (다운로드 화면의 체크 상자와 같음)": "下载完成后立即放入制作 MKV 的视频列表,并从下载列表移除(与下载界面的复选框相同)",
"다 받으면 MKV 만들기의 동영상 목록에 자동으로 넣고, 이 다운로드 목록에서는 뺍니다 (받은 파일은 그대로)": "下载完成后自动加入制作 MKV 的视频列表,并从此下载列表移除(已下载的文件保留)",
"다른 곳에서 지웠거나 옮겼을 수 있습니다. 위 폴더로 가 보세요.": "可能已在别处被删除或移动。请返回上级文件夹。",
"다른 버전 ({0}) 을 쓰다가 v{1} 로 돌아왔습니다. 그 버전은 이 버전의 설정 일부를 모를 수 있습니다.\nv{1} 를 마지막으로 쓸 때 보관해 둔 설정 (동영상 목록 · 즐겨찾기 포함) 으로 되돌릴까요?": "使用其他版本 ({0}) 后回到了 v{1}。该版本可能不认识此版本的部分设置。\n要恢复最后一次使用 v{1} 时保存的设置 (包括视频列表 · 书签) 吗?",
"다른 버전 고르기": "选择其他版本",
"다른 브라우저의 즐겨찾기 가져오기": "导入其他浏览器的书签",
@ -396,6 +399,7 @@
"다시 만들기 대기로": "改回等待重新制作",
"다시 설치": "重新安装",
"다시 세기": "重新统计",
"다시 시도": "重试",
"다시 시작": "重新启动",
"다시 켤 때 동기화": "重新启动时同步",
"다운로드": "下载",
@ -641,6 +645,11 @@
"새로고침 (F5)": "刷新(F5)",
"색 보정": "色彩校正",
"색온도": "色温",
"서버 설정 고치기": "修改服务器设置",
"서버 인증서를 확인할 수 없습니다": "无法验证服务器证书",
"서버에 닿지 않습니다": "无法连接服务器",
"서버에서 이 계정의 권한을 확인하세요.": "请在服务器上检查此账户的权限。",
"서버와 같은 네트워크에 있거나 VPN (예: Tailscale) 이 켜져 있어야 합니다. 켠 뒤 [다시 시도] 를 누르세요.": "需要与服务器在同一网络,或打开 VPN (如 Tailscale)。打开后点 [重试]。",
"선택": "选择",
"선택 {0} / {1}개": "已选 {0} / {1} 个",
"선택 끝": "完成选择",
@ -731,6 +740,7 @@
"아래에 줄 추가": "在下方添加一行",
"아이디": "用户名",
"아이디 (선택)": "用户名(可选)",
"아이디 또는 비밀번호가 맞지 않습니다": "用户名或密码错误",
"아이디·비밀번호는 환경 설정에서 넣을 수 있습니다 (선택, 하루 받기 횟수가 늘어남).": "可在设置中填写用户名 · 密码(可选,可增加每日下载次数)。",
"아이콘 · 목록 모양: X-plore (썸네일 · 두 줄) · Windows 탐색기 (컬러 아이콘 · 열) · Total Commander (촘촘한 목록 · [폴더])": "图标 · 列表外观: X-plore (缩略图 · 两行) · Windows 资源管理器 (彩色图标 · 列) · Total Commander (紧凑列表 · [文件夹])",
"아직 만든 MKV · 자막이 없습니다: {0}": "尚未制作 MKV · 字幕: {0}",
@ -891,6 +901,8 @@
"이 폴더 선택": "选择此文件夹",
"이 폴더 전체": "整个文件夹",
"이 폴더를 rsync 원본 · 대상으로 고르기": "选择此文件夹作为 rsync 源 · 目标",
"이 폴더를 볼 권한이 없습니다": "没有查看此文件夹的权限",
"이 폴더를 읽을 권한이 없습니다": "没有读取此文件夹的权限",
"이 폴더에 동영상이 없습니다.": "此文件夹中没有视频。",
"이 폴더에 새 폴더": "在此文件夹中新建文件夹",
"이 폴더에 현재 페이지 추가": "将当前页面添加到此文件夹",
@ -942,6 +954,7 @@
"일반": "常规",
"일본어": "日语",
"일시정지": "暂停",
"읽을 수 없습니다": "无法读取",
"읽을 수 없음": "无法读取",
"임시 파일 · 남은 조각 정리": "清理临时文件 · 残留碎片",
"자동": "自动",
@ -1096,6 +1109,7 @@
"지금 쓰는 버전: v{0}": "当前版本: v{0}",
"지금 쓰는 버전: v{0} → 새 버전: v{1}": "当前版本: v{0} → 新版本: v{1}",
"지금: {0}": "当前: {0}",
"지금은 듀얼 앱 (복제한 앱) 입니다. Android 가 듀얼 앱에는 이 권한을 주지 않을 수 있습니다. 허용 화면에서 켤 수 없으면 배지 없는 원래 JJ_MKVMaker 아이콘으로 여세요.": "当前是双开应用 (复制的应用)。Android 可能不会给双开应用此权限。如果无法开启,请从没有徽标的原 JJ_MKVMaker 图标打开。",
"지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "上次运行未正常结束(最后检查 {0}:{1})。已记录在任务记录中。",
"지난 작업 기록": "旧任务记录",
"지우기": "删除",
@ -1110,6 +1124,7 @@
"직접 지정": "手动指定",
"진행 중인 작업 없음": "没有进行中的任务",
"진행 중인 작업이 있습니다: {0}\n중지하고 끝낼까요?": "有正在进行的任务: {0}\n要停止并退出吗?",
"집 NAS 처럼 자체 서명 인증서면 [서버 설정 고치기] 에서 \"인증서 확인 안 함\" 을 켜세요.": "如果是自签名证书 (如家用 NAS),请在 [修改服务器设置] 中开启“不验证证书”。",
"창": "窗格",
"창 · 작업 표시줄 · 트레이와 앱 안 왼쪽 위 버튼의 아이콘입니다 (실행 파일 · 바탕 화면 바로 가기 아이콘은 그대로)": "窗口、任务栏、托盘以及应用内左上角按钮的图标(程序文件和桌面快捷方式的图标不变)",
"창 배치": "窗格布局",
@ -1188,6 +1203,7 @@
"파일 확인 중…": "正在检查文件…",
"파일만 골랐을 때": "只选择文件时",
"파일을 분석할 수 없습니다: {0}": "无法分析文件: {0}",
"파일이 보이지 않습니다: \"모든 파일에 대한 접근\" 권한이 없습니다": "文件不可见: 没有“所有文件访问”权限",
"팝업 끄기": "关闭弹出窗口",
"팝업 보기": "弹出窗口观看",
"패키지 라이선스": "软件包许可",
@ -1217,6 +1233,8 @@
"폴더: {0}": "文件夹: {0}",
"폴더가 들어 있을 때": "包含文件夹时",
"폴더가 아닙니다: {0}": "不是文件夹: {0}",
"폴더가 없습니다": "文件夹不存在",
"폴더는 보여도 그 안의 동영상 · 파일은 이 권한이 있어야 보입니다. 허용 화면에서 JJ_MKVMaker 를 켜고 돌아오세요.": "虽然能看到文件夹,但其中的视频和文件需要此权限。请在权限界面开启 JJ_MKVMaker 后返回。",
"폴더를 만들 수 없습니다: {0}": "无法创建文件夹: {0}",
"폴더를 자기 자신 안으로 {0} 수 없습니다: {1}": "不能将文件夹{0}到其自身内部: {1}",
"폴더만 lsync 로 옮길 수 있습니다.": "只有文件夹可以移到 lsync。",
@ -1247,6 +1265,8 @@
"필수 프로그램이 모두 설치되어 있습니다.": "所有必需程序均已安装。",
"하위 폴더가 없습니다": "没有子文件夹",
"한 번: 바로 실행": "单击: 立即打开",
"한 번: 바로 실행 (기본)": "单击: 直接打开 (默认)",
"한 번: 선택 · 두 번: 실행": "单击: 选择 · 双击: 打开",
"한 번: 선택 · 두 번: 실행 (기본)": "单击: 选择 · 双击: 打开 (默认)",
"한 번에": "一次执行",
"한 쪽 맞추기": "适应整页",
@ -1282,6 +1302,7 @@
"화면 작게": "缩小界面",
"화면 크게": "放大界面",
"화면 크기": "画面大小",
"화면 크기 따라 (기본)": "按屏幕大小 (默认)",
"화면이 있는 컴포넌트는 하나 이상 남아 있어야 합니다.": "至少要保留一个带界面的组件。",
"화질": "画质",
"확인": "确定",

View File

@ -285,11 +285,11 @@ class AppSettings {
// ── 파일 탐색기 (X-plore 참고) ──
/// 창: 'dual' 두 창 (기본) · 'split' 왼쪽 폴더 트리 + 오른쪽 그 폴더의 파일 목록 · 'single' 한 창
String explorerLayout = 'dual';
String explorerLayout = 'auto';
/// 누르기: 'select' 한 번 = 선택 · 두 번 = 열기 (기본) / 'open' 한 번 = 바로 열기.
/// 길게 누르기 · 오른쪽 클릭은 늘 기능 메뉴.
String explorerClick = 'select';
String explorerClick = Platform.isAndroid ? 'open' : 'select';
/// 모양: 'xplore' (기본) · 'windows' Windows 탐색기 · 'totalcmd' Total Commander
String explorerStyle = 'xplore';
@ -356,8 +356,8 @@ class AppSettings {
/// 보기: 밝기 (-0.7 어둡게 ~ 0.7 밝게)
double readerBrightness = 0;
/// ZIP · CBZ 를 두 번 누르면 안의 그림을 만화처럼 보기 (끄면 목록)
bool zipComic = true;
/// ZIP · CBZ 를 두 번 누르면 안의 그림을 만화처럼 보기 (끄면 목록). 기본은 목록 (사용자 지시)
bool zipComic = false;
/// WebDAV 서버 (파일 탐색기 · Rsync 화면 위쪽 "SD 카드" 옆 탭). 비밀번호는 이 설정 파일에만.
List<DavServer> webdavServers = [];
@ -462,6 +462,8 @@ class AppSettings {
'webJavaScript': webJavaScript,
'explorerLayout': explorerLayout,
'explorerClick': explorerClick,
// 예전 기본값 (두 창 · 한 번 누르면 선택) 은 사용자가 고른 값이 아니므로 한 번 새 기본값으로 옮긴다
'explorerV2': true,
'explorerStyle': explorerStyle,
'explorerOrientation': explorerOrientation,
'explorerToolbar': explorerToolbar,
@ -481,6 +483,8 @@ class AppSettings {
'readerRtl': readerRtl,
'readerBrightness': readerBrightness,
'zipComic': zipComic,
// 예전 기본값 (만화 보기 켜짐) 은 지시와 반대였으므로 한 번 꺼진 상태 (목록) 로
'zipComicV2': true,
'webdavServers': [for (final x in webdavServers) x.toJson()],
'rsyncOptions': rsyncOptions,
'robocopyOptions': robocopyOptions,
@ -572,8 +576,18 @@ class AppSettings {
..externalBrowser = j['externalBrowser'] as String? ?? 'system'
..webTranslate = j['webTranslate'] as bool? ?? false
..webJavaScript = j['webJavaScript'] as bool? ?? true
..explorerLayout = const ['single', 'split'].contains(j['explorerLayout']) ? j['explorerLayout'] as String : 'dual'
..explorerClick = j['explorerClick'] == 'open' ? 'open' : 'select'
// 'auto' = 넓은 화면은 두 창, 좁은 화면 (폰 세로) 은 한 창. 예전 기본 'dual' 은 한 번 'auto' 로
..explorerLayout = const ['single', 'split', 'auto'].contains(j['explorerLayout'])
? j['explorerLayout'] as String
: j['explorerLayout'] == 'dual' && j['explorerV2'] == true
? 'dual'
: 'auto'
// Android 는 손가락으로 한 번 누르면 바로 실행이 기본. 예전 기본 'select' 는 Android 에서 한 번 'open' 으로
..explorerClick = j['explorerClick'] == 'open'
? 'open'
: j['explorerClick'] == 'select' && (j['explorerV2'] == true || !Platform.isAndroid)
? 'select'
: (Platform.isAndroid ? 'open' : 'select')
..explorerStyle = const ['windows', 'totalcmd'].contains(j['explorerStyle']) ? j['explorerStyle'] as String : 'xplore'
..explorerOrientation = const ['side', 'stacked'].contains(j['explorerOrientation'])
? j['explorerOrientation'] as String
@ -600,7 +614,7 @@ class AppSettings {
..readerFit = j['readerFit'] == 'width' ? 'width' : 'page'
..readerRtl = j['readerRtl'] as bool? ?? false
..readerBrightness = ((j['readerBrightness'] as num?)?.toDouble() ?? 0).clamp(-0.7, 0.7)
..zipComic = j['zipComic'] as bool? ?? true
..zipComic = j['zipComic'] == true && j['zipComicV2'] == true
..webdavServers = [
for (final x in (j['webdavServers'] as List?) ?? const [])
if (x is Map) DavServer.fromJson(x),

View File

@ -61,9 +61,16 @@ class _FileBrowserState extends State<_FileBrowser> {
_start();
}
/// 듀얼 앱 (복제한 앱): Android 가 "모든 파일 접근" 을 주지 않을 수 있어 안내를 따로
bool _dual = false;
Future<void> _start() async {
_volumes = await AndroidAccess.volumes();
final allowed = await AndroidAccess.hasAllFiles();
try {
final root = await AndroidAccess.storageRoot();
_dual = root.contains('/emulated/') && !root.endsWith('/emulated/0');
} catch (_) {}
final first = [widget.initialDirectory, _lastDir, _volumes.first.$1].firstWhere(
(d) => d != null && Directory(d).existsSync() && _volumeOf(d) != null,
orElse: () => _volumes.first.$1)!;
@ -285,6 +292,15 @@ class _FileBrowserState extends State<_FileBrowser> {
tr('동영상을 고르고, 동영상 옆 jj_mkv 폴더에 MKV 를 만들려면\n"모든 파일에 대한 접근" 권한이 필요합니다.'),
textAlign: TextAlign.center,
),
if (_dual) ...[
const SizedBox(height: 12),
Text(
tr('지금은 듀얼 앱 (복제한 앱) 입니다. Android 가 듀얼 앱에는 이 권한을 주지 않을 수 있습니다. '
'허용 화면에서 켤 수 없으면 배지 없는 원래 JJ_MKVMaker 아이콘으로 여세요.'),
textAlign: TextAlign.center,
style: const TextStyle(color: JjColors.textDim),
),
],
const SizedBox(height: 16),
FilledButton(onPressed: AndroidAccess.request, child: Text(tr('권한 허용 화면 열기'))),
const SizedBox(height: 8),

View File

@ -212,6 +212,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
for (final pane in _panes) {
pane.dispose();
}
if (_allFiles != null) _life.dispose();
super.dispose();
}
@ -222,18 +223,183 @@ class _ExplorerPageState extends State<ExplorerPage> {
Future<void> _init() async {
_local = await _loadVolumes();
if (_local.isEmpty) _local = [(Platform.isWindows ? r'C:\' : '/', Platform.isWindows ? 'C:' : '/')];
if (Platform.isAndroid) unawaited(_checkAccess());
final saved = widget.rsync ? c.settings.rsyncPaths : c.settings.explorerPaths;
// 창마다 따로 읽는다: 한 창이 WebDAV 서버를 기다려도 다른 창 (이 기기 파일) 은 바로 보이게
final wants = [
for (var i = 0; i < 2; i++) i < saved.length && _reachable(saved[i]) ? saved[i] : _local[0].$1,
];
for (var i = 0; i < 2; i++) {
final want = i < saved.length && _reachable(saved[i]) ? saved[i] : _local[0].$1;
await _goTo(_panes[i], want, remember: false);
_panes[i]
..root = _volumeOf(wants[i])
..current = wants[i]
..focused = wants[i];
}
if (!mounted) return;
setState(() => _ready = true);
for (final pane in _panes) {
_reveal(pane, pane.current); // 목록이 그려진 뒤에
for (var i = 0; i < 2; i++) {
final pane = _panes[i];
unawaited(_goTo(pane, wants[i], remember: false).then((_) => _reveal(pane, pane.current)));
}
}
// ───────── 권한 (Android) ─────────
/// "모든 파일에 대한 접근" 권한 (없으면 폴더만 보이고 파일은 안 보인다). null = 아직 모름
bool? _allFiles;
/// 듀얼 앱 (복제한 앱) 인지: 저장소가 /storage/emulated/0 이 아님
bool _dualApp = false;
late final AppLifecycleListener _life = AppLifecycleListener(onResume: () {
if (Platform.isAndroid && _allFiles == false) unawaited(_checkAccess(reload: true));
});
Future<void> _checkAccess({bool reload = false}) async {
_life; // 다시 돌아오면 (권한 화면에서 허용하고 오면) 다시 확인
final ok = await AndroidAccess.hasAllFiles();
var dual = false;
try {
final root = await AndroidAccess.storageRoot();
dual = root.contains('/emulated/') && !root.endsWith('/emulated/0');
} catch (_) {}
if (!mounted) return;
final changed = ok != _allFiles;
setState(() {
_allFiles = ok;
_dualApp = dual;
});
if (reload && changed && ok) {
for (final pane in _panes) {
pane.cache.clear();
for (final d in pane.expanded.toList()) {
await _load(pane, d, force: true);
}
}
}
}
/// 권한이 없을 때 목록 위 안내 (이유 · 할 일 · 허용 화면 열기 · 다시 확인)
Widget? _accessNotice(_Pane pane) {
if (!Platform.isAndroid || _allFiles != false || isDav(pane.root)) return null;
return _notice(
icon: Icons.lock_outline,
title: tr('파일이 보이지 않습니다: "모든 파일에 대한 접근" 권한이 없습니다'),
body: _dualApp
? tr('지금은 듀얼 앱 (복제한 앱) 입니다. Android 가 듀얼 앱에는 이 권한을 주지 않을 수 있습니다. '
'허용 화면에서 켤 수 없으면 배지 없는 원래 JJ_MKVMaker 아이콘으로 여세요.')
: tr('폴더는 보여도 그 안의 동영상 · 파일은 이 권한이 있어야 보입니다. 허용 화면에서 JJ_MKVMaker 를 켜고 돌아오세요.'),
actions: [
FilledButton.tonal(onPressed: AndroidAccess.request, child: Text(tr('권한 허용 화면 열기'))),
TextButton(onPressed: () => _checkAccess(reload: true), child: Text(tr('허용했으면 다시 확인'))),
],
);
}
/// 목록 위 안내 상자
Widget _notice({required IconData icon, required String title, required String body, required List<Widget> actions}) =>
Container(
width: double.infinity,
margin: const EdgeInsets.fromLTRB(6, 6, 6, 0),
padding: const EdgeInsets.fromLTRB(12, 10, 12, 6),
decoration: BoxDecoration(
color: Colors.redAccent.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.redAccent.withValues(alpha: 0.5)),
),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [
Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
Icon(icon, size: 18, color: Colors.redAccent),
const SizedBox(width: 8),
Expanded(child: Text(title, style: const TextStyle(fontWeight: FontWeight.w600))),
]),
const SizedBox(height: 4),
Text(body, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
Wrap(spacing: 8, children: actions),
]),
);
// ───────── 읽을 수 없을 때 (12) ─────────
/// 오류 → (무엇이 문제인지, 무엇을 하면 되는지)
(String, String) _explain(String e, {required bool dav}) {
final s = e.toLowerCase();
if (dav && (s.contains('socketexception') || s.contains('connection refused') || s.contains('failed host lookup') ||
s.contains('network is unreachable') || s.contains('no route') || s.contains('timed out') ||
s.contains('timeoutexception') || s.contains('connection reset') || s.contains('connection closed'))) {
return (
tr('서버에 닿지 않습니다'),
tr('서버와 같은 네트워크에 있거나 VPN (예: Tailscale) 이 켜져 있어야 합니다. 켠 뒤 [다시 시도] 를 누르세요.'),
);
}
if (s.contains(' 401') || s.contains('status: 401') || s.contains('아이디 · 비밀번호')) {
return (tr('아이디 또는 비밀번호가 맞지 않습니다'), tr('[서버 설정 고치기] 에서 아이디 · 비밀번호를 확인하세요.'));
}
if (s.contains('handshake') || s.contains('certificate')) {
return (
tr('서버 인증서를 확인할 수 없습니다'),
tr('집 NAS 처럼 자체 서명 인증서면 [서버 설정 고치기] 에서 "인증서 확인 안 함" 을 켜세요.'),
);
}
if (s.contains(' 403') || s.contains('권한 없음')) return (tr('이 폴더를 볼 권한이 없습니다'), tr('서버에서 이 계정의 권한을 확인하세요.'));
if (s.contains(' 404') || s.contains('없는 경로')) return (tr('폴더가 없습니다'), tr('다른 곳에서 지웠거나 옮겼을 수 있습니다. 위 폴더로 가 보세요.'));
if (s.contains('permission denied') || s.contains('pathaccessexception') || s.contains('errno = 13')) {
return (
tr('이 폴더를 읽을 권한이 없습니다'),
Platform.isAndroid ? tr('Android 가 막은 폴더 (Android/data 등) 이거나 "모든 파일에 대한 접근" 권한이 없습니다.') : '',
);
}
return (tr('읽을 수 없습니다'), e);
}
Widget? _errorNotice(_Pane pane, String dir) {
final err = pane.errors[dir] ?? pane.errors[pane.root];
if (err == null) return null;
final where = pane.errors[dir] != null ? dir : pane.root;
final dav = isDav(where);
final (title, body) = _explain(err, dav: dav);
return _notice(
icon: dav ? Icons.cloud_off_outlined : Icons.error_outline,
title: '${_displayPath(where)}: $title',
body: body,
actions: [
FilledButton.tonal(
onPressed: () async {
pane.errors.remove(where);
pane.changed();
await _load(pane, where, force: true);
for (final d in pane.expanded.toList()) {
if (isSameOrInside(d, where) && d != where) await _load(pane, d, force: true);
}
},
child: Text(tr('다시 시도')),
),
if (dav) TextButton(onPressed: () => _editDav(pane, pane.root), child: Text(tr('서버 설정 고치기'))),
],
);
}
/// 화면에 보일 경로 (5 · 14): Android 는 "SD 카드 › JJ_sdtest › jj_mkv", WebDAV 는 "☁ 서버 › 폴더", Windows 는 그대로
String _displayPath(String path) {
if (isDav(path)) {
final d = DavPath.parse(path);
final label = DavRegistry.server(d.server)?.label ?? 'WebDAV';
return ['☁ $label', ...d.rel.split('/').where((s) => s.isNotEmpty)].join(' › ');
}
if (!Platform.isAndroid) return path;
String? best;
var label = '';
for (final (v, l) in _local) {
if ((samePath(v, path) || p.isWithin(v, path)) && (best == null || v.length > best.length)) {
best = v;
label = l;
}
}
if (best == null) return path;
final rest = p.relative(path, from: best);
return [label, if (rest != '.') ...p.split(rest)].join(' › ');
}
Future<List<(String, String)>> _loadVolumes() async {
if (Platform.isWindows) return windowsDrives();
if (Platform.isAndroid) {
@ -362,7 +528,11 @@ class _ExplorerPageState extends State<ExplorerPage> {
bool get _split => !widget.rsync && c.settings.explorerLayout == 'split';
/// 좌우 두 창 (Rsync 화면은 늘)
bool get _dual => widget.rsync || c.settings.explorerLayout == 'dual';
bool get _dual =>
widget.rsync ||
c.settings.explorerLayout == 'dual' ||
// 자동: 넓으면 두 창, 좁은 화면 (폰 세로) 은 한 창 - 같은 목록이 반씩 잘려 두 번 보이지 않게
(c.settings.explorerLayout == 'auto' && !isCompact(context));
/// 한 번 누르면 선택 · 두 번 누르면 열기 (아니면 한 번에 바로 열기)
bool get _selectMode => c.settings.explorerClick != 'open';
@ -1553,6 +1723,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
width: 420,
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
group(tr('창'), layout, [
('auto', tr('화면 크기 따라 (기본)')),
('dual', tr('두 창')),
('split', tr('폴더 + 파일 목록')),
('single', tr('한 창')),
@ -1708,7 +1879,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
Text(widget.rsync ? 'Rsync' : tr('파일 탐색기'), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
const SizedBox(width: 12),
Expanded(
child: Text(_ready ? vDisplay(_pane.current) : '',
child: Text(_ready ? _displayPath(_pane.current) : '',
maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(color: JjColors.textDim)),
),
PopupMenuButton<ExplorerButton>(
@ -1839,6 +2010,10 @@ class _ExplorerPageState extends State<ExplorerPage> {
return _frame(i, [
_paneHeader(pane),
const Divider(height: 1),
?_accessNotice(pane),
?_errorNotice(pane, pane.current),
// 읽는 중 (WebDAV 서버를 기다릴 때 등)
if (pane.loading.isNotEmpty) const LinearProgressIndicator(minHeight: 2),
if (look.columns && !foldersOnly) ExplorerColumnsHeader(style: look, markColumn: _selecting),
Expanded(
child: ListView.builder(
@ -1867,9 +2042,11 @@ class _ExplorerPageState extends State<ExplorerPage> {
height: 40,
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Text(dir, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(color: JjColors.textDim)),
child: Text(_displayPath(dir), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(color: JjColors.textDim)),
),
const Divider(height: 1),
?_accessNotice(pane),
?_errorNotice(pane, dir),
if (look.columns) ExplorerColumnsHeader(style: look, markColumn: _selecting),
Expanded(
child: pane.loading.contains(dir)
@ -2139,13 +2316,32 @@ class _Toolbar extends StatelessWidget {
const _Toolbar(
{required this.buttons, required this.vertical, required this.onPressed, required this.enabled, required this.selected});
/// 창이 위아래로 놓이면 (버튼 줄이 가로) "좌 → 우" 대신 "위 → 아래"
String _label(ExplorerButton b) => vertical
? b.label
: switch (b) {
ExplorerButton.toRight => '위 → 아래',
ExplorerButton.toLeft => '위 ← 아래',
ExplorerButton.both => '위 ⇄ 아래',
_ => b.label,
};
IconData _icon(ExplorerButton b) => vertical
? b.icon
: switch (b) {
ExplorerButton.toRight => Icons.arrow_downward,
ExplorerButton.toLeft => Icons.arrow_upward,
ExplorerButton.both => Icons.swap_vert,
_ => b.icon,
};
@override
Widget build(BuildContext context) {
Widget btn(ExplorerButton b) {
final on = enabled(b);
final sel = selected(b);
return Tooltip(
message: tr(b.label),
message: tr(_label(b)),
child: InkWell(
onTap: on ? () => onPressed(b) : null,
borderRadius: BorderRadius.circular(6),
@ -2153,9 +2349,9 @@ class _Toolbar extends StatelessWidget {
width: 72,
height: 58,
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Icon(b.icon, size: 22, color: !on ? JjColors.textDim.withValues(alpha: 0.4) : sel ? JjColors.accent : null),
Icon(_icon(b), size: 22, color: !on ? JjColors.textDim.withValues(alpha: 0.4) : sel ? JjColors.accent : null),
const SizedBox(height: 2),
Text(tr(b.label),
Text(tr(_label(b)),
maxLines: 2,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,

View File

@ -576,8 +576,9 @@ class _SettingsPageState extends State<SettingsPage> {
trailing: DropdownButton<String>(
value: s.explorerClick,
items: [
DropdownMenuItem(value: 'select', child: Text(tr('한 번: 선택 · 두 번: 실행 (기본)'))),
DropdownMenuItem(value: 'open', child: Text(tr('한 번: 바로 실행'))),
DropdownMenuItem(
value: 'select', child: Text(desk ? tr('한 번: 선택 · 두 번: 실행 (기본)') : tr('한 번: 선택 · 두 번: 실행'))),
DropdownMenuItem(value: 'open', child: Text(desk ? tr('한 번: 바로 실행') : tr('한 번: 바로 실행 (기본)'))),
],
onChanged: (v) => c.updateSettings((x) => x.explorerClick = v!),
),
@ -601,6 +602,7 @@ class _SettingsPageState extends State<SettingsPage> {
trailing: DropdownButton<String>(
value: s.explorerLayout,
items: [
DropdownMenuItem(value: 'auto', child: Text(tr('화면 크기 따라 (기본)'))),
DropdownMenuItem(value: 'dual', child: Text(tr('두 창'))),
DropdownMenuItem(value: 'split', child: Text(tr('폴더 + 파일 목록'))),
DropdownMenuItem(value: 'single', child: Text(tr('한 창'))),