From c25a754035d0bcd500918f4134605c65edcc7964 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Fri, 9 Oct 2026 20:35:17 +0900 Subject: [PATCH] =?UTF-8?q?50=20Android=20=EA=B0=80=20=EB=A7=89=EC=9D=80?= =?UTF-8?q?=20=ED=8F=B4=EB=8D=94=20(Android/data=20=C2=B7=20obb):=20?= =?UTF-8?q?=EB=B9=88=20=ED=8F=B4=EB=8D=94=EA=B0=80=20=EC=95=84=EB=8B=88?= =?UTF-8?q?=EB=9D=BC=20"=EC=9D=B4=20=ED=8F=B4=EB=8D=94=EB=8A=94=20Android?= =?UTF-8?q?=20=EA=B0=80=20=EB=A7=89=EC=95=84=20=EB=B3=BC=20=EC=88=98=20?= =?UTF-8?q?=EC=97=86=EC=8A=B5=EB=8B=88=EB=8B=A4"=20=C2=B7=20[=EC=9C=84=20?= =?UTF-8?q?=ED=8F=B4=EB=8D=94=EB=A1=9C]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Android 11 부터 이런 폴더는 오류 없이 빈 목록으로 읽힌다 - 막힌 것이라고 알리고 그 앱의 내보내기 · 공유를 안내 - 다시 해도 같으므로 [다시 시도] 대신 [위 폴더로] - 시험: file_error_test (경로 판별 · 안내 글) Co-Authored-By: Claude Opus 5.5 --- assets/l10n/en.json | 3 +++ assets/l10n/ja.json | 3 +++ assets/l10n/zh-Hans.json | 3 +++ lib/ui/explorer_page.dart | 26 ++++++++++++++++++++++++-- lib/ui/file_error.dart | 14 ++++++++++++++ test/file_error_test.dart | 31 +++++++++++++++++++++++++++++++ 6 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 test/file_error_test.dart diff --git a/assets/l10n/en.json b/assets/l10n/en.json index d488c8c..5a50363 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -102,6 +102,7 @@ "API 키 (필수, 무료)": "API key (required, free)", "API 키 또는 아이디:비밀번호 (선택)": "API key or user:password (optional)", "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 11 부터 다른 앱의 데이터 폴더 (Android/data · Android/obb) 는 어떤 파일 앱도 열 수 없습니다. 그 앱 안의 내보내기 · 공유로 파일을 꺼내세요.": "Since Android 11, no file app can open other apps' data folders (Android/data · Android/obb). Use export or share inside that app to get the files.", "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.", @@ -1270,6 +1271,7 @@ "웹 페이지 자동 번역": "Translate web pages automatically", "웹 페이지를 번역하지 못했습니다: {0}": "Could not translate the web page: {0}", "위 폴더": "Parent folder", + "위 폴더로": "Go up", "위로": "Up", "위아래": "Top and bottom", "위아래 ⇅": "Stacked ⇅", @@ -1331,6 +1333,7 @@ "이 폴더 선택": "Choose this folder", "이 폴더 쌍은 기억한 옵션을 씁니다 (환경 설정이 아니라 모니터링 > 복사 · rsync 에서 고칩니다)": "This folder pair uses its remembered options (edit them in Monitoring > Copy · rsync, not in settings)", "이 폴더 전체": "This whole folder", + "이 폴더는 Android 가 막아 볼 수 없습니다": "Android blocks this folder, so it cannot be viewed", "이 폴더를 rsync 원본 · 대상으로 고르기": "Choose this folder as rsync source · target", "이 폴더를 볼 권한이 없습니다": "No permission to view this folder", "이 폴더를 읽을 권한이 없습니다": "No permission to read this folder", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index dc62649..0e8ecf1 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -102,6 +102,7 @@ "API 키 (필수, 무료)": "API キー(必須・無料)", "API 키 또는 아이디:비밀번호 (선택)": "API キーまたは ID:パスワード (任意)", "AV1 영상은 이 기기에서 다시 인코딩할 수 없습니다. 코덱을 \"원본 유지\" 로 바꿔 MKV 를 만드세요. (PC 판은 됩니다)": "AV1 動画はこの端末では再エンコードできません。コーデックを「元のまま」に変えて MKV を作成してください。(PC 版はできます)", + "Android 11 부터 다른 앱의 데이터 폴더 (Android/data · Android/obb) 는 어떤 파일 앱도 열 수 없습니다. 그 앱 안의 내보내기 · 공유로 파일을 꺼내세요.": "Android 11 以降、他のアプリのデータフォルダー (Android/data・Android/obb) はどのファイルアプリでも開けません。そのアプリ内のエクスポート・共有でファイルを取り出してください。", "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 キー · パスワードは保存しないので再入力が必要です。", @@ -1270,6 +1271,7 @@ "웹 페이지 자동 번역": "ウェブページを自動翻訳", "웹 페이지를 번역하지 못했습니다: {0}": "ウェブページを翻訳できませんでした: {0}", "위 폴더": "上のフォルダー", + "위 폴더로": "上のフォルダーへ", "위로": "上へ", "위아래": "上下", "위아래 ⇅": "上下 ⇅", @@ -1331,6 +1333,7 @@ "이 폴더 선택": "このフォルダーを選択", "이 폴더 쌍은 기억한 옵션을 씁니다 (환경 설정이 아니라 모니터링 > 복사 · rsync 에서 고칩니다)": "このフォルダーの組は記憶したオプションを使います (環境設定ではなくモニタリング > コピー・rsync で変更します)", "이 폴더 전체": "このフォルダー全体", + "이 폴더는 Android 가 막아 볼 수 없습니다": "このフォルダーは Android によってブロックされているため表示できません", "이 폴더를 rsync 원본 · 대상으로 고르기": "このフォルダーを rsync の元 · 対象に選ぶ", "이 폴더를 볼 권한이 없습니다": "このフォルダーを表示する権限がありません", "이 폴더를 읽을 권한이 없습니다": "このフォルダーを読む権限がありません", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 3344213..5c21d0d 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -102,6 +102,7 @@ "API 키 (필수, 무료)": "API 密钥(必填,免费)", "API 키 또는 아이디:비밀번호 (선택)": "API 密钥或 用户名:密码 (可选)", "AV1 영상은 이 기기에서 다시 인코딩할 수 없습니다. 코덱을 \"원본 유지\" 로 바꿔 MKV 를 만드세요. (PC 판은 됩니다)": "此设备无法重新编码 AV1 视频。请将编解码器改为“保持原样”后制作 MKV。(PC 版可以)", + "Android 11 부터 다른 앱의 데이터 폴더 (Android/data · Android/obb) 는 어떤 파일 앱도 열 수 없습니다. 그 앱 안의 내보내기 · 공유로 파일을 꺼내세요.": "从 Android 11 起,任何文件应用都无法打开其他应用的数据文件夹 (Android/data · Android/obb)。请在该应用内使用导出或分享取出文件。", "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 密钥 · 密码不会保存,需要重新输入。", @@ -1270,6 +1271,7 @@ "웹 페이지 자동 번역": "自动翻译网页", "웹 페이지를 번역하지 못했습니다: {0}": "无法翻译网页: {0}", "위 폴더": "上级文件夹", + "위 폴더로": "返回上级文件夹", "위로": "向上", "위아래": "上下", "위아래 ⇅": "上下 ⇅", @@ -1331,6 +1333,7 @@ "이 폴더 선택": "选择此文件夹", "이 폴더 쌍은 기억한 옵션을 씁니다 (환경 설정이 아니라 모니터링 > 복사 · rsync 에서 고칩니다)": "此文件夹组合使用记住的选项 (请在 监视 > 复制 · rsync 中修改,而非设置)", "이 폴더 전체": "整个文件夹", + "이 폴더는 Android 가 막아 볼 수 없습니다": "此文件夹被 Android 阻止,无法查看", "이 폴더를 rsync 원본 · 대상으로 고르기": "选择此文件夹作为 rsync 源 · 目标", "이 폴더를 볼 권한이 없습니다": "没有查看此文件夹的权限", "이 폴더를 읽을 권한이 없습니다": "没有读取此文件夹的权限", diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index 604588b..9ea2072 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -473,6 +473,23 @@ class _ExplorerPageState extends State { final server = dav ? DavRegistry.server(DavPath.parse(where).server) : null; final (title, body) = explainFileError(err, dav: dav, noPassword: server != null && server.password.isEmpty); final locked = isLockedError(err); + // 50: 막힌 폴더는 다시 해도 같으니 [다시 시도] 없이 [위 폴더로] + if (err == androidRestrictedError) { + return _notice( + icon: Icons.lock_outline, + title: '${_displayPath(where)}: $title', + body: body, + actions: [ + FilledButton.tonal( + onPressed: () { + pane.errors.remove(where); + _goTo(pane, vDirname(where)); + }, + child: Text(tr('위 폴더로')), + ), + ], + ); + } return _notice( icon: dav ? Icons.cloud_off_outlined : Icons.error_outline, title: '${_displayPath(where)}: $title', @@ -584,10 +601,15 @@ class _ExplorerPageState extends State { try { final list = await listEntries(dir, showHidden: c.settings.explorerShowHidden); pane.cache[dir] = _sorted(list); - pane.errors.remove(dir); + // 50: Android 가 막은 폴더는 비어 보인다 (오류 없이) - 비어 있는 것이 아니라 막힌 것이라고 알린다 + if (list.isEmpty && isAndroidRestricted(dir)) { + pane.errors[dir] = androidRestrictedError; + } else { + pane.errors.remove(dir); + } } catch (e) { pane.cache[dir] = const []; - pane.errors[dir] = '$e'; + pane.errors[dir] = isAndroidRestricted(dir) ? androidRestrictedError : '$e'; } finally { pane.loading.remove(dir); pane.changed(); diff --git a/lib/ui/file_error.dart b/lib/ui/file_error.dart index 69c6d00..35a8002 100644 --- a/lib/ui/file_error.dart +++ b/lib/ui/file_error.dart @@ -5,7 +5,21 @@ import '../l10n/tr.dart'; /// 파일 · WebDAV 오류를 사람이 읽을 말로 (제목, 할 일). 원문 예외는 알아볼 수 없을 때만 그대로. /// 파일 탐색기 · 폴더 고르기 · 그 밖에 WebDAV 를 읽는 화면이 함께 쓴다 (12 · 134). +/// 50: Android 가 막은 폴더 (Android/data · obb) 표시 (파일 탐색기가 오류 자리에 넣는다) +const androidRestrictedError = 'ANDROID_RESTRICTED_FOLDER'; + +/// 50: Android 11 부터 어떤 파일 앱도 다른 앱의 데이터 폴더 (Android/data · Android/obb) 를 볼 수 없다 +bool isAndroidRestricted(String path, {bool? android}) => + (android ?? Platform.isAndroid) && RegExp(r'/Android/(data|obb)(/|$)', caseSensitive: false).hasMatch(path); + (String, String) explainFileError(String e, {required bool dav, bool noPassword = false}) { +if (e == androidRestrictedError) { + return ( + tr('이 폴더는 Android 가 막아 볼 수 없습니다'), + tr('Android 11 부터 다른 앱의 데이터 폴더 (Android/data · Android/obb) 는 어떤 파일 앱도 열 수 없습니다. ' + '그 앱 안의 내보내기 · 공유로 파일을 꺼내세요.'), + ); +} // 128 · 133: 마스터 비밀번호를 기다리는 중 (고장이 아님) if (isLockedError(e)) { return (tr('마스터 비밀번호가 필요합니다'), tr('이 서버의 저장된 비밀번호를 쓰려면 마스터 비밀번호를 넣으세요.')); diff --git a/test/file_error_test.dart b/test/file_error_test.dart new file mode 100644 index 0000000..67b2948 --- /dev/null +++ b/test/file_error_test.dart @@ -0,0 +1,31 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:jj_mkvmaker/ui/file_error.dart'; + +void main() { + test('50: Android 가 막은 폴더 (Android/data · obb) 알아보기', () { + for (final p in [ + '/storage/emulated/0/Android/data', + '/storage/emulated/0/Android/data/com.example.app', + '/storage/emulated/0/Android/obb/', + '/storage/1234-ABCD/Android/data/x', + ]) { + expect(isAndroidRestricted(p, android: true), isTrue, reason: p); + } + for (final p in [ + '/storage/emulated/0/Android', + '/storage/emulated/0/Android/media', + '/storage/emulated/0/Download/Android/datas', + '/storage/emulated/0/Movies', + ]) { + expect(isAndroidRestricted(p, android: true), isFalse, reason: p); + } + // Windows 등은 해당 없음 + expect(isAndroidRestricted('/storage/emulated/0/Android/data', android: false), isFalse); + }); + + test('50: 막힌 폴더는 "이 폴더는 Android 가 막아 볼 수 없습니다" 와 할 일', () { + final (title, body) = explainFileError(androidRestrictedError, dav: false); + expect(title, '이 폴더는 Android 가 막아 볼 수 없습니다'); + expect(body, contains('내보내기')); + }); +}