From dc77998e634ff560c9008353e14b474aebf08b72 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:23:40 +0900 Subject: [PATCH] =?UTF-8?q?WIP=20X-plore=20=EB=A7=9E=EC=B6=94=EA=B8=B0=20(?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20=EC=A7=80=EC=8B=9C=2010/20):=20?= =?UTF-8?q?=ED=8F=B4=EB=8D=94=20=EA=B8=B8=EA=B2=8C=20=EB=88=84=EB=A5=B4?= =?UTF-8?q?=EA=B8=B0=20[=EC=82=AC=EC=A7=84=20=EC=8A=AC=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=93=9C=EC=87=BC]=20-=20=EA=B7=B8=20=ED=8F=B4=EB=8D=94?= =?UTF-8?q?=EC=9D=98=20=EA=B7=B8=EB=A6=BC=20(=EC=A7=80=EA=B8=88=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC=20=C2=B7=20=EC=88=A8=EA=B8=B4=20=EA=B2=83=20?= =?UTF-8?q?=EB=B9=BC=EA=B3=A0)=20=EC=9D=84=20=EB=B3=B4=EA=B8=B0=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EC=9C=BC=EB=A1=9C=20=EC=97=B4=EA=B3=A0=20[?= =?UTF-8?q?=EA=B3=84=EC=86=8D=20=EB=B3=B4=EA=B8=B0]=20(117)=20=EB=A5=BC=20?= =?UTF-8?q?=EC=BC=A0=20=EC=B1=84=20=EC=8B=9C=EC=9E=91,=20=EA=B7=B8?= =?UTF-8?q?=EB=A6=BC=EC=9D=B4=20=EC=97=86=EC=9C=BC=EB=A9=B4=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=C2=B7=20=EC=8B=9C=ED=97=98=20(=EA=B2=80=ED=86=A0?= =?UTF-8?q?=20=EB=8C=80=EA=B8=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/reader_page.dart | 10 +++++++--- test/explorer_test.dart | 27 +++++++++++++++++++++++++++ 6 files changed, 69 insertions(+), 3 deletions(-) diff --git a/assets/l10n/en.json b/assets/l10n/en.json index d2928b8..ccfbbdd 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -1521,6 +1521,7 @@ "사용자가 취소했습니다.": "Cancelled by the user.", "사용할 수 있는 자막 사이트가 없습니다.": "No subtitle site is available.", "사진": "Photos", + "사진 슬라이드쇼": "Photo slideshow", "사진에는 '사진' 모델이 더 낫지만, 이 기기 (그래픽카드 없음) 에서는 한 장에 약 20분 걸립니다.": "The 'Photos' model is better for photos, but on this device (no graphics card) it takes about 20 minutes per image.", "사진용": "Photos", "삭제": "Delete", @@ -2103,6 +2104,7 @@ "이 폴더에 현재 페이지 추가": "Add the current page to this folder", "이 폴더에는 고를 파일이 없습니다": "There are no files to choose in this folder", "이 폴더에는 그림 · 동영상이 없습니다": "No pictures or videos in this folder", + "이 폴더에는 그림이 없습니다": "There are no images in this folder", "이 폴더에만: {0}": "This folder only: {0}", "이 폴더의 동영상 재생": "Play videos in this folder", "이 폴더의 파일": "Files in this folder", @@ -2734,6 +2736,7 @@ "폴더는 보여도 그 안의 동영상 · 파일은 이 권한이 있어야 보입니다. 허용 화면에서 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}", "폴더를 읽는 중…": "Reading folders…", + "폴더를 읽을 수 없습니다: {0}": "Can't read the folder: {0}", "폴더를 자기 안으로 복사 · 이동할 수 없습니다": "Cannot copy or move a folder into itself", "폴더를 자기 안으로 복사할 수 없습니다": "Cannot copy a folder into itself", "폴더를 자기 안으로 옮길 수 없습니다": "Cannot move a folder into itself", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index be1d869..c8877da 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -1521,6 +1521,7 @@ "사용자가 취소했습니다.": "ユーザーがキャンセルしました。", "사용할 수 있는 자막 사이트가 없습니다.": "使用できる字幕サイトがありません。", "사진": "写真", + "사진 슬라이드쇼": "写真スライドショー", "사진에는 '사진' 모델이 더 낫지만, 이 기기 (그래픽카드 없음) 에서는 한 장에 약 20분 걸립니다.": "写真には「写真」モデルが適していますが、この端末 (グラフィックカードなし) では 1 枚に約 20 分かかります。", "사진용": "写真用", "삭제": "削除", @@ -2103,6 +2104,7 @@ "이 폴더에 현재 페이지 추가": "このフォルダーに現在のページを追加", "이 폴더에는 고를 파일이 없습니다": "このフォルダーには選べるファイルがありません", "이 폴더에는 그림 · 동영상이 없습니다": "このフォルダーには画像・動画がありません", + "이 폴더에는 그림이 없습니다": "このフォルダーには画像がありません", "이 폴더에만: {0}": "このフォルダーだけ: {0}", "이 폴더의 동영상 재생": "このフォルダーの動画を再生", "이 폴더의 파일": "このフォルダーのファイル", @@ -2734,6 +2736,7 @@ "폴더는 보여도 그 안의 동영상 · 파일은 이 권한이 있어야 보입니다. 허용 화면에서 JJ_MKVMaker 를 켜고 돌아오세요.": "フォルダーは見えても、中の動画・ファイルはこの権限が必要です。許可画面で JJ_MKVMaker をオンにして戻ってください。", "폴더를 만들 수 없습니다: {0}": "フォルダーを作成できません: {0}", "폴더를 읽는 중…": "フォルダーを読み込み中…", + "폴더를 읽을 수 없습니다: {0}": "フォルダーを読み込めません: {0}", "폴더를 자기 안으로 복사 · 이동할 수 없습니다": "フォルダーを自分自身の中にコピー · 移動できません", "폴더를 자기 안으로 복사할 수 없습니다": "フォルダーを自分自身の中にコピーできません", "폴더를 자기 안으로 옮길 수 없습니다": "フォルダーを自分自身の中に移動できません", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 37d1822..f1f3c36 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -1521,6 +1521,7 @@ "사용자가 취소했습니다.": "用户已取消。", "사용할 수 있는 자막 사이트가 없습니다.": "没有可用的字幕网站。", "사진": "照片", + "사진 슬라이드쇼": "照片幻灯片", "사진에는 '사진' 모델이 더 낫지만, 이 기기 (그래픽카드 없음) 에서는 한 장에 약 20분 걸립니다.": "照片更适合使用“照片”模型,但在本设备 (无显卡) 上每张约需 20 分钟。", "사진용": "照片", "삭제": "删除", @@ -2103,6 +2104,7 @@ "이 폴더에 현재 페이지 추가": "将当前页面添加到此文件夹", "이 폴더에는 고를 파일이 없습니다": "此文件夹中没有可选择的文件", "이 폴더에는 그림 · 동영상이 없습니다": "此文件夹中没有图片或视频", + "이 폴더에는 그림이 없습니다": "此文件夹中没有图片", "이 폴더에만: {0}": "仅此文件夹: {0}", "이 폴더의 동영상 재생": "播放此文件夹中的视频", "이 폴더의 파일": "此文件夹中的文件", @@ -2734,6 +2736,7 @@ "폴더는 보여도 그 안의 동영상 · 파일은 이 권한이 있어야 보입니다. 허용 화면에서 JJ_MKVMaker 를 켜고 돌아오세요.": "虽然能看到文件夹,但其中的视频和文件需要此权限。请在权限界面开启 JJ_MKVMaker 后返回。", "폴더를 만들 수 없습니다: {0}": "无法创建文件夹: {0}", "폴더를 읽는 중…": "正在读取文件夹…", + "폴더를 읽을 수 없습니다: {0}": "无法读取文件夹: {0}", "폴더를 자기 안으로 복사 · 이동할 수 없습니다": "不能将文件夹复制或移动到其自身内部", "폴더를 자기 안으로 복사할 수 없습니다": "不能将文件夹复制到其自身内部", "폴더를 자기 안으로 옮길 수 없습니다": "不能将文件夹移动到其自身内部", diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index e96ee68..2d55362 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -1765,6 +1765,28 @@ class _ExplorerPageState extends State { await openReader(context, c, ImageFilesSource(images, tempDir: temp, title: vBasename(dir)), start: start); } + /// X-plore 맞추기 (관리자 지시 10/20): 폴더의 그림 (지금 정렬 순서) 을 보기 화면으로 열고 [계속 보기] 를 켠 채 시작 + Future _slideshow(_Pane pane, String dir) async { + List list; + try { + list = pane.cache[dir] ?? _sorted(await listEntries(dir, showHidden: c.settings.explorerShowHidden), dir); + } catch (e) { + _snack(trf('폴더를 읽을 수 없습니다: {0}', [e])); + return; + } + final images = [ + for (final e in list) + if (!e.isDir && c.settings.imageExts.contains(extOf(e.path)) && (_showAppHidden || !_appHidden(e.path))) e.path, + ]; + if (images.isEmpty) { + _snack(tr('이 폴더에는 그림이 없습니다')); + return; + } + final temp = await c.services.storage.tempDirectory(); + if (!mounted) return; + await openReader(context, c, ImageFilesSource(images, tempDir: temp, title: vBasename(dir)), autoPlay: true); + } + /// 문서 → PDF (진행 창) → 보기 Future _viewDoc(String path) async { final temp = await c.services.storage.tempDirectory(); @@ -1999,6 +2021,8 @@ class _ExplorerPageState extends State { if (!e.isDir && !dav) ('hash', Icons.tag, tr('해시 (MD5 · SHA-256)')), if (e.isDir && dual) ('openOther', Icons.vertical_split_outlined, tr('다른 창에서 열기')), if (e.isDir) ('playFolder', Icons.play_circle_outline, tr('이 폴더의 동영상 재생')), + // X-plore 맞추기 (관리자 지시 10/20): 이 폴더의 그림을 [계속 보기] 로 + if (e.isDir && viewer) ('slideshow', Icons.slideshow_outlined, tr('사진 슬라이드쇼')), // X-plore 비교 g: 그림 · 동영상을 썸네일 격자로 if (e.isDir && !dav) ('grid', Icons.grid_view, tr('격자로 보기 (그림 · 동영상)')), if (video || audio) ('playInternal', Icons.play_circle_outline, tr('내장 플레이어로 재생')), @@ -2075,6 +2099,8 @@ class _ExplorerPageState extends State { setState(() => _active = _panes.indexOf(other)); case 'playFolder': await _playFolder(e.path); + case 'slideshow': + await _slideshow(pane, e.path); case 'playInternal': await _play([e.path], internal: true); case 'grid': diff --git a/lib/ui/reader_page.dart b/lib/ui/reader_page.dart index fa450e6..27750a7 100644 --- a/lib/ui/reader_page.dart +++ b/lib/ui/reader_page.dart @@ -15,9 +15,9 @@ import 'ai_upscale_ui.dart'; import 'theme.dart'; /// 그림 · 만화 · PDF 보기를 연다. [start] 처음 볼 장. 닫으면 [source] 를 정리한다. -Future openReader(BuildContext context, AppController c, ReaderSource source, {int start = 0}) async { +Future openReader(BuildContext context, AppController c, ReaderSource source, {int start = 0, bool autoPlay = false}) async { await Navigator.of(context).push(MaterialPageRoute( - builder: (_) => ReaderPage(c: c, source: source, start: start), + builder: (_) => ReaderPage(c: c, source: source, start: start, autoPlay: autoPlay), )); await source.dispose(); } @@ -45,7 +45,10 @@ class ReaderPage extends StatefulWidget { final AppController c; final ReaderSource source; final int start; - const ReaderPage({super.key, required this.c, required this.source, this.start = 0}); + + /// X-plore 맞추기 (관리자 지시 10/20): 폴더 [사진 슬라이드쇼] - [계속 보기] (117) 를 켠 채 연다 + final bool autoPlay; + const ReaderPage({super.key, required this.c, required this.source, this.start = 0, this.autoPlay = false}); /// 계속 보기 동안 화면 켜 두기 (시험에서 바꿈) static Future Function(bool on) keepScreenOn = (on) => on ? WakelockPlus.enable() : WakelockPlus.disable(); @@ -81,6 +84,7 @@ class _ReaderPageState extends State { _applySystemBars(); // 224: 앱으로 돌아올 때 그사이 밖에서 지운 그림을 목록에서 뺀다 _life = AppLifecycleListener(onResume: () => unawaited(dropMissingFiles())); + if (widget.autoPlay) WidgetsBinding.instance.addPostFrameCallback((_) => mounted ? _play(true) : null); } late final AppLifecycleListener _life; diff --git a/test/explorer_test.dart b/test/explorer_test.dart index c1ad763..1f49172 100644 --- a/test/explorer_test.dart +++ b/test/explorer_test.dart @@ -24,6 +24,8 @@ 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/path_label.dart'; +import 'package:image/image.dart' as img; +import 'package:jj_mkvmaker/ui/reader_page.dart' show ReaderPage; import 'package:path/path.dart' as p; import 'support/fake_recycle_bin.dart'; import 'package:jj_mkvmaker/platform/windows/recycle_bin.dart' show recycleBinForTest; @@ -167,6 +169,31 @@ void main() { await tester.pump(const Duration(seconds: 2)); }); + testWidgets('X-plore 맞추기 (관리자 지시 10/20): 폴더 [사진 슬라이드쇼] - 그 폴더 그림을 [계속 보기] 로 열기 · 그림이 없으면 알림', (tester) async { + for (final n in ['b.png', 'a.png']) { + File(p.join(left, 'sub', n)).writeAsBytesSync(img.encodePng(img.Image(width: 20, height: 30))); + } + Directory(p.join(left, 'empty')).createSync(); + await open(tester); + await tester.longPress(find.text('sub')); + await calm(tester); + await act(tester, () => tester.tap(find.text('사진 슬라이드쇼'))); + await settle(tester, () => find.byType(ReaderPage).evaluate().isNotEmpty); + expect(find.text('1 / 2'), findsOneWidget); + expect(find.text('a.png'), findsOneWidget, reason: '지금 정렬 순서 (이름) 의 첫 그림부터'); + expect(find.byTooltip('계속 보기 멈추기'), findsOneWidget, reason: '[계속 보기] 를 켠 채 시작'); + await act(tester, () async => tester.state(find.byType(Navigator).first).pop()); + await calm(tester); + // 그림이 없는 폴더 + await tester.longPress(find.text('empty')); + await calm(tester); + await act(tester, () => tester.tap(find.text('사진 슬라이드쇼'))); + await settle(tester, () => find.text('이 폴더에는 그림이 없습니다').evaluate().isNotEmpty); + expect(find.byType(ReaderPage), findsNothing); + ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars(); + await calm(tester); + }); + testWidgets('X-plore P3: 길게 누르기 [해시] (MD5 · SHA-256) · 폴더 [폴더 속성] · 빠른 보기 [16진]', (tester) async { await open(tester); // 해시: doc.txt = "hello"