From 23cc9f6e5bbd01c6e67b5d8c8f6cb99008f79d7a Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Sun, 25 Oct 2026 18:00:31 +0900 Subject: [PATCH] =?UTF-8?q?M4=201=EB=AC=B6=EC=9D=8C:=20"Android=20?= =?UTF-8?q?=EC=9D=B8=EC=A7=80"=20=EC=A0=84=EC=97=AD=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=20=EB=91=98=20(=EC=95=B1=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=EB=8B=A8=EC=B6=94=20=C2=B7=20=EB=B3=BC=EB=A5=A8=20=ED=82=A4)?= =?UTF-8?q?=20=EC=9D=84=20PlatformInfo=20=EB=A1=9C=20+=20WakeLock=20(?= =?UTF-8?q?=EB=B3=B4=EA=B8=B0=20=ED=99=94=EB=A9=B4=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=BC=9C=20=EB=91=90=EA=B8=B0)=20=EC=9D=84=20=EC=A3=BC?= =?UTF-8?q?=EC=9E=85=EC=9C=BC=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ExplorerButton.explorerValues · defaultOrder · fromSettings 는 android 를 받는다 (부르는 쪽이 c.services.platform.isAndroid) - 예전 바꿀 수 있는 전역 함수 appsButtonSupported - 보기 화면 볼륨 키는 c.services.platform.isAndroid (예전 ReaderPage.volumeKeysSupported) - PlatformServices.keepScreenOn (처음 값 WakelockPlus - 예전 ReaderPage.keepScreenOn) - 시험: app_manager · explorer_archive_trash · explorer_conflict · reader · reader_more 는 주입으로 (2회 통과). 볼륨 키 시험은 Android 조립과 Windows 조립 두 화면으로 - 사용자가 보게 될 동작: 없음 Co-Authored-By: Claude Opus 5.5 --- integration_test/support/shot_services.dart | 1 + lib/services/platform_services.dart | 8 ++++++++ lib/ui/explorer/explorer_body.dart | 2 +- lib/ui/explorer/explorer_buttons.dart | 19 ++++++++----------- lib/ui/explorer/view_dialogs.dart | 9 +++++---- lib/ui/reader_page.dart | 12 +++--------- test/app_manager_test.dart | 9 +++------ test/explorer_archive_trash_test.dart | 2 +- test/explorer_conflict_test.dart | 4 ++-- test/reader_more_test.dart | 10 ++++------ test/reader_test.dart | 16 ++++++++++------ 11 files changed, 46 insertions(+), 46 deletions(-) diff --git a/integration_test/support/shot_services.dart b/integration_test/support/shot_services.dart index dddd5ec..c42f1b9 100644 --- a/integration_test/support/shot_services.dart +++ b/integration_test/support/shot_services.dart @@ -32,6 +32,7 @@ PlatformServices shotServices() { vault: r.vault, privileged: r.privileged, isMetered: r.isMetered, + keepScreenOn: r.keepScreenOn, updater: r.updater, usage: r.usage == null ? null : const FixedUsage(), models: r.models, diff --git a/lib/services/platform_services.dart b/lib/services/platform_services.dart index 3230dbb..5230649 100644 --- a/lib/services/platform_services.dart +++ b/lib/services/platform_services.dart @@ -38,6 +38,7 @@ import 'subtitle_provider.dart'; import 'system_usage.dart'; import 'updater.dart'; import '../l10n/tr.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; /// 플랫폼별 구현을 한 곳에서 고른다. /// Android 이식 시 이 파일에 분기만 추가하면 된다. @@ -91,6 +92,9 @@ class PlatformServices { /// 지금 쓰는 망이 데이터 요금제인지 (원격 썸네일 "Wi-Fi 에서만" - 설계 M3 ThumbPolicy). 처음 값 = 실제 (Android 만 묻고 PC 는 false) final Future Function() isMetered; + /// 화면을 켜 둔다 / 놓는다 (보기 화면의 자동 넘기기 - 설계 M4 WakeLock). 처음 값 = WakelockPlus + final Future Function(bool on) keepScreenOn; + /// 이 기기 (Android · Windows · 메모리) - 기기마다 다르게 도는 곳은 이것을 본다 (M1, 처음 값 = 이 기기) final PlatformInfo platform; @@ -118,9 +122,11 @@ class PlatformServices { VaultLocation? vault, this.privileged, Future Function()? isMetered, + Future Function(bool on)? keepScreenOn, }) : models = models ?? ModelStore(), vault = vault ?? VaultLocation.under(storage.tempDirectory), isMetered = isMetered ?? deviceMetered, + keepScreenOn = keepScreenOn ?? _wakeLock, platform = platform ?? PlatformInfo.host, shell = shell ?? NoopShell(); @@ -192,3 +198,5 @@ class PlatformServices { throw UnsupportedError(trf('아직 지원하지 않는 플랫폼입니다: {0}', [Platform.operatingSystem])); } } + +Future _wakeLock(bool on) => on ? WakelockPlus.enable() : WakelockPlus.disable(); diff --git a/lib/ui/explorer/explorer_body.dart b/lib/ui/explorer/explorer_body.dart index 4493584..062522d 100644 --- a/lib/ui/explorer/explorer_body.dart +++ b/lib/ui/explorer/explorer_body.dart @@ -103,7 +103,7 @@ class ExplorerPanesBody extends StatelessWidget { // X-plore b: 길게 누르기 = 짝 동작 (Rsync 화면은 없음) onLongPress: ctl.rsync ? null : buttons.longPress, // Rsync 화면: 늘 같은 버튼 (→ ← ⇄ 모니터링) 을 두 창 사이에 - buttons: ctl.rsync ? ExplorerButton.rsyncButtons : ExplorerButton.fromSettings(s.explorer.buttons), + buttons: ctl.rsync ? ExplorerButton.rsyncButtons : ExplorerButton.fromSettings(s.explorer.buttons, android: ctl.c.services.platform.isAndroid), vertical: side && !rsyncOne, panesSide: side, onPressed: buttons.press, diff --git a/lib/ui/explorer/explorer_buttons.dart b/lib/ui/explorer/explorer_buttons.dart index fb5fcbd..1f2559a 100644 --- a/lib/ui/explorer/explorer_buttons.dart +++ b/lib/ui/explorer/explorer_buttons.dart @@ -1,4 +1,3 @@ -import 'dart:io'; import 'package:flutter/material.dart'; @@ -61,30 +60,28 @@ enum ExplorerButton { /// Rsync 화면에만 있는 버튼 (파일 탐색기 버튼 구성에서는 빼기) bool get rsyncOnly => this == toRight || this == toLeft || this == both || this == monitor; - /// 파일 탐색기에서 고를 수 있는 버튼 - static List get explorerValues => [for (final b in values) if (!b.rsyncOnly && (b != apps || appsButtonSupported())) b]; - - /// 앱 관리자 버튼을 쓰는 기기 (Android - 시험에서 바꿈) - static bool Function() appsButtonSupported = () => Platform.isAndroid; + /// 파일 탐색기에서 고를 수 있는 버튼. [앱 관리자] 는 Android 에서만 ([android] = c.services.platform.isAndroid - 설계 M4, 예전 전역 함수 appsButtonSupported) + static List explorerValues({required bool android}) => + [for (final b in values) if (!b.rsyncOnly && (b != apps || android)) b]; /// Rsync 화면의 버튼 (늘 이대로, 두 창 사이) // 208: 방향 실행 (Rsync 화면의 핵심) 을 앞에 - 좁아서 [더 보기] 로 가는 것은 뒤쪽 static const rsyncButtons = [toRight, toLeft, both, monitor, up, refresh, newFolder, bookmarks]; /// 147 (관리자): 처음 값 (버튼 구성을 바꾸지 않았을 때) 은 자주 쓰는 것부터 - 좁은 화면에서 [더 보기] 로 가는 것은 뒤쪽 - static List get defaultOrder { + static List defaultOrder({required bool android}) { const first = [copy, move, delete, newFolder, rename, select, search, bookmarks, orient]; - return [...first, for (final b in explorerValues) if (!first.contains(b)) b]; + return [...first, for (final b in explorerValues(android: android)) if (!first.contains(b)) b]; } - static List fromSettings(List names) { + static List fromSettings(List names, {required bool android}) { final out = [ for (final n in names) - for (final b in explorerValues) + for (final b in explorerValues(android: android)) if (b.name == n) b, ]; // 사용자가 정한 순서는 그대로 (비어 있을 때만 처음 값) - return out.isEmpty ? defaultOrder : out; + return out.isEmpty ? defaultOrder(android: android) : out; } } diff --git a/lib/ui/explorer/view_dialogs.dart b/lib/ui/explorer/view_dialogs.dart index ee37bf5..c2e7518 100644 --- a/lib/ui/explorer/view_dialogs.dart +++ b/lib/ui/explorer/view_dialogs.dart @@ -197,8 +197,9 @@ class ExplorerViewDialogs { } Future buttonsDialog(BuildContext context) async { - final current = ExplorerButton.fromSettings(ctl.c.settings.explorer.buttons); - final order = [...current, ...ExplorerButton.explorerValues.where((b) => !current.contains(b))]; + final android = ctl.c.services.platform.isAndroid; + final current = ExplorerButton.fromSettings(ctl.c.settings.explorer.buttons, android: android); + final order = [...current, ...ExplorerButton.explorerValues(android: android).where((b) => !current.contains(b))]; final on = {...current}; final ok = await showDialog( context: context, @@ -237,10 +238,10 @@ class ExplorerViewDialogs { onPressed: () => set(() { order ..clear() - ..addAll(ExplorerButton.defaultOrder); + ..addAll(ExplorerButton.defaultOrder(android: android)); on ..clear() - ..addAll(ExplorerButton.explorerValues); + ..addAll(ExplorerButton.explorerValues(android: android)); }), child: Text(tr('기본값')), ), diff --git a/lib/ui/reader_page.dart b/lib/ui/reader_page.dart index 608cb90..a5207fb 100644 --- a/lib/ui/reader_page.dart +++ b/lib/ui/reader_page.dart @@ -1,10 +1,8 @@ import 'dart:async'; -import 'dart:io' show Platform; import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; import '../app/app_controller.dart'; import '../core/reader_sources.dart'; @@ -49,11 +47,7 @@ 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(); @override State createState() => _ReaderPageState(); @@ -217,7 +211,7 @@ class _ReaderPageState extends State { // 지금 장이 이미 보이면 바로 재기 시작, 아직 받는 중이면 보인 뒤에 ([_markShown]) if (!_show.set(on, _v.isNotEmpty ? _curKey : null)) return; setState(() {}); - unawaited(ReaderPage.keepScreenOn(on).catchError((_) {})); + unawaited(c.services.keepScreenOn(on).catchError((_) {})); } void _tick() { @@ -274,7 +268,7 @@ class _ReaderPageState extends State { _gallery.dispose(); _life.dispose(); _show.dispose(); - if (_playing) unawaited(ReaderPage.keepScreenOn(false).catchError((_) {})); + if (_playing) unawaited(c.services.keepScreenOn(false).catchError((_) {})); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); _images.dispose(); _zoom.reset(); @@ -311,7 +305,7 @@ class _ReaderPageState extends State { /// X-plore 맞추기 (관리자 지시 10/20): 볼륨 키로 넘기기 (설정을 켰고 Android 일 때만 - 처리한 키는 시스템 볼륨을 바꾸지 않는다) KeyEventResult _onKey(FocusNode _, KeyEvent e) { - final step = volumeKeyStep(e, enabled: c.settings.reader.volumeKeys && ReaderPage.volumeKeysSupported()); + final step = volumeKeyStep(e, enabled: c.settings.reader.volumeKeys && c.services.platform.isAndroid); if (step == null) return KeyEventResult.ignored; if (step != 0) _go(step); return KeyEventResult.handled; diff --git a/test/app_manager_test.dart b/test/app_manager_test.dart index 2f24e35..919ab62 100644 --- a/test/app_manager_test.dart +++ b/test/app_manager_test.dart @@ -110,11 +110,8 @@ void main() { }); test('가운데 버튼 [앱 관리자] 는 Android 에서만 고를 수 있음', () { - final was = ExplorerButton.appsButtonSupported; - addTearDown(() => ExplorerButton.appsButtonSupported = was); - ExplorerButton.appsButtonSupported = () => false; - expect(ExplorerButton.explorerValues, isNot(contains(ExplorerButton.apps))); - ExplorerButton.appsButtonSupported = () => true; - expect(ExplorerButton.explorerValues, contains(ExplorerButton.apps)); + // 설계 M4: Android 인지는 부르는 쪽이 넘긴다 (c.services.platform.isAndroid) + expect(ExplorerButton.explorerValues(android: false), isNot(contains(ExplorerButton.apps))); + expect(ExplorerButton.explorerValues(android: true), contains(ExplorerButton.apps)); }); } diff --git a/test/explorer_archive_trash_test.dart b/test/explorer_archive_trash_test.dart index 19764cf..ec6f42e 100644 --- a/test/explorer_archive_trash_test.dart +++ b/test/explorer_archive_trash_test.dart @@ -113,7 +113,7 @@ void main() { device.trash = true; device.volumes = () => [(tmp.path, 'T:')]; // [휴지통] 버튼이 줄에 보이게 (처음 구성은 좁으면 [더 보기] 로 간다) - c.settings.explorer.buttons = ['trash', for (final b in ExplorerButton.defaultOrder) if (b != ExplorerButton.trash) b.name]; + c.settings.explorer.buttons = ['trash', for (final b in ExplorerButton.defaultOrder(android: false)) if (b != ExplorerButton.trash) b.name]; addTearDown(() { }); await open(tester); diff --git a/test/explorer_conflict_test.dart b/test/explorer_conflict_test.dart index eaa3a50..cf2721a 100644 --- a/test/explorer_conflict_test.dart +++ b/test/explorer_conflict_test.dart @@ -390,9 +390,9 @@ void main() { expect(c.settings.explorer.showHidden, !before); // 창 배치 버튼은 좁아도 늘 줄에 (사용자 요청) · 처음 값은 자주 쓰는 것부터 (복사 · 이동 · 삭제 …) expect(find.byTooltip('창 배치: 좌우 (누르면 위아래)'), findsOneWidget); - expect(ExplorerButton.fromSettings(const []).take(9).map((b) => b.name), + expect(ExplorerButton.fromSettings(const [], android: false).take(9).map((b) => b.name), ['copy', 'move', 'delete', 'newFolder', 'rename', 'select', 'search', 'bookmarks', 'orient']); - expect(ExplorerButton.fromSettings(const ['hidden', 'copy']), [ExplorerButton.hidden, ExplorerButton.copy], + expect(ExplorerButton.fromSettings(const ['hidden', 'copy'], android: false), [ExplorerButton.hidden, ExplorerButton.copy], reason: '사용자가 정한 순서는 그대로'); // 넉넉한 화면에서는 [더 보기] 없이 모두 tester.view.physicalSize = const Size(1600, 1400); diff --git a/test/reader_more_test.dart b/test/reader_more_test.dart index f332927..90e7819 100644 --- a/test/reader_more_test.dart +++ b/test/reader_more_test.dart @@ -51,13 +51,11 @@ class _FolderSized extends ImageFilesSource { void main() { late List screenOn; - setUp(() { - screenOn = []; - ReaderPage.keepScreenOn = (on) async => screenOn.add(on); - }); + setUp(() => screenOn = []); - AppController controller() => - AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); + // 설계 M4 WakeLock: 화면 켜 두기는 PlatformServices.keepScreenOn 으로 넘긴다 + AppController controller() => AppController(PlatformServices( + mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService(), keepScreenOn: (on) async => screenOn.add(on))); Future<_Sized> source(WidgetTester t, List<(int, int)> sizes) async { final images = []; diff --git a/test/reader_test.dart b/test/reader_test.dart index 877a862..a5dcfb7 100644 --- a/test/reader_test.dart +++ b/test/reader_test.dart @@ -13,6 +13,7 @@ import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart'; import 'package:jj_mkvmaker/services/platform_services.dart'; import 'package:jj_mkvmaker/ui/reader_page.dart'; import 'package:path/path.dart' as p; +import 'package:jj_mkvmaker/services/platform_info.dart'; Uint8List _png(int r, int g, int b, {int w = 40, int h = 60}) { final im = img.Image(width: w, height: h); @@ -219,14 +220,13 @@ void main() { 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().reader.volumeKeys, 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())); + // 설계 M4: Android 인지는 PlatformInfo 주입 (예전 전역 함수 ReaderPage.volumeKeysSupported) + final c = AppController( + PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService(), platform: PlatformInfo.android)); await tester.pumpWidget(MaterialApp(home: ReaderPage(c: c, source: ImageFilesSource(files, tempDir: tmp.path, title: 'imgs')))); await tester.pump(); // 끔: 볼륨 키를 가로채지 않는다 (시스템 볼륨이 바뀜) @@ -244,8 +244,12 @@ void main() { await tester.sendKeyEvent(LogicalKeyboardKey.audioVolumeUp); await tester.pumpAndSettle(); expect(find.text('2 / 3'), findsOneWidget); - // Android 가 아니면 쓰지 않음 - ReaderPage.volumeKeysSupported = () => false; + // Android 가 아니면 쓰지 않음 (같은 설정 · Windows 조립) + final win = AppController( + PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService(), platform: PlatformInfo.windows)); + win.settings.reader.volumeKeys = true; + await tester.pumpWidget(MaterialApp(home: ReaderPage(c: win, source: ImageFilesSource(files, tempDir: tmp.path, title: 'imgs')))); + await tester.pump(); expect(await tester.sendKeyEvent(LogicalKeyboardKey.audioVolumeDown), isFalse); await tester.pumpWidget(const SizedBox()); });