From ed24463192a09a91ae9b42e03f8cfd28fe2ccd99 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:29:55 +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?=EB=B3=B4=EA=B8=B0=20=ED=99=94=EB=A9=B4=20=EB=B3=BC=EB=A5=A8=20?= =?UTF-8?q?=ED=82=A4=EB=A1=9C=20=EB=84=98=EA=B8=B0=EA=B8=B0=20(=EC=95=84?= =?UTF-8?q?=EB=9E=98=20=3D=20=EB=8B=A4=EC=9D=8C=20=EC=9E=A5,=20=EC=9C=84?= =?UTF-8?q?=20=3D=20=EC=9D=B4=EC=A0=84=20=EC=9E=A5,=20Android)=20-=20?= =?UTF-8?q?=EC=B2=98=EC=9D=8C=20=EB=81=94=20(=ED=99=98=EA=B2=BD=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20>=20=EB=B3=B4=EA=B8=B0),=20=EC=BC=B0?= =?UTF-8?q?=EC=9D=84=20=EB=95=8C=EB=A7=8C=20=EB=B3=B4=EA=B8=B0=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=EC=97=90=EC=84=9C=20=EA=B0=80=EB=A1=9C=EC=B1=84?= =?UTF-8?q?=EA=B3=A0=20(=EC=B2=98=EB=A6=AC=ED=95=9C=20=ED=82=A4=EB=8A=94?= =?UTF-8?q?=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EB=B3=BC=EB=A5=A8=EC=9D=84=20?= =?UTF-8?q?=EB=B0=94=EA=BE=B8=EC=A7=80=20=EC=95=8A=EC=9D=8C)=20=EB=8F=99?= =?UTF-8?q?=EC=98=81=EC=83=81=20=C2=B7=20=EB=8B=A4=EB=A5=B8=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=EC=9D=80=20=EA=B7=B8=EB=8C=80=EB=A1=9C=20=C2=B7=20?= =?UTF-8?q?=EC=8B=9C=ED=97=98=20(=EA=B2=80=ED=86=A0=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 | 2 ++ assets/l10n/ja.json | 2 ++ assets/l10n/zh-Hans.json | 2 ++ lib/app/settings.dart | 6 ++++++ lib/ui/component_settings.dart | 9 +++++++++ lib/ui/reader_page.dart | 19 ++++++++++++++++- test/reader_test.dart | 37 +++++++++++++++++++++++++++++++++- 7 files changed, 75 insertions(+), 2 deletions(-) diff --git a/assets/l10n/en.json b/assets/l10n/en.json index 0b77140..47aa09d 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -1411,6 +1411,7 @@ "보기": "View", "보기 {0}초 · Ctrl+휠 확대/축소": "View {0} s · Ctrl+wheel to zoom", "보기 화면에 [품질 올리기]: AI 그림 (그림 → 그림, 약하게) 으로 다듬은 뒤 해상도 올리기를 이어서. 새 파일 (이름_q) 로 저장하며 AI 그림 모델이 필요합니다": "Shows [Improve quality] in the viewer: refines with AI image (image → image, light) and then upscales. Saves a new file (name_q); needs the AI image model", + "보기 화면에서 볼륨 아래 = 다음 장, 위 = 이전 장 (동영상 · 다른 화면의 볼륨은 그대로)": "In the viewer, volume down = next, volume up = previous (videos and other screens keep normal volume keys)", "보기 화면의 [계속 보기] 가 다음 장으로 넘기는 간격": "How often [Slideshow] in the viewer turns to the next page", "보내지 못했습니다": "Could not send", "보낼 것 {0} / 있는 것 {1}": "sending {0} / existing {1}", @@ -1461,6 +1462,7 @@ "복사했습니다.": "copied.", "볼 그림이 없습니다.": "No images to show.", "볼 수 없는 영상입니다 (삭제 · 지역 제한).": "This video is unavailable (deleted · region restricted).", + "볼륨 키로 넘기기": "Turn pages with volume keys", "분석": "Analyze", "분석 실패: {0} - {1}": "Analysis failed: {0} - {1}", "분석에 실패했던 동영상 {0}개를 다시 분석했습니다": "Re-analyzed {0} videos that had failed", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index 2808cb3..48bec88 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -1411,6 +1411,7 @@ "보기": "表示", "보기 {0}초 · Ctrl+휠 확대/축소": "表示 {0}秒・Ctrl+ホイールで拡大/縮小", "보기 화면에 [품질 올리기]: AI 그림 (그림 → 그림, 약하게) 으로 다듬은 뒤 해상도 올리기를 이어서. 새 파일 (이름_q) 로 저장하며 AI 그림 모델이 필요합니다": "ビューアーに [画質を上げる]: AI 画像 (画像 → 画像、弱め) で整えてから解像度を上げます。新しいファイル (名前_q) に保存し、AI 画像モデルが必要です", + "보기 화면에서 볼륨 아래 = 다음 장, 위 = 이전 장 (동영상 · 다른 화면의 볼륨은 그대로)": "ビューアーで音量下 = 次、音量上 = 前 (動画・ほかの画面の音量はそのまま)", "보기 화면의 [계속 보기] 가 다음 장으로 넘기는 간격": "表示画面の [連続表示] が次のページへ進む間隔", "보내지 못했습니다": "送信できませんでした", "보낼 것 {0} / 있는 것 {1}": "送る方 {0} / 既存 {1}", @@ -1461,6 +1462,7 @@ "복사했습니다.": "コピーしました。", "볼 그림이 없습니다.": "表示する画像がありません。", "볼 수 없는 영상입니다 (삭제 · 지역 제한).": "視聴できない動画です(削除・地域制限)。", + "볼륨 키로 넘기기": "音量キーでページ送り", "분석": "分析", "분석 실패: {0} - {1}": "分析失敗: {0} - {1}", "분석에 실패했던 동영상 {0}개를 다시 분석했습니다": "分析に失敗していた動画 {0} 件を再分析しました", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index a2f68c3..47c32f6 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -1411,6 +1411,7 @@ "보기": "查看", "보기 {0}초 · Ctrl+휠 확대/축소": "显示 {0} 秒 · Ctrl+滚轮缩放", "보기 화면에 [품질 올리기]: AI 그림 (그림 → 그림, 약하게) 으로 다듬은 뒤 해상도 올리기를 이어서. 새 파일 (이름_q) 로 저장하며 AI 그림 모델이 필요합니다": "在查看器中显示 [提升画质]: 先用 AI 图片 (图生图,轻度) 优化再提高分辨率。保存为新文件 (名称_q),需要 AI 图片模型", + "보기 화면에서 볼륨 아래 = 다음 장, 위 = 이전 장 (동영상 · 다른 화면의 볼륨은 그대로)": "在查看器中音量减 = 下一张, 音量加 = 上一张 (视频和其他界面的音量不变)", "보기 화면의 [계속 보기] 가 다음 장으로 넘기는 간격": "浏览画面中 [连续浏览] 翻到下一页的间隔", "보내지 못했습니다": "无法发送", "보낼 것 {0} / 있는 것 {1}": "要发送 {0} / 已有 {1}", @@ -1461,6 +1462,7 @@ "복사했습니다.": "已复制。", "볼 그림이 없습니다.": "没有可显示的图片。", "볼 수 없는 영상입니다 (삭제 · 지역 제한).": "该视频无法观看(已删除 · 地区限制)。", + "볼륨 키로 넘기기": "用音量键翻页", "분석": "分析", "분석 실패: {0} - {1}": "分析失败: {0} - {1}", "분석에 실패했던 동영상 {0}개를 다시 분석했습니다": "已重新分析之前分析失败的 {0} 个视频", diff --git a/lib/app/settings.dart b/lib/app/settings.dart index 4cf8a82..88463fb 100644 --- a/lib/app/settings.dart +++ b/lib/app/settings.dart @@ -558,6 +558,10 @@ class AppSettings { /// X-plore 맞추기 (관리자 지시 10/20): 그림 보기 화면 아래 갤러리 줄 (썸네일, 누르면 그 장으로). 처음 끔 bool readerGallery = false; + /// X-plore 맞추기 (관리자 지시 10/20): 보기 화면에서 볼륨 키로 넘기기 (아래 = 다음 장, 위 = 이전 장, Android). 처음 끔. + /// 보기 화면에서만 가로챈다 (동영상 · 다른 화면의 볼륨은 그대로) + bool readerVolumeKeys = false; + /// 119: 한 장에 두 쪽이 붙은 그림을 반씩 나눠 보기 - 'auto' 가로가 세로보다 긴 그림만 · 'on' 늘 · 'off' 안 함 String readerSplit = 'auto'; @@ -871,6 +875,7 @@ class AppSettings { 'readerSystemBars': readerSystemBars, 'readerAutoSeconds': readerAutoSeconds, 'readerGallery': readerGallery, + 'readerVolumeKeys': readerVolumeKeys, 'readerSplit': readerSplit, 'startMenuShortcut': startMenuShortcut, 'syncStopToast': syncStopToast, @@ -1127,6 +1132,7 @@ class AppSettings { ..readerSystemBars = j['readerSystemBars'] != false ..readerAutoSeconds = ((j['readerAutoSeconds'] as num?)?.toInt() ?? 3).clamp(1, 60) ..readerGallery = j['readerGallery'] as bool? ?? false + ..readerVolumeKeys = j['readerVolumeKeys'] as bool? ?? false ..readerSplit = const ['auto', 'on', 'off'].contains(j['readerSplit']) ? j['readerSplit'] as String : 'auto' ..startMenuShortcut = j['startMenuShortcut'] != false ..syncStopToast = j['syncStopToast'] != false diff --git a/lib/ui/component_settings.dart b/lib/ui/component_settings.dart index 2833a2b..073fba4 100644 --- a/lib/ui/component_settings.dart +++ b/lib/ui/component_settings.dart @@ -72,6 +72,15 @@ class ViewerSettings extends StatelessWidget { title: Text(tr('그림 보기 아래 갤러리 줄')), subtitle: Text(tr('그림을 볼 때 아래에 작은 그림 줄을 보입니다 (누르면 그 장으로)')), ), + // X-plore 맞추기 (관리자 지시 10/20): 볼륨 키로 넘기기 (Android) + if (Platform.isAndroid) + SwitchListTile( + secondary: const Icon(Icons.volume_down_outlined), + value: s.readerVolumeKeys, + onChanged: (v) => c.updateSettings((x) => x.readerVolumeKeys = v), + title: Text(tr('볼륨 키로 넘기기')), + subtitle: Text(tr('보기 화면에서 볼륨 아래 = 다음 장, 위 = 이전 장 (동영상 · 다른 화면의 볼륨은 그대로)')), + ), // 117: 계속 보기 간격 SettingTile( leading: const Icon(Icons.slideshow_outlined), diff --git a/lib/ui/reader_page.dart b/lib/ui/reader_page.dart index 41f6f67..16a4118 100644 --- a/lib/ui/reader_page.dart +++ b/lib/ui/reader_page.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io' show Platform; import 'dart:math' as math; import 'package:flutter/material.dart'; @@ -50,6 +51,9 @@ class ReaderPage extends StatefulWidget { final bool autoPlay; const ReaderPage({super.key, required this.c, required this.source, this.start = 0, this.autoPlay = false}); + /// 볼륨 키로 넘기기를 쓰는 기기인지 (Android - 시험에서 바꿈) + static bool Function() volumeKeysSupported = () => Platform.isAndroid; + /// 계속 보기 동안 화면 켜 두기 (시험에서 바꿈) static Future Function(bool on) keepScreenOn = (on) => on ? WakelockPlus.enable() : WakelockPlus.disable(); @@ -412,6 +416,15 @@ class _ReaderPageState extends State { _jumpLater(); } + /// X-plore 맞추기 (관리자 지시 10/20): 볼륨 키로 넘기기 (설정을 켰고 Android 일 때만 - 처리한 키는 시스템 볼륨을 바꾸지 않는다) + KeyEventResult _onKey(FocusNode _, KeyEvent e) { + if (!c.settings.readerVolumeKeys || !ReaderPage.volumeKeysSupported()) return KeyEventResult.ignored; + final k = e.logicalKey; + if (k != LogicalKeyboardKey.audioVolumeDown && k != LogicalKeyboardKey.audioVolumeUp) return KeyEventResult.ignored; + if (e is KeyDownEvent || e is KeyRepeatEvent) _go(k == LogicalKeyboardKey.audioVolumeDown ? 1 : -1); + return KeyEventResult.handled; + } + void _go(int delta) { final to = (_index + delta).clamp(0, math.max(0, _v.length - 1)).toInt(); if (to == _index) { @@ -933,7 +946,10 @@ class _ReaderPageState extends State { Navigator.of(context).pop(); } }, - child: Scaffold( + child: Focus( + autofocus: true, + onKeyEvent: _onKey, + child: Scaffold( backgroundColor: Colors.black, body: LayoutBuilder(builder: (context, box) { // 화면 너비 (기기 픽셀) 에 맞춰 그림을 줄여 읽는다 @@ -977,6 +993,7 @@ class _ReaderPageState extends State { ], ]); }), + ), ), ); } diff --git a/test/reader_test.dart b/test/reader_test.dart index ab6c2f3..c512e38 100644 --- a/test/reader_test.dart +++ b/test/reader_test.dart @@ -1,8 +1,8 @@ import 'dart:io'; -import 'dart:typed_data'; import 'package:archive/archive_io.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:image/image.dart' as img; import 'package:jj_mkvmaker/app/app_controller.dart'; @@ -215,6 +215,41 @@ void main() { await tester.pumpWidget(const SizedBox()); }); + testWidgets('X-plore 맞추기: 볼륨 키로 넘기기 - 처음은 끔 (키를 가로채지 않음), 켜면 아래 = 다음 · 위 = 이전 (Android 만)', (tester) async { + tester.view.physicalSize = const Size(900, 1200); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + final was = ReaderPage.volumeKeysSupported; + addTearDown(() => ReaderPage.volumeKeysSupported = was); + ReaderPage.volumeKeysSupported = () => true; + expect(AppSettings().readerVolumeKeys, isFalse); + final files = [ + for (final (i, n) in ['a.png', 'b.png', 'c.png'].indexed) (File(p.join(tmp.path, n))..writeAsBytesSync(_png(i * 80, 0, 0))).path, + ]; + final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); + await tester.pumpWidget(MaterialApp(home: ReaderPage(c: c, source: ImageFilesSource(files, tempDir: tmp.path, title: 'imgs')))); + await tester.pump(); + // 끔: 볼륨 키를 가로채지 않는다 (시스템 볼륨이 바뀜) + expect(await tester.sendKeyEvent(LogicalKeyboardKey.audioVolumeDown), isFalse); + await tester.pumpAndSettle(); + expect(find.text('1 / 3'), findsOneWidget); + // 켬 + c.settings.readerVolumeKeys = true; + expect(await tester.sendKeyEvent(LogicalKeyboardKey.audioVolumeDown), isTrue, reason: '처리함 - 시스템 볼륨은 바뀌지 않음'); + await tester.pumpAndSettle(); + expect(find.text('2 / 3'), findsOneWidget); + await tester.sendKeyEvent(LogicalKeyboardKey.audioVolumeDown); + await tester.pumpAndSettle(); + expect(find.text('3 / 3'), findsOneWidget); + await tester.sendKeyEvent(LogicalKeyboardKey.audioVolumeUp); + await tester.pumpAndSettle(); + expect(find.text('2 / 3'), findsOneWidget); + // Android 가 아니면 쓰지 않음 + ReaderPage.volumeKeysSupported = () => false; + expect(await tester.sendKeyEvent(LogicalKeyboardKey.audioVolumeDown), isFalse); + await tester.pumpWidget(const SizedBox()); + }); + testWidgets('그림이 깨졌으면 화면 글 + [자세히], 파일을 고친 뒤 [다시 시도] 하면 열린다 (28 지적: 같은 키 · 같은 그림이라 Image 가 다시 읽지 않았다 - 장의 키를 올리고 그림 캐시도 비움)', (tester) async { tester.view.physicalSize = const Size(900, 1200); tester.view.devicePixelRatio = 1;