diff --git a/integration_test/reader_dialog_semantics_test.dart b/integration_test/reader_dialog_semantics_test.dart index 9470b00..2c04a64 100644 --- a/integration_test/reader_dialog_semantics_test.dart +++ b/integration_test/reader_dialog_semantics_test.dart @@ -30,7 +30,7 @@ void main() { (File(p.join(tmp.path, 'p$i.png'))..writeAsBytesSync(img.encodePng(img.Image(width: 300, height: 200)))).path, ]; final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); - c.settings.readerGallery = gallery; + c.settings.reader.gallery = gallery; Future wait(int ms) async { for (var i = 0; i < ms ~/ 100; i++) { await t.runAsync(() => Future.delayed(const Duration(milliseconds: 100))); diff --git a/lib/app/settings.dart b/lib/app/settings.dart index e2578eb..1a414ef 100644 --- a/lib/app/settings.dart +++ b/lib/app/settings.dart @@ -15,6 +15,7 @@ import '../services/app_shell.dart' show appIconOf; import '../core/remote_idle.dart'; import 'settings/records.dart'; import 'settings/settings_group.dart'; +import 'settings/reader_settings.dart'; import 'settings/ai_image_settings.dart'; import 'settings/copy_settings.dart'; import 'settings/tor_settings.dart'; @@ -24,6 +25,7 @@ import 'settings/explorer_settings.dart'; export 'settings/records.dart'; export 'settings/settings_group.dart'; export 'settings/settings_store.dart'; +export 'settings/reader_settings.dart'; export 'settings/ai_image_settings.dart'; export 'settings/copy_settings.dart'; export 'settings/tor_settings.dart'; @@ -254,30 +256,13 @@ class AppSettings { /// 그림 보기 (만화 보기) 대상 확장자 (파일 탐색기에서 두 번 누르면 보기로 연다) List imageExts = [...defaultImageExtensions]; - /// 보기: 'page' 한 쪽 맞추기 · 'width' 좌우 맞추기 (세로로 밀어 봄) - String readerFit = 'page'; - /// 보기: 오른쪽에서 왼쪽으로 넘김 (일본 만화) - bool readerRtl = false; - /// 보기: 밝기 (-0.7 어둡게 ~ 0.7 밝게) - double readerBrightness = 0; - /// 30 · 101: 보기 화면에서 위 · 아래 시스템 막대 (시계 · 알림 · 뒤로) 를 그대로 보이기 (기본: 보임 - 기기의 평소 동작, 화면 가득은 버튼 · 설정으로) - bool readerSystemBars = true; - /// 117: 보기 화면 "계속 보기" 간격 (초, 1 ~ 60) - int readerAutoSeconds = 3; - /// X-plore 맞추기 (관리자 지시 10/20): 그림 보기 화면 아래 갤러리 줄 (썸네일, 누르면 그 장으로). 처음 끔 - bool readerGallery = false; - /// X-plore 맞추기 (관리자 지시 10/20): 보기 화면에서 볼륨 키로 넘기기 (아래 = 다음 장, 위 = 이전 장, Android). 처음 끔. - /// 보기 화면에서만 가로챈다 (동영상 · 다른 화면의 볼륨은 그대로) - bool readerVolumeKeys = false; - /// 119: 한 장에 두 쪽이 붙은 그림을 반씩 나눠 보기 - 'auto' 가로가 세로보다 긴 그림만 · 'on' 늘 · 'off' 안 함 - String readerSplit = 'auto'; /// 99 (Windows): 시작 메뉴에 등록 - 알림이 JJ_MKVMaker 로 보이고 눌러서 열린다 bool startMenuShortcut = true; @@ -322,8 +307,6 @@ class AppSettings { /// 내장 플레이어 자막 글자 크기 배율 (0.5 ~ 2.5, 기본 1) double subtitleScale = 1.0; - /// ZIP · CBZ 를 두 번 누르면 안의 그림을 만화처럼 보기 (끄면 목록). 기본은 목록 (사용자 지시) - bool zipComic = false; /// WebDAV 서버 (파일 탐색기 · Rsync 화면 위쪽 "SD 카드" 옆 탭). 비밀번호는 이 설정 파일에만. List webdavServers = []; @@ -434,8 +417,11 @@ class AppSettings { /// AI 그림 · 해상도 올리기 (123 · 121) final aiImage = AiImageSettings(); + /// 보기 화면 (그림 · 만화 · PDF · 문서) + final reader = ReaderSettings(); + /// 설정 분야들 (설정 A) - 저마다 자기 키를 읽고 쓴다. toJson · fromJson 은 이 목록을 돌며 합치기만 한다 - List get groups => [explorer, network, tor, copy, aiImage]; + List get groups => [explorer, network, tor, copy, aiImage, reader]; /// [plainSecrets]: 안전 저장소를 쓸 수 없을 때만 (예전 파일에 있던 비밀 값을 잃지 않게) 예전처럼 파일에 둔다 Map toJson({bool plainSecrets = false}) { @@ -513,14 +499,6 @@ class AppSettings { 'navOrder': navOrder, 'swipeNav': swipeNav, 'imageExts': imageExts, - 'readerFit': readerFit, - 'readerRtl': readerRtl, - 'readerBrightness': readerBrightness, - 'readerSystemBars': readerSystemBars, - 'readerAutoSeconds': readerAutoSeconds, - 'readerGallery': readerGallery, - 'readerVolumeKeys': readerVolumeKeys, - 'readerSplit': readerSplit, 'startMenuShortcut': startMenuShortcut, 'swipeWrap': swipeWrap, 'browserSwipeHinted': browserSwipeHinted, @@ -535,9 +513,6 @@ class AppSettings { // 116: 예전 판의 표시 ('allFilesHintShown') 는 권한이 없어진 것을 몰라 다시 띄우지 못했다 → 새 이름으로 한 번 더 'allFilesHint2': allFilesHintShown, 'subtitleScale': subtitleScale, - 'zipComic': zipComic, - // 예전 기본값 (만화 보기 켜짐) 은 지시와 반대였으므로 한 번 꺼진 상태 (목록) 로 - 'zipComicV2': true, 'webdavServers': [ for (final x in webdavServers) {...x.toJson(), if (plainSecrets) 'password': x.password}, ], @@ -668,14 +643,6 @@ class AppSettings { ..imageExts = j['imageExts'] is List ? [for (final x in j['imageExts'] as List) '$x'.toLowerCase()] : [...defaultImageExtensions] - ..readerFit = j['readerFit'] == 'width' ? 'width' : 'page' - ..readerRtl = j['readerRtl'] as bool? ?? false - ..readerBrightness = ((j['readerBrightness'] as num?)?.toDouble() ?? 0).clamp(-0.7, 0.7) - ..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 ..swipeWrap = j['swipeWrap'] != false ..browserSwipeHinted = j['browserSwipeHinted'] == true @@ -691,7 +658,6 @@ class AppSettings { ..davExternalOpen = const ['fetch', 'url'].contains(j['davExternalOpen']) ? j['davExternalOpen'] as String : 'ask' ..allFilesHintShown = j['allFilesHint2'] == true ..subtitleScale = ((j['subtitleScale'] as num?)?.toDouble() ?? 1.0).clamp(0.5, 2.5) - ..zipComic = j['zipComic'] == true && j['zipComicV2'] == true ..webdavServers = [ for (final x in (j['webdavServers'] as List?) ?? const []) if (x is Map) DavServer.fromJson(x), @@ -803,7 +769,7 @@ class AppSettings { // 53 · 28: 앱 안 브라우저 쿠키를 쓰던 사람에게 넘기는 사이트가 넓어졌음을 한 번 알린다 if (j['cookieScopeV2'] != true && j.isNotEmpty && (j['ytCookiesBrowser'] ?? internalBrowserCookies) == internalBrowserCookies) 'cookieScope', - if (j['zipComicV2'] != true && j['zipComic'] != false && j.containsKey('imageExts') && !s.zipComic) 'zipComic', + if (j['zipComicV2'] != true && j['zipComic'] != false && j.containsKey('imageExts') && !s.reader.zipComic) 'zipComic', if (j['backgroundV2'] != true && j['runInBackground'] == false && s.runInBackground && Platform.isAndroid) 'background', // 193: 그 설정이 생기기 전 판 (설정 파일은 있는데 그 항목이 없음 - 예: 10.03) 에서 올라온 사람이 바로 겪는 동작 변화. diff --git a/lib/app/settings/reader_settings.dart b/lib/app/settings/reader_settings.dart new file mode 100644 index 0000000..a8d1648 --- /dev/null +++ b/lib/app/settings/reader_settings.dart @@ -0,0 +1,62 @@ + +import 'settings_group.dart'; + +/// 약속 (설정 A): 환경 설정 > 보기 화면 (그림 · 만화 · PDF · 문서) - 자기 키 (이름 · 처음 값 · 읽기 · 쓰기) 를 스스로 갖는다. +/// 설정 하나를 더할 때는 이 파일만 고친다. 저장 파일의 키 이름은 예전 그대로 (평평한 키) +class ReaderSettings implements SettingsGroup { + /// 보기: 'page' 한 쪽 맞추기 · 'width' 좌우 맞추기 (세로로 밀어 봄) + String fit = 'page'; + + /// 보기: 오른쪽에서 왼쪽으로 넘김 (일본 만화) + bool rtl = false; + + /// 보기: 밝기 (-0.7 어둡게 ~ 0.7 밝게) + double brightness = 0; + + /// 30 · 101: 보기 화면에서 위 · 아래 시스템 막대 (시계 · 알림 · 뒤로) 를 그대로 보이기 (기본: 보임 - 기기의 평소 동작, 화면 가득은 버튼 · 설정으로) + bool systemBars = true; + + /// 117: 보기 화면 "계속 보기" 간격 (초, 1 ~ 60) + int autoSeconds = 3; + + /// X-plore 맞추기 (관리자 지시 10/20): 그림 보기 화면 아래 갤러리 줄 (썸네일, 누르면 그 장으로). 처음 끔 + bool gallery = false; + + /// X-plore 맞추기 (관리자 지시 10/20): 보기 화면에서 볼륨 키로 넘기기 (아래 = 다음 장, 위 = 이전 장, Android). 처음 끔. + /// 보기 화면에서만 가로챈다 (동영상 · 다른 화면의 볼륨은 그대로) + bool volumeKeys = false; + + /// 119: 한 장에 두 쪽이 붙은 그림을 반씩 나눠 보기 - 'auto' 가로가 세로보다 긴 그림만 · 'on' 늘 · 'off' 안 함 + String split = 'auto'; + + /// ZIP · CBZ 를 두 번 누르면 안의 그림을 만화처럼 보기 (끄면 목록). 기본은 목록 (사용자 지시) + bool zipComic = false; + + @override + void write(Map j, {required bool plainSecrets}) { + j['readerFit'] = fit; + j['readerRtl'] = rtl; + j['readerBrightness'] = brightness; + j['readerSystemBars'] = systemBars; + j['readerAutoSeconds'] = autoSeconds; + j['readerGallery'] = gallery; + j['readerVolumeKeys'] = volumeKeys; + j['readerSplit'] = split; + j['zipComic'] = zipComic; + // 예전 기본값 (만화 보기 켜짐) 은 지시와 반대였으므로 한 번 꺼진 상태 (목록) 로 + j['zipComicV2'] = true; + } + + @override + void read(Map j) { + fit = j['readerFit'] == 'width' ? 'width' : 'page'; + rtl = j['readerRtl'] as bool? ?? false; + brightness = ((j['readerBrightness'] as num?)?.toDouble() ?? 0).clamp(-0.7, 0.7); + systemBars = j['readerSystemBars'] != false; + autoSeconds = ((j['readerAutoSeconds'] as num?)?.toInt() ?? 3).clamp(1, 60); + gallery = j['readerGallery'] as bool? ?? false; + volumeKeys = j['readerVolumeKeys'] as bool? ?? false; + split = const ['auto', 'on', 'off'].contains(j['readerSplit']) ? j['readerSplit'] as String : 'auto'; + zipComic = j['zipComic'] == true && j['zipComicV2'] == true; + } +} diff --git a/lib/ui/component_settings.dart b/lib/ui/component_settings.dart index a10a2cc..4960739 100644 --- a/lib/ui/component_settings.dart +++ b/lib/ui/component_settings.dart @@ -44,8 +44,8 @@ class ViewerSettings extends StatelessWidget { ), SwitchListTile( secondary: const Icon(Icons.auto_stories_outlined), - value: s.zipComic, - onChanged: (v) => c.updateSettings((x) => x.zipComic = v), + value: s.reader.zipComic, + onChanged: (v) => c.updateSettings((x) => x.reader.zipComic = v), title: Text(tr('ZIP · CBZ 를 만화로 보기')), subtitle: Text(tr('켜면 두 번 눌렀을 때 안의 그림을 바로 넘겨 봅니다. 끄면 목록 (골라서 풀기) 을 엽니다.')), ), @@ -55,20 +55,20 @@ class ViewerSettings extends StatelessWidget { title: Text(tr('두 쪽 나눠 보기')), subtitle: Text(tr('한 장에 두 쪽이 붙은 그림을 반씩 넘겨 봅니다 (읽는 방향의 쪽부터)')), trailing: DropdownButton( - value: s.readerSplit, + value: s.reader.split, items: [ DropdownMenuItem(value: 'auto', child: Text(tr('자동 (가로로 긴 그림만)'))), DropdownMenuItem(value: 'on', child: Text(tr('켜기'))), DropdownMenuItem(value: 'off', child: Text(tr('끄기'))), ], - onChanged: (v) => c.updateSettings((x) => x.readerSplit = v!), + onChanged: (v) => c.updateSettings((x) => x.reader.split = v!), ), ), // X-plore 맞추기 (관리자 지시 10/20): 아래 갤러리 줄 (보기 화면 위 막대에서도 켜고 끔) SwitchListTile( secondary: const Icon(Icons.view_carousel_outlined), - value: s.readerGallery, - onChanged: (v) => c.updateSettings((x) => x.readerGallery = v), + value: s.reader.gallery, + onChanged: (v) => c.updateSettings((x) => x.reader.gallery = v), title: Text(tr('그림 보기 아래 갤러리 줄')), subtitle: Text(tr('그림을 볼 때 아래에 작은 그림 줄을 보입니다 (누르면 그 장으로)')), ), @@ -76,8 +76,8 @@ class ViewerSettings extends StatelessWidget { if (Platform.isAndroid) SwitchListTile( secondary: const Icon(Icons.volume_down_outlined), - value: s.readerVolumeKeys, - onChanged: (v) => c.updateSettings((x) => x.readerVolumeKeys = v), + value: s.reader.volumeKeys, + onChanged: (v) => c.updateSettings((x) => x.reader.volumeKeys = v), title: Text(tr('볼륨 키로 넘기기')), subtitle: Text(tr('보기 화면에서 볼륨 아래 = 다음 장, 위 = 이전 장 (동영상 · 다른 화면의 볼륨은 그대로)')), ), @@ -87,9 +87,9 @@ class ViewerSettings extends StatelessWidget { title: Text(tr('계속 보기 간격')), subtitle: Text(tr('보기 화면의 [계속 보기] 가 다음 장으로 넘기는 간격')), trailing: DropdownButton( - value: s.readerAutoSeconds, + value: s.reader.autoSeconds, items: [for (var n = 1; n <= 60; n++) DropdownMenuItem(value: n, child: Text(trf('{0}초', [n])))], - onChanged: (v) => c.updateSettings((x) => x.readerAutoSeconds = v!), + onChanged: (v) => c.updateSettings((x) => x.reader.autoSeconds = v!), ), ), ]); diff --git a/lib/ui/explorer/open_flow.dart b/lib/ui/explorer/open_flow.dart index 5663b0d..cabf2b1 100644 --- a/lib/ui/explorer/open_flow.dart +++ b/lib/ui/explorer/open_flow.dart @@ -254,7 +254,7 @@ class ExplorerOpener { Future openZipFile(BuildContext context, String path, ExplorerPane? pane) async { try { - if (ctl.c.settings.zipComic && await openZipComic(context, ctl.c, path)) return; + if (ctl.c.settings.reader.zipComic && await openZipComic(context, ctl.c, path)) return; } catch (e) { ctl.host.snack(trf('열 수 없습니다: {0}', [e])); return; diff --git a/lib/ui/reader/reader_bar_items.dart b/lib/ui/reader/reader_bar_items.dart index ef9a3b2..c297afd 100644 --- a/lib/ui/reader/reader_bar_items.dart +++ b/lib/ui/reader/reader_bar_items.dart @@ -65,9 +65,9 @@ List readerBarItems(BuildContext context, AppController c, Reader final pictures = src is ImageFilesSource || src is ZipImagesSource; final aiOn = c.settings.components.contains('aiimage'); final playing = st.playing; - final secs = s.readerAutoSeconds; + final secs = s.reader.autoSeconds; ReaderBarItem one(ReaderAct a) => ReaderBarItem(48, [a]); - final split = switch (s.readerSplit) { + final split = switch (s.reader.split) { 'on' => tr('두 쪽 나눠 보기: 켜기 (누르면 끄기)'), 'off' => tr('두 쪽 나눠 보기: 끄기 (누르면 자동)'), _ => tr('두 쪽 나눠 보기: 자동 - 두 쪽 스캔으로 보이는 그림만 (누르면 켜기)'), @@ -94,30 +94,30 @@ List readerBarItems(BuildContext context, AppController c, Reader if (src is ImageFilesSource) one(ReaderAct(const Icon(Icons.info_outline), tr('사진 정보 (Exif)'), on.photoInfo)), // X-plore 맞추기 (관리자 지시 10/20): 아래 갤러리 줄 켜고 끄기 (그림 원본만) if (pictures) - one(ReaderAct(const Icon(Icons.view_carousel_outlined), s.readerGallery ? tr('아래 갤러리 줄 숨기기') : tr('아래 갤러리 줄 보이기'), - on.toggleGallery, on: s.readerGallery)), + one(ReaderAct(const Icon(Icons.view_carousel_outlined), s.reader.gallery ? tr('아래 갤러리 줄 숨기기') : tr('아래 갤러리 줄 보이기'), + on.toggleGallery, on: s.reader.gallery)), // 119 · 120 - one(ReaderAct(const Icon(Icons.vertical_split_outlined), split, on.cycleSplit, on: s.readerSplit != 'off')), + one(ReaderAct(const Icon(Icons.vertical_split_outlined), split, on.cycleSplit, on: s.reader.split != 'off')), one(ReaderAct(const Icon(Icons.rotate_right), tr('90° 회전 (시계 방향)'), () => on.rotate(1), on: st.turns != 0)), one(ReaderAct(const Icon(Icons.rotate_left), tr('반대로 90° 회전'), () => on.rotate(3), on: st.turns != 0)), one(ReaderAct(const RotatedBox(quarterTurns: 1, child: Icon(Icons.flip)), tr('상하 반전'), on.toggleFlip, on: st.flipV)), - one(ReaderAct(const Icon(Icons.fit_screen_outlined), tr('한 쪽 맞추기'), () => on.updateSettings((x) => x.readerFit = 'page'), - on: s.readerFit == 'page')), + one(ReaderAct(const Icon(Icons.fit_screen_outlined), tr('한 쪽 맞추기'), () => on.updateSettings((x) => x.reader.fit = 'page'), + on: s.reader.fit == 'page')), one(ReaderAct(const Icon(Icons.width_full_outlined), tr('좌우 맞추기 (세로로 밀어 봄)'), - () => on.updateSettings((x) => x.readerFit = 'width'), on: s.readerFit == 'width')), + () => on.updateSettings((x) => x.reader.fit = 'width'), on: s.reader.fit == 'width')), one(ReaderAct(const Icon(Icons.swap_horiz), - s.readerRtl ? tr('넘기는 방향: 오른쪽 → 왼쪽 (만화)') : tr('넘기는 방향: 왼쪽 → 오른쪽'), - () => on.updateSettings((x) => x.readerRtl = !x.readerRtl), on: s.readerRtl)), + s.reader.rtl ? tr('넘기는 방향: 오른쪽 → 왼쪽 (만화)') : tr('넘기는 방향: 왼쪽 → 오른쪽'), + () => on.updateSettings((x) => x.reader.rtl = !x.reader.rtl), on: s.reader.rtl)), one(ReaderAct(const Icon(Icons.brightness_low), tr('어둡게'), - () => on.updateSettings((x) => x.readerBrightness = (x.readerBrightness - 0.1).clamp(-0.7, 0.7)))), + () => on.updateSettings((x) => x.reader.brightness = (x.reader.brightness - 0.1).clamp(-0.7, 0.7)))), one(ReaderAct(const Icon(Icons.brightness_high), tr('밝게'), - () => on.updateSettings((x) => x.readerBrightness = (x.readerBrightness + 0.1).clamp(-0.7, 0.7)))), + () => on.updateSettings((x) => x.reader.brightness = (x.reader.brightness + 0.1).clamp(-0.7, 0.7)))), // 30: 시계 · 알림 · 뒤로 막대를 그대로 / 화면 가득 - one(ReaderAct(Icon(s.readerSystemBars ? Icons.fullscreen : Icons.fullscreen_exit), - s.readerSystemBars ? tr('화면 가득 (시스템 막대 숨기기)') : tr('시스템 막대 보이기 (시계 · 알림 · 뒤로)'), () async { - await on.updateSettings((x) => x.readerSystemBars = !x.readerSystemBars); + one(ReaderAct(Icon(s.reader.systemBars ? Icons.fullscreen : Icons.fullscreen_exit), + s.reader.systemBars ? tr('화면 가득 (시스템 막대 숨기기)') : tr('시스템 막대 보이기 (시계 · 알림 · 뒤로)'), () async { + await on.updateSettings((x) => x.reader.systemBars = !x.reader.systemBars); on.applySystemBars(); - }, on: s.readerSystemBars)), + }, on: s.reader.systemBars)), // AI 는 막대가 좁으면 먼저 ⋮ 로 (늘 쓰는 버튼이 막대에 남게) // 121: AI 해상도 올리기 (이 장 · 모든 장) - 새 파일로 저장, 원본 · ZIP 은 그대로 if (aiOn && src is! PdfSource) ...[ diff --git a/lib/ui/reader_page.dart b/lib/ui/reader_page.dart index 88c9c47..586657c 100644 --- a/lib/ui/reader_page.dart +++ b/lib/ui/reader_page.dart @@ -123,7 +123,7 @@ class _ReaderPageState extends State { /// 보기 화면은 위 · 아래 시스템 막대까지 그림으로 (30: 설정으로 막대를 그대로 둘 수 있다) void _applySystemBars() => SystemChrome.setEnabledSystemUIMode( - c.settings.readerSystemBars ? SystemUiMode.edgeToEdge : SystemUiMode.immersiveSticky); + c.settings.reader.systemBars ? SystemUiMode.edgeToEdge : SystemUiMode.immersiveSticky); // ───────── 쪽 나누기 (119) · 회전 · 반전 (120) ───────── @@ -139,14 +139,14 @@ class _ReaderPageState extends State { bool _flipV = false; /// 이 장을 반씩 나눠 볼지. 자동은 두 쪽 스캔일 가능성이 높을 때만 ([autoSplitsPage]) - bool _splits(int page) => splitsPage(c.settings.readerSplit, page, _dims, + bool _splits(int page) => splitsPage(c.settings.reader.split, page, _dims, turns: _turns, // 관리자 지시 (10/20) · 내 판단: HWP · HWPX 문서의 가로 쪽 (가로 표 등) 은 만화 두 쪽 스캔이 아니라 나누지 않는다 bundle: src is! ImageFilesSource && src is! HwpSource); /// 223: 나눠 본 장에서 바로 한 장으로 (설정을 "끄기" 로) Future _splitOff() async { - await c.updateSettings((x) => x.readerSplit = 'off'); + await c.updateSettings((x) => x.reader.split = 'off'); if (!mounted) return; setState(() { _resetZoom(); @@ -177,7 +177,7 @@ class _ReaderPageState extends State { unawaited(ReaderImageCache.imageSize(r).then((size) { if (!mounted || size == null || v != _version || _dims.containsKey(page)) return; _dims[page] = size; - if (c.settings.readerSplit == 'auto') setState(_rebuildPages); + if (c.settings.reader.split == 'auto') setState(_rebuildPages); })); } @@ -188,7 +188,7 @@ class _ReaderPageState extends State { }); Future _cycleSplit() async { - await c.updateSettings((x) => x.readerSplit = switch (x.readerSplit) { 'auto' => 'on', 'on' => 'off', _ => 'auto' }); + await c.updateSettings((x) => x.reader.split = switch (x.reader.split) { 'auto' => 'on', 'on' => 'off', _ => 'auto' }); if (!mounted) return; setState(() { _resetZoom(); @@ -198,7 +198,7 @@ class _ReaderPageState extends State { // ───────── 계속 보기 (117) ───────── - late final _show = ReaderSlideshow(seconds: () => c.settings.readerAutoSeconds, onTick: _tick); + late final _show = ReaderSlideshow(seconds: () => c.settings.reader.autoSeconds, onTick: _tick); bool get _playing => _show.playing; /// 계속 보기를 멈추게 한 누름은 장 넘기기 · 막대 숨기기로 쓰지 않는다 @@ -235,7 +235,7 @@ class _ReaderPageState extends State { } Future _setSeconds(int d) async { - await c.updateSettings((x) => x.readerAutoSeconds = (x.readerAutoSeconds + d).clamp(1, 60)); + await c.updateSettings((x) => x.reader.autoSeconds = (x.reader.autoSeconds + d).clamp(1, 60)); if (!mounted) return; setState(() {}); if (_playing) _play(true); // 새 간격으로 (지금 장이 보이면 처음부터 다시) @@ -311,7 +311,7 @@ class _ReaderPageState extends State { /// X-plore 맞추기 (관리자 지시 10/20): 볼륨 키로 넘기기 (설정을 켰고 Android 일 때만 - 처리한 키는 시스템 볼륨을 바꾸지 않는다) KeyEventResult _onKey(FocusNode _, KeyEvent e) { - final step = volumeKeyStep(e, enabled: c.settings.readerVolumeKeys && ReaderPage.volumeKeysSupported()); + final step = volumeKeyStep(e, enabled: c.settings.reader.volumeKeys && ReaderPage.volumeKeysSupported()); if (step == null) return KeyEventResult.ignored; if (step != 0) _go(step); return KeyEventResult.handled; @@ -332,7 +332,7 @@ class _ReaderPageState extends State { _swallowTap = false; return; } - final step = tapZone(d.localPosition.dx, width, rtl: c.settings.readerRtl); + final step = tapZone(d.localPosition.dx, width, rtl: c.settings.reader.rtl); if (step != 0) { _go(step); } else { @@ -357,7 +357,7 @@ class _ReaderPageState extends State { imageKey: '$_version:${slot.page}:${slot.part}:${_retry[slot.page] ?? 0}', image: _images.sharp(i) ?? (slot.part >= 0 ? _images.half(slot.page, _width) : _load(slot.page)), zoom: _zoom.of(i), zoomed: _zoom.zoomed, holdPage: _zoom.holdPage, - turns: _turns, flipV: _flipV, rtl: c.settings.readerRtl, fit: c.settings.readerFit, box: box, + turns: _turns, flipV: _flipV, rtl: c.settings.reader.rtl, fit: c.settings.reader.fit, box: box, onShown: () => _markShown(i), onZoomed: _setZoomed, onZoomEnd: () => _onZoomEnd(i), // 관리자 지시 (10/20, HWP 2단계): [다시 시도] - 그 장의 기억을 지우고 다시 부른다 onRetry: () => setState(() { @@ -430,7 +430,7 @@ class _ReaderPageState extends State { } Future _toggleGallery() async { - await c.updateSettings((x) => x.readerGallery = !x.readerGallery); + await c.updateSettings((x) => x.reader.gallery = !x.reader.gallery); if (!mounted) return; setState(() {}); WidgetsBinding.instance.addPostFrameCallback((_) => _scrollGallery(animate: false)); @@ -449,9 +449,9 @@ class _ReaderPageState extends State { final n = src.length; return ReaderBottomBar( pages: n, slots: _v.length, index: _index, label: slotLabel(_cur), - pageName: n == 0 ? '' : src.pageName(_cur.page), rtl: c.settings.readerRtl, onSlide: _pages.jumpToPage, - gallery: _pictures && c.settings.readerGallery && n > 0 - ? ReaderGalleryStrip(controller: _gallery, count: n, current: _cur.page, rtl: c.settings.readerRtl, + pageName: n == 0 ? '' : src.pageName(_cur.page), rtl: c.settings.reader.rtl, onSlide: _pages.jumpToPage, + gallery: _pictures && c.settings.reader.gallery && n > 0 + ? ReaderGalleryStrip(controller: _gallery, count: n, current: _cur.page, rtl: c.settings.reader.rtl, thumb: _images.thumb, onTap: _goPage) : null, ); @@ -478,9 +478,9 @@ class _ReaderPageState extends State { version: _version, count: _v.length, controller: _pages, - rtl: s.readerRtl, + rtl: s.reader.rtl, holdPage: _zoom.holdPage, - brightness: s.readerBrightness, + brightness: s.reader.brightness, onWidth: (w) => _width = w, onPointerDown: _onPointerDown, onPointerUp: () => _finger(-1), diff --git a/test/decisions_test.dart b/test/decisions_test.dart index 7eefc7e..a82368b 100644 --- a/test/decisions_test.dart +++ b/test/decisions_test.dart @@ -28,7 +28,7 @@ void main() { final s = AppSettings(); expect([ s.startMenuShortcut, s.copy.syncStopToast, s.swipeWrap, s.migrationNotice, s.allowInnerToOuter, - s.rememberPasswords, s.recycleOnDelete, s.readerSystemBars, + s.rememberPasswords, s.recycleOnDelete, s.reader.systemBars, ], everyElement(isTrue)); expect(s.cleanupPrechecked, ['work', 'download']); expect(s.tripleTapAction, 'move'); @@ -37,7 +37,7 @@ void main() { expect([s.webPauseOnLeave, s.autoCheckUpdates, s.youtubeAdSkip, s.youtubeAdHide, s.runInBackground, s.swipeNav], everyElement(isTrue)); // 탐색기 처음 배치 = 두 창 (규칙 7 · 관리자 결정 3, 10/22) - expect([s.copy.liveSyncOnStart, s.copy.liveSyncIntervalSec, s.explorer.layout, s.zipComic], ['auto', 30, 'dual', false]); + expect([s.copy.liveSyncOnStart, s.copy.liveSyncIntervalSec, s.explorer.layout, s.reader.zipComic], ['auto', 30, 'dual', false]); expect([AppSettings.uiScaleMin, AppSettings.uiScaleMax], [0.5, 2.5]); // 바꾼 값은 남는다 final b = AppSettings.fromJson((AppSettings() diff --git a/test/reader_more_test.dart b/test/reader_more_test.dart index 1228b58..f332927 100644 --- a/test/reader_more_test.dart +++ b/test/reader_more_test.dart @@ -84,15 +84,15 @@ void main() { test('117 · 119 설정: 처음 값 3초 · 자동, 1 ~ 60 초', () { final s = AppSettings.fromJson({}); - expect((s.readerAutoSeconds, s.readerSplit), (3, 'auto')); + expect((s.reader.autoSeconds, s.reader.split), (3, 'auto')); final b = AppSettings.fromJson((AppSettings() - ..readerAutoSeconds = 12 - ..readerSplit = 'on') + ..reader.autoSeconds = 12 + ..reader.split = 'on') .toJson()); - expect((b.readerAutoSeconds, b.readerSplit), (12, 'on')); - expect(AppSettings.fromJson({'readerAutoSeconds': 500}).readerAutoSeconds, 60); - expect(AppSettings.fromJson({'readerAutoSeconds': 0}).readerAutoSeconds, 1); - expect(AppSettings.fromJson({'readerSplit': 'x'}).readerSplit, 'auto'); + expect((b.reader.autoSeconds, b.reader.split), (12, 'on')); + expect(AppSettings.fromJson({'readerAutoSeconds': 500}).reader.autoSeconds, 60); + expect(AppSettings.fromJson({'readerAutoSeconds': 0}).reader.autoSeconds, 1); + expect(AppSettings.fromJson({'readerSplit': 'x'}).reader.split, 'auto'); }); testWidgets('119: 자동이면 가로로 긴 그림만 반씩 (12-1 · 12-2), 표지 (세로) 는 통째 · 반쪽은 두 배 너비로 읽음', (t) async { @@ -113,17 +113,17 @@ void main() { // 버튼: 자동 → 켜기 (모든 장) → 끄기 → 자동 await t.tap(find.byTooltip('두 쪽 나눠 보기: 자동 - 두 쪽 스캔으로 보이는 그림만 (누르면 켜기)')); await t.pumpAndSettle(); - expect(c.settings.readerSplit, 'on'); + expect(c.settings.reader.split, 'on'); expect(find.text('2-2 / 3장'), findsOneWidget, reason: '보던 쪽 그대로'); await tapAt(t, 850); expect(find.text('3-1 / 3장'), findsOneWidget); await t.tap(find.byTooltip('두 쪽 나눠 보기: 켜기 (누르면 끄기)')); await t.pumpAndSettle(); - expect(c.settings.readerSplit, 'off'); + expect(c.settings.reader.split, 'off'); expect(find.text('3 / 3'), findsOneWidget); await t.tap(find.byTooltip('두 쪽 나눠 보기: 끄기 (누르면 자동)')); await t.pumpAndSettle(); - expect(c.settings.readerSplit, 'auto'); + expect(c.settings.reader.split, 'auto'); }); test('223: 자동 나누기 판단 - 묶음 (ZIP · CBZ · PDF) 의 가로 그림은 나눔, 폴더는 다른 그림 대부분이 세로일 때만', () { @@ -154,12 +154,12 @@ void main() { expect(find.text('1-1 / 1장'), findsOneWidget); await t.tap(find.byTooltip('두 쪽 나눔 끄기 (한 장으로 보기)')); await t.pumpAndSettle(); - expect(c.settings.readerSplit, 'off'); + expect(c.settings.reader.split, 'off'); expect(find.text('1 / 1'), findsOneWidget); }); testWidgets('119: 오른쪽 → 왼쪽으로 읽으면 오른쪽 반부터', (t) async { - final c = controller()..settings.readerRtl = true; + final c = controller()..settings.reader.rtl = true; final src = await source(t, [(200, 100)]); await open(t, c, src); expect(find.text('1-1 / 1장'), findsOneWidget); @@ -199,13 +199,13 @@ void main() { await open(t, c, src); await t.tap(find.byTooltip('계속 보기 간격 늘리기 (지금 3초)')); await t.pumpAndSettle(); - expect(c.settings.readerAutoSeconds, 4); + expect(c.settings.reader.autoSeconds, 4); expect(find.text('4초'), findsOneWidget); await t.tap(find.byTooltip('계속 보기 간격 줄이기 (지금 4초)')); await t.pumpAndSettle(); await t.tap(find.byTooltip('계속 보기 간격 줄이기 (지금 3초)')); await t.pumpAndSettle(); - expect(c.settings.readerAutoSeconds, 2); + expect(c.settings.reader.autoSeconds, 2); await t.tap(find.byTooltip('계속 보기 (2초마다 다음 장)')); await t.pump(); @@ -298,7 +298,7 @@ void main() { } await t.tap(find.text('넘기는 방향: 왼쪽 → 오른쪽')); await t.pumpAndSettle(); - expect(c.settings.readerRtl, isTrue); + expect(c.settings.reader.rtl, isTrue); // 넓으면 메뉴 없이 모두 await open(t, c, src, width: 1200); expect(find.byTooltip('더 보기'), findsNothing); diff --git a/test/reader_test.dart b/test/reader_test.dart index c512e38..a8fed6f 100644 --- a/test/reader_test.dart +++ b/test/reader_test.dart @@ -58,12 +58,12 @@ void main() { test('보기 설정 저장 · 읽기', () { final s = AppSettings() ..imageExts = ['png', 'webp'] - ..readerFit = 'width' - ..readerRtl = true - ..readerBrightness = 0.3 - ..zipComic = false; + ..reader.fit = 'width' + ..reader.rtl = true + ..reader.brightness = 0.3 + ..reader.zipComic = false; final b = AppSettings.fromJson(s.toJson()); - expect([b.imageExts, b.readerFit, b.readerRtl, b.readerBrightness, b.zipComic], [ + expect([b.imageExts, b.reader.fit, b.reader.rtl, b.reader.brightness, b.reader.zipComic], [ ['png', 'webp'], 'width', true, 0.3, false, ]); expect(AppSettings.fromJson({}).imageExts, defaultImageExtensions); @@ -140,15 +140,15 @@ void main() { // 만화 (오른쪽 → 왼쪽): 왼쪽을 누르면 다음 await tester.tap(find.byTooltip('넘기는 방향: 왼쪽 → 오른쪽')); await tester.pumpAndSettle(); - expect(c.settings.readerRtl, isTrue); + expect(c.settings.reader.rtl, isTrue); await tapAt(50); expect(find.text('3 / 3'), findsOneWidget); // 맞추기 · 밝기 await tester.tap(find.byTooltip('좌우 맞추기 (세로로 밀어 봄)')); await tester.tap(find.byTooltip('밝게')); await tester.pump(); - expect(c.settings.readerFit, 'width'); - expect(c.settings.readerBrightness, closeTo(0.1, 1e-9)); + expect(c.settings.reader.fit, 'width'); + expect(c.settings.reader.brightness, closeTo(0.1, 1e-9)); }); testWidgets('30: 두 손가락 확대 → 밀어도 장이 넘어가지 않음 · 더 크게 다시 그림 · 시스템 막대 보이기 설정', (tester) async { @@ -181,20 +181,20 @@ void main() { await tester.pumpAndSettle(); expect(find.text('1 / 2'), findsOneWidget); // 101: 기본은 시스템 막대 보임, 버튼으로 화면 가득 (설정에 남음) - expect(c.settings.readerSystemBars, isTrue); - expect(AppSettings.fromJson({}).readerSystemBars, isTrue); + expect(c.settings.reader.systemBars, isTrue); + expect(AppSettings.fromJson({}).reader.systemBars, isTrue); await tester.tap(find.byTooltip('화면 가득 (시스템 막대 숨기기)')); await tester.pumpAndSettle(); - expect(c.settings.readerSystemBars, isFalse); - expect(AppSettings.fromJson(c.settings.toJson()).readerSystemBars, isFalse); + expect(c.settings.reader.systemBars, isFalse); + expect(AppSettings.fromJson(c.settings.toJson()).reader.systemBars, isFalse); }); testWidgets('X-plore 맞추기: 아래 갤러리 줄 - 처음은 끔, 켜면 썸네일 · 누르면 그 장으로, 위 막대에서 끄기', (tester) async { tester.view.physicalSize = const Size(1400, 1200); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); - expect(AppSettings().readerGallery, isFalse, reason: '처음 값은 지금 동작 그대로'); - expect(AppSettings.fromJson({'readerGallery': true}).readerGallery, isTrue); + expect(AppSettings().reader.gallery, isFalse, reason: '처음 값은 지금 동작 그대로'); + expect(AppSettings.fromJson({'readerGallery': true}).reader.gallery, isTrue); 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, ]; @@ -204,7 +204,7 @@ void main() { expect(find.byKey(const ValueKey('thumb0')), findsNothing, reason: '처음은 끔'); await tester.tap(find.byTooltip('아래 갤러리 줄 보이기')); await tester.pumpAndSettle(); - expect(c.settings.readerGallery, isTrue); + expect(c.settings.reader.gallery, isTrue); expect(find.byKey(const ValueKey('thumb2')), findsOneWidget); await tester.tap(find.byKey(const ValueKey('thumb2'))); await tester.pumpAndSettle(); @@ -222,7 +222,7 @@ void main() { final was = ReaderPage.volumeKeysSupported; addTearDown(() => ReaderPage.volumeKeysSupported = was); ReaderPage.volumeKeysSupported = () => true; - expect(AppSettings().readerVolumeKeys, isFalse); + 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, ]; @@ -234,7 +234,7 @@ void main() { await tester.pumpAndSettle(); expect(find.text('1 / 3'), findsOneWidget); // 켬 - c.settings.readerVolumeKeys = true; + c.settings.reader.volumeKeys = true; expect(await tester.sendKeyEvent(LogicalKeyboardKey.audioVolumeDown), isTrue, reason: '처리함 - 시스템 볼륨은 바뀌지 않음'); await tester.pumpAndSettle(); expect(find.text('2 / 3'), findsOneWidget); diff --git a/test/settings_store_test.dart b/test/settings_store_test.dart index f5fe23c..309e286 100644 --- a/test/settings_store_test.dart +++ b/test/settings_store_test.dart @@ -143,7 +143,7 @@ void main() { expect(old.screenOrientation, 'auto'); // 규칙 7 · 결정 3 (10/22): 처음 배치가 두 창이 되어 예전 'dual' 은 그대로 (옮겼다는 알림 없음) expect(old.explorer.layout, 'dual'); - expect(old.zipComic, isFalse); + expect(old.reader.zipComic, isFalse); expect(old.migrated, containsAll(['orientation', 'zipComic'])); expect(old.migrated, isNot(contains('explorerLayout'))); // 다시 저장 · 읽기: 이제 사용자가 고른 값으로 보고 그대로 · 알림 없음