diff --git a/integration_test/tor_dns_real_test.dart b/integration_test/tor_dns_real_test.dart index 59ac2c4..93ccf8e 100644 --- a/integration_test/tor_dns_real_test.dart +++ b/integration_test/tor_dns_real_test.dart @@ -126,7 +126,7 @@ void main() { expect(TorBundle.windowsExe, isNotNull); expect(TorBundle.installedVersion(), TorBundle.version); c.settings - ..components = [...c.settings.components, 'tor'] + ..display.components = [...c.settings.display.components, 'tor'] ..tor.noticeSeen = true; await tester.pumpWidget(MaterialApp( builder: (context, child) => AppScope(controller: c, bookmarks: bm, child: child!), diff --git a/integration_test/web_translate_test.dart b/integration_test/web_translate_test.dart index 00e3377..2dd2fea 100644 --- a/integration_test/web_translate_test.dart +++ b/integration_test/web_translate_test.dart @@ -56,7 +56,7 @@ void main() { final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings ..webViewDataDir = dataDir - ..uiLanguage = 'ko' + ..display.language = 'ko' ..browser.translate = true; final bm = BookmarksController(p.join(dataDir, 'bm.json')); await tester.pumpWidget(MaterialApp(home: BrowserPage(c: c, bookmarks: bm, initialUrl: url))); diff --git a/integration_test/windows_662_shot_test.dart b/integration_test/windows_662_shot_test.dart index 4b53de6..54d767c 100644 --- a/integration_test/windows_662_shot_test.dart +++ b/integration_test/windows_662_shot_test.dart @@ -45,8 +45,8 @@ void main() { AppController plain() { final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings - ..components = ['mkv', 'explorer', 'rsync'] - ..navOrder = ['mkv', 'explorer', 'rsync']; + ..display.components = ['mkv', 'explorer', 'rsync'] + ..display.navOrder = ['mkv', 'explorer', 'rsync']; return c; } @@ -90,8 +90,8 @@ void main() { final c = plain(); // 실제 앱처럼 웹 브라우저도 화면 목록에 (그래야 위쪽 막대에 브라우저 아이콘이 있다) c.settings - ..components = ['mkv', 'browser', 'explorer', 'rsync'] - ..navOrder = ['mkv', 'browser', 'explorer', 'rsync']; + ..display.components = ['mkv', 'browser', 'explorer', 'rsync'] + ..display.navOrder = ['mkv', 'browser', 'explorer', 'rsync']; final base = Platform.environment['JJ_SHOT_TMP']; final dir = testTempDir('jj_shot_662_', base: base); final bm = BookmarksController(p.join(dir.path, 'bookmarks.json')); diff --git a/integration_test/windows_i18n_shot_test.dart b/integration_test/windows_i18n_shot_test.dart index c73972e..3e06cb7 100644 --- a/integration_test/windows_i18n_shot_test.dart +++ b/integration_test/windows_i18n_shot_test.dart @@ -50,7 +50,7 @@ void main() { t.view.physicalSize = const Size(1280, 800); t.view.devicePixelRatio = 1; final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); - if (Platform.environment['JJ_SHOT_THEME'] == 'light') c.settings.uiBrightness = 'light'; + if (Platform.environment['JJ_SHOT_THEME'] == 'light') c.settings.display.brightness = 'light'; await t.runAsync(() => i18n.apply(lang, save: false)); await t.runAsync(() => t.pumpWidget(RepaintBoundary(key: shotKey, child: JjMkvMakerApp(controller: c, navigatorKey: navKey)))); await settle(t); @@ -92,7 +92,7 @@ void main() { t.view.devicePixelRatio = 1; addTearDown(t.view.reset); final c = AppController(PlatformServices.create()); - if (Platform.environment['JJ_SHOT_THEME'] == 'light') c.settings.uiBrightness = 'light'; + if (Platform.environment['JJ_SHOT_THEME'] == 'light') c.settings.display.brightness = 'light'; for (var i = 0; i < checked; i++) { final v = VideoItem(p.join(Directory.systemTemp.path, 'jj_i18n_none', 'v$i.mp4')); c.videos.add(v); diff --git a/integration_test/windows_misc_shot_test.dart b/integration_test/windows_misc_shot_test.dart index fbffba8..0488777 100644 --- a/integration_test/windows_misc_shot_test.dart +++ b/integration_test/windows_misc_shot_test.dart @@ -61,7 +61,7 @@ void main() { final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings ..tor.noticeSeen = true - ..components = [...c.settings.components, 'tor']; + ..display.components = [...c.settings.display.components, 'tor']; final key = GlobalKey(); Future shot(String name, Size size, Widget home, {Future Function()? then}) async { diff --git a/integration_test/windows_mkvportrait_shot_test.dart b/integration_test/windows_mkvportrait_shot_test.dart index afa0b62..6f852fc 100644 --- a/integration_test/windows_mkvportrait_shot_test.dart +++ b/integration_test/windows_mkvportrait_shot_test.dart @@ -45,7 +45,7 @@ void main() { createRecognizer: () => throw UnimplementedError(), createTranslator: () => throw UnimplementedError(), )); - c.settings.showLog = true; + c.settings.display.showLog = true; c.settings.moveTargets = [MoveTarget('보관', p.join(tmp.path, '보관'))]; for (var i = 1; i <= 4; i++) { c.videos.add(VideoItem(p.join(tmp.path, '아주 긴 이름의 동영상 파일 $i.mp4'))); diff --git a/lib/app/components.dart b/lib/app/components.dart index 1cd48fa..5ce9e9e 100644 --- a/lib/app/components.dart +++ b/lib/app/components.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; /// 앱의 기능 묶음 (컴포넌트). 코드는 앱에 들어 있고, 환경 설정 > 컴포넌트에서 설치 (필요한 파일을 받아 켬) · 제거 (끔) 한다. -/// 화면이 있는 것 ([page]) 은 위쪽 이동 버튼 · 좌우로 밀기로 오간다 (순서는 [AppSettings.navOrder]). +/// 화면이 있는 것 ([page]) 은 위쪽 이동 버튼 · 좌우로 밀기로 오간다 (순서는 [AppSettings.display.navOrder]). class AppComponent { final String id; diff --git a/lib/app/i18n_controller.dart b/lib/app/i18n_controller.dart index 6a39fdc..57ca8ec 100644 --- a/lib/app/i18n_controller.dart +++ b/lib/app/i18n_controller.dart @@ -84,7 +84,7 @@ class I18nController extends ChangeNotifier { } /// 고를 수 있는 언어: 기본 + 더한 것 - List get available => [...builtIn, ...?_app?.settings.uiLanguagesAdded]; + List get available => [...builtIn, ...?_app?.settings.display.languagesAdded]; /// 더할 수 있는 언어 (AI 번역 모델이 아는 언어 중 아직 없는 것) List get addable => [ @@ -119,7 +119,7 @@ class I18nController extends ChangeNotifier { } /// 설정에서 고른 값 ('system' 이면 시스템 언어 따르기) - String get chosen => _app?.settings.uiLanguage ?? uiLanguage; + String get chosen => _app?.settings.display.language ?? uiLanguage; /// 화면 언어 바꾸기 (설정에 저장하고 화면 전체를 다시 그린다). [system] 이면 기기 언어를 따른다 Future apply(String code, {bool save = true}) async { @@ -129,8 +129,8 @@ class I18nController extends ChangeNotifier { final dict = code == 'ko' ? const {} : await _read(code); final fallback = code == 'ko' || code == 'en' ? const {} : await _read('en'); setTranslations(code, dict, fallback); - if (save && _app != null && _c.settings.uiLanguage != want) { - await _c.updateSettings((x) => x.uiLanguage = want); + if (save && _app != null && _c.settings.display.language != want) { + await _c.updateSettings((x) => x.display.language = want); } rebuildAll(); notifyListeners(); @@ -179,7 +179,7 @@ class I18nController extends ChangeNotifier { await Directory(_dir).create(recursive: true); await File(p.join(_dir, '${l.code}.json')) .writeAsString(const JsonEncoder.withIndent(' ').convert(dict)); - await _c.updateSettings((x) => x.uiLanguagesAdded = [...x.uiLanguagesAdded, l.code]); + await _c.updateSettings((x) => x.display.languagesAdded = [...x.display.languagesAdded, l.code]); } finally { await translator.dispose(); } @@ -192,7 +192,7 @@ class I18nController extends ChangeNotifier { try { await File(p.join(_dir, '$code.json')).delete(); } catch (_) {} - await _c.updateSettings((x) => x.uiLanguagesAdded = [...x.uiLanguagesAdded]..remove(code)); + await _c.updateSettings((x) => x.display.languagesAdded = [...x.display.languagesAdded]..remove(code)); if (uiLanguage == code) { await apply('ko'); } else { diff --git a/lib/app/settings.dart b/lib/app/settings.dart index 62515b4..2145e23 100644 --- a/lib/app/settings.dart +++ b/lib/app/settings.dart @@ -2,14 +2,13 @@ import 'dart:io'; import 'package:path/path.dart' as p; -import 'components.dart'; import '../core/reader_sources.dart' show defaultImageExtensions; import '../core/download_detect.dart'; import '../core/encode_options.dart'; import '../core/playlist.dart'; -import '../services/app_shell.dart' show appIconOf; import 'settings/records.dart'; import 'settings/settings_group.dart'; +import 'settings/display_settings.dart'; import 'settings/remote_settings.dart'; import 'settings/explorer_tool_settings.dart'; import 'settings/vault_settings.dart'; @@ -26,6 +25,7 @@ import 'settings/explorer_settings.dart'; export 'settings/records.dart'; export 'settings/settings_group.dart'; export 'settings/settings_store.dart'; +export 'settings/display_settings.dart'; export 'settings/remote_settings.dart'; export 'settings/explorer_tool_settings.dart'; export 'settings/vault_settings.dart'; @@ -78,44 +78,19 @@ class AppSettings { /// 동시에 만드는 MKV 수 (0 = 무제한) int maxParallelJobs = 5; - /// 메인 화면 아래 작업 기록 창 보이기 - bool showLog = true; /// 폰 세로 (좁은 화면) MKV 목록 크기: 'auto' 목록은 내용만큼 · 자세히 칸에 최소 높이 (처음 값) · 'small' · 'large' (관리자 판단 10/17) String mkvCompactList = 'auto'; - /// 처음 화면: 'home' = 지금처럼 MKV 화면 (기본), 'browser' = 웹 브라우저 ([browser.homeUrl]), 'files' = 파일 탐색기 - String startScreen = 'home'; - /// 화면 (글자 · 버튼) 크기 배율. [uiScale] 은 지금 크기 (위쪽 막대의 − · + 로 바꿈), - /// [uiScaleDefault] 는 기본 크기 (환경 설정에서 정함, 가운데 숫자를 누르면 이 크기로 돌아감) - double uiScale = 1.0; - double uiScaleDefault = 1.0; - // 18: 50 ~ 250% (사용자 결정) - static const double uiScaleMin = 0.5, uiScaleMax = 2.5; - /// X-plore 비교 5-1 ⑦ 화면 밝기 (관리자 지시 10/21): 'dark' 어둡게 (처음 값 - 예전 그대로) · 'light' 밝게 · 'system' 기기 따라. - /// 다 끝날 때까지 설정에 "시험" 표시. 모르는 값은 어둡게 - String uiBrightness = 'dark'; - static const uiBrightnessModes = ['dark', 'light', 'system']; - static double clampUiScale(num? v) => - ((v ?? 1.0).toDouble().clamp(uiScaleMin, uiScaleMax) * 20).round() / 20; /// 탐색기에서 동영상을 열었을 때 (더블클릭 · 연결 프로그램): 'play' 바로 재생 / 'add' 편집 목록에 추가 String openFileAction = 'play'; - /// Android 화면 방향: 'auto' 기기 방향 따라 (기본) / 'landscape' 가로 고정 / 'portrait' 세로 고정. - /// 값이 없거나 틀리면 자동 (제한 없는 쪽). 고정은 사용자가 환경 설정에서 고를 때만. - String screenOrientation = 'auto'; - /// 화면 언어 (ko · en · ja · zh-Hans, 또는 더한 언어) - String uiLanguage = 'system'; - /// 앱 아이콘 (appIconIds 중 하나: 'yellow' 기본 · 'black' · 'film_jj' · 'film' · 'blue') - String appIcon = 'yellow'; - /// 더한 화면 언어 (AI 로 자동 번역한 사전이 설정 폴더 l10n 에 있음) - List uiLanguagesAdded = []; /// 프로그램이 켜져 있을 때 탐색기에서 연 동영상 재생: 'same' 켜져 있는 창에서 / 'new' 새 재생 창 String openFileWindow = 'same'; @@ -186,14 +161,8 @@ class AppSettings { - /// 설치한 (켠) 컴포넌트 id (app/components.dart). 처음엔 기본으로 들어 있는 것 모두 - List components = AppComponent.defaultInstalled; - /// 화면 순서 (위쪽 이동 버튼 · 화면 가운데를 좌우로 밀어 이동): 마지막 다음은 처음으로 - List navOrder = AppComponent.defaultOrder; - /// 화면 가운데를 좌우로 밀어 다음 · 이전 화면으로 - bool swipeNav = true; /// 그림 보기 (만화 보기) 대상 확장자 (파일 탐색기에서 두 번 누르면 보기로 연다) List imageExts = [...defaultImageExtensions]; @@ -210,8 +179,6 @@ class AppSettings { // ── 102: 사용자에게 물어 정한 것 · 관리자 1차 판단 (기본값 = 그 답) ── - /// 66: 좌우로 밀 때 끝에서 처음으로 (끄면 끝에서 멈춤) - bool swipeWrap = true; /// 저장 공간 정리 창에서 미리 체크할 묶음 (작업 임시 파일 · 받다 만 다운로드) @@ -336,8 +303,11 @@ class AppSettings { /// 원격 서버 (WebDAV · SFTP · SMB · FTP) final remote = RemoteSettings(); + /// 화면 · 앱 모양 (배율 · 밝기 · 방향 · 언어 · 앱 아이콘 · 컴포넌트 · 화면 순서 · 밀기) + final display = DisplaySettings(); + /// 설정 분야들 (설정 A) - 저마다 자기 키를 읽고 쓴다. toJson · fromJson 은 이 목록을 돌며 합치기만 한다 - List get groups => [explorer, network, tor, copy, aiImage, reader, download, browser, run, vault, tools, remote]; + List get groups => [explorer, network, tor, copy, aiImage, reader, download, browser, run, vault, tools, remote, display]; /// [plainSecrets]: 안전 저장소를 쓸 수 없을 때만 (예전 파일에 있던 비밀 값을 잃지 않게) 예전처럼 파일에 둔다 Map toJson({bool plainSecrets = false}) { @@ -361,28 +331,12 @@ class AppSettings { }, 'cookieScopeV2': true, 'maxParallelJobs': maxParallelJobs, - 'showLog': showLog, 'mkvCompactList': mkvCompactList, - 'startScreen': startScreen, - 'uiScale': uiScale, - 'uiScaleDefault': uiScaleDefault, - 'uiBrightness': uiBrightness, 'openFileAction': openFileAction, - 'screenOrientation': screenOrientation, - // 기본이 가로 고정이던 때의 값은 한 번 자동으로 옮긴다 (사용자가 정한 적 없는 가로 고정) - 'orientationV2': true, - 'uiLanguage': uiLanguage, - 'appIcon': appIcon, - 'uiLanguagesAdded': uiLanguagesAdded, 'openFileWindow': openFileWindow, 'mkvExisting': mkvExisting, - 'components': components, - 'aiImageV1': true, 'robocopyLinksV1': true, - 'navOrder': navOrder, - 'swipeNav': swipeNav, 'imageExts': imageExts, - 'swipeWrap': swipeWrap, 'cleanupPrechecked': cleanupPrechecked, 'migrationNotice': migrationNotice, 'allowInnerToOuter': allowInnerToOuter, @@ -439,39 +393,13 @@ class AppSettings { ..openSubtitlesUser = j['openSubtitlesUser'] as String? ?? '' ..openSubtitlesPassword = j['openSubtitlesPassword'] as String? ?? '' ..maxParallelJobs = (j['maxParallelJobs'] as num?)?.toInt() ?? 5 - ..showLog = j['showLog'] as bool? ?? true ..mkvCompactList = const ['auto', 'small', 'large'].contains(j['mkvCompactList']) ? j['mkvCompactList'] as String : 'auto' - ..startScreen = j['startScreen'] as String? ?? 'home' - ..uiScale = clampUiScale(j['uiScale'] as num?) - ..uiScaleDefault = clampUiScale(j['uiScaleDefault'] as num?) - ..uiBrightness = uiBrightnessModes.contains(j['uiBrightness']) ? j['uiBrightness'] as String : 'dark' ..openFileAction = j['openFileAction'] == 'add' ? 'add' : 'play' - ..uiLanguage = j['uiLanguage'] as String? ?? 'ko' - ..appIcon = appIconOf(j['appIcon'] as String?) - ..uiLanguagesAdded = [for (final x in (j['uiLanguagesAdded'] as List?) ?? const []) '$x'] - ..screenOrientation = j['orientationV2'] == true && const ['landscape', 'portrait', 'auto'].contains(j['screenOrientation']) - ? j['screenOrientation'] as String - : 'auto' ..openFileWindow = j['openFileWindow'] == 'new' ? 'new' : 'same' ..mkvExisting = const ['rename', 'overwrite', 'skip'].contains(j['mkvExisting']) ? j['mkvExisting'] as String : 'ask' - ..components = j['components'] is List - ? [ - // 225: 이 판이 모르는 구성 요소 (새 판의 Tor 등) 도 지우지 않고 그대로 둔다 - 예전 판을 거쳐 다시 올려도 켠 것이 남게 (40 과 같은 원칙). - // 화면 · 버튼은 아는 것만 쓴다 (AppComponent.pages · contains) - for (final x in j['components'] as List) if (x is String && x.isNotEmpty) x, - // 123: 새 화면 "AI 그림" 은 예전 설정에도 한 번 켜서 넣는다 (끄기: 환경 설정 > 컴포넌트) - if (j['aiImageV1'] != true && !(j['components'] as List).contains('aiimage')) 'aiimage', - ] - : AppComponent.defaultInstalled - ..navOrder = j['navOrder'] is List - // 225: 모르는 화면 id 도 순서에 그대로 (그 판으로 돌아가면 사용자가 정한 순서대로) - ? [for (final x in j['navOrder'] as List) if (x is String && x.isNotEmpty) x] - : AppComponent.defaultOrder - ..swipeNav = j['swipeNav'] as bool? ?? true ..imageExts = j['imageExts'] is List ? [for (final x in j['imageExts'] as List) '$x'.toLowerCase()] : [...defaultImageExtensions] - ..swipeWrap = j['swipeWrap'] != false ..cleanupPrechecked = j['cleanupPrechecked'] is List ? [for (final x in j['cleanupPrechecked'] as List) '$x'] : ['work', 'download'] @@ -559,7 +487,7 @@ class AppSettings { } static List _migrated(Map j, AppSettings s) => [ - if (j['orientationV2'] != true && j['screenOrientation'] == 'landscape' && s.screenOrientation == 'auto') + if (j['orientationV2'] != true && j['screenOrientation'] == 'landscape' && s.display.orientation == 'auto') 'orientation', if (j['explorerV2'] != true && j['explorerLayout'] == 'dual' && s.explorer.layout == 'auto') 'explorerLayout', if (j['explorerV2'] != true && j['explorerClick'] == 'select' && s.explorer.click == 'open') 'explorerClick', @@ -580,8 +508,8 @@ class AppSettings { if (j.isNotEmpty && j['explorerRulesV1'] != true && Platform.isAndroid && j['explorerClick'] != 'select') 'explorerClickRule', if (j.isNotEmpty && j['explorerRulesV1'] != true && (j['explorerLayout'] ?? 'auto') == 'auto') 'explorerLayoutRule', - if (j.isNotEmpty && !j.containsKey('screenOrientation') && s.screenOrientation == 'auto' && Platform.isAndroid) 'orientation', - if (j.isNotEmpty && !j.containsKey('swipeNav') && s.swipeNav) 'swipeNav', + if (j.isNotEmpty && !j.containsKey('screenOrientation') && s.display.orientation == 'auto' && Platform.isAndroid) 'orientation', + if (j.isNotEmpty && !j.containsKey('swipeNav') && s.display.swipeNav) 'swipeNav', if (j.isNotEmpty && !j.containsKey('runInBackground') && s.run.inBackground && Platform.isAndroid) 'background', // DNS 강제 (사용자 요청 10/10, 처음 값 직접 지정 8.8.8.8 · 8.8.4.4) 가 없던 판에서 올라오면 이름을 묻는 서버가 바뀐다 - 193 과 같이 한 번 알림 if (j.isNotEmpty && !j.containsKey('dnsMode') && s.network.mode == 'custom') 'dnsForced', diff --git a/lib/app/settings/display_settings.dart b/lib/app/settings/display_settings.dart new file mode 100644 index 0000000..b404036 --- /dev/null +++ b/lib/app/settings/display_settings.dart @@ -0,0 +1,106 @@ + +import '../components.dart'; +import '../../services/app_shell.dart'; +import 'settings_group.dart'; + +/// 약속 (설정 A): 환경 설정 > 화면 · 앱 모양 (배율 · 밝기 · 방향 · 언어 · 앱 아이콘 · 컴포넌트 · 화면 순서 · 밀기) - 자기 키 (이름 · 처음 값 · 읽기 · 쓰기) 를 스스로 갖는다. +/// 설정 하나를 더할 때는 이 파일만 고친다. 저장 파일의 키 이름은 예전 그대로 (평평한 키) +class DisplaySettings implements SettingsGroup { + /// 메인 화면 아래 작업 기록 창 보이기 + bool showLog = true; + + /// 처음 화면: 'home' = 지금처럼 MKV 화면 (기본), 'browser' = 웹 브라우저 ([browser.homeUrl]), 'files' = 파일 탐색기 + String startScreen = 'home'; + + /// 화면 (글자 · 버튼) 크기 배율. [scale] 은 지금 크기 (위쪽 막대의 − · + 로 바꿈), + /// [scaleDefault] 는 기본 크기 (환경 설정에서 정함, 가운데 숫자를 누르면 이 크기로 돌아감) + double scale = 1.0; + + double scaleDefault = 1.0; + + /// X-plore 비교 5-1 ⑦ 화면 밝기 (관리자 지시 10/21): 'dark' 어둡게 (처음 값 - 예전 그대로) · 'light' 밝게 · 'system' 기기 따라. + /// 다 끝날 때까지 설정에 "시험" 표시. 모르는 값은 어둡게 + String brightness = 'dark'; + static const brightnessModes = ['dark', 'light', 'system']; + + /// Android 화면 방향: 'auto' 기기 방향 따라 (기본) / 'landscape' 가로 고정 / 'portrait' 세로 고정. + /// 값이 없거나 틀리면 자동 (제한 없는 쪽). 고정은 사용자가 환경 설정에서 고를 때만. + String orientation = 'auto'; + + /// 화면 언어 (ko · en · ja · zh-Hans, 또는 더한 언어) + String language = 'system'; + + /// 앱 아이콘 (appIconIds 중 하나: 'yellow' 기본 · 'black' · 'film_jj' · 'film' · 'blue') + String appIcon = 'yellow'; + + /// 더한 화면 언어 (AI 로 자동 번역한 사전이 설정 폴더 l10n 에 있음) + List languagesAdded = []; + + /// 설치한 (켠) 컴포넌트 id (app/components.dart). 처음엔 기본으로 들어 있는 것 모두 + List components = AppComponent.defaultInstalled; + + /// 화면 순서 (위쪽 이동 버튼 · 화면 가운데를 좌우로 밀어 이동): 마지막 다음은 처음으로 + List navOrder = AppComponent.defaultOrder; + + /// 화면 가운데를 좌우로 밀어 다음 · 이전 화면으로 + bool swipeNav = true; + + /// 66: 좌우로 밀 때 끝에서 처음으로 (끄면 끝에서 멈춤) + bool swipeWrap = true; + + // 18: 50 ~ 250% (사용자 결정) + static const double scaleMin = 0.5, scaleMax = 2.5; + + static double clampScale(num? v) => + ((v ?? 1.0).toDouble().clamp(scaleMin, scaleMax) * 20).round() / 20; + + @override + void write(Map j, {required bool plainSecrets}) { + j['showLog'] = showLog; + j['startScreen'] = startScreen; + j['uiScale'] = scale; + j['uiScaleDefault'] = scaleDefault; + j['uiBrightness'] = brightness; + j['screenOrientation'] = orientation; + j['uiLanguage'] = language; + j['appIcon'] = appIcon; + j['uiLanguagesAdded'] = languagesAdded; + j['components'] = components; + j['navOrder'] = navOrder; + j['swipeNav'] = swipeNav; + j['swipeWrap'] = swipeWrap; + // 기본이 가로 고정이던 때의 값은 한 번 자동으로 옮긴다 (사용자가 정한 적 없는 가로 고정) + j['orientationV2'] = true; + j['aiImageV1'] = true; + } + + @override + void read(Map j) { + showLog = j['showLog'] as bool? ?? true; + startScreen = j['startScreen'] as String? ?? 'home'; + scale = clampScale(j['uiScale'] as num?); + scaleDefault = clampScale(j['uiScaleDefault'] as num?); + brightness = brightnessModes.contains(j['uiBrightness']) ? j['uiBrightness'] as String : 'dark'; + language = j['uiLanguage'] as String? ?? 'ko'; + appIcon = appIconOf(j['appIcon'] as String?); + languagesAdded = [for (final x in (j['uiLanguagesAdded'] as List?) ?? const []) '$x']; + orientation = j['orientationV2'] == true && const ['landscape', 'portrait', 'auto'].contains(j['screenOrientation']) + ? j['screenOrientation'] as String + : 'auto'; + components = j['components'] is List + ? [ + // 225: 이 판이 모르는 구성 요소 (새 판의 Tor 등) 도 지우지 않고 그대로 둔다 - 예전 판을 거쳐 다시 올려도 켠 것이 남게 (40 과 같은 원칙). + // 화면 · 버튼은 아는 것만 쓴다 (AppComponent.pages · contains) + for (final x in j['components'] as List) if (x is String && x.isNotEmpty) x, + // 123: 새 화면 "AI 그림" 은 예전 설정에도 한 번 켜서 넣는다 (끄기: 환경 설정 > 컴포넌트) + if (j['aiImageV1'] != true && !(j['components'] as List).contains('aiimage')) 'aiimage', + ] + : AppComponent.defaultInstalled; + navOrder = j['navOrder'] is List + // 225: 모르는 화면 id 도 순서에 그대로 (그 판으로 돌아가면 사용자가 정한 순서대로) + ? [for (final x in j['navOrder'] as List) if (x is String && x.isNotEmpty) x] + : AppComponent.defaultOrder; + swipeNav = j['swipeNav'] as bool? ?? true; + swipeWrap = j['swipeWrap'] != false; + } +} diff --git a/lib/main.dart b/lib/main.dart index 2c9dfe1..4f06855 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -105,7 +105,7 @@ Future main(List args) async { final claimed = await instance.claim(request, waitForExit: args.contains('--restart'), forward: !newWindow); if (claimed == ClaimResult.unresponsive) { // 같은 데이터 위에 창이 두 개 뜨지 않게 묻는다 (처음 고른 것은 [닫기] - 관리자 결정 10/12) - await i18n.apply(startSettings.uiLanguage, save: false); + await i18n.apply(startSettings.display.language, save: false); if (askOpenAnyway(appTitle)) { await instance.takeOverAnyway(request); } else { @@ -151,7 +151,7 @@ Future main(List args) async { final live = LiveSync(controller)..start(hold: controller.settings.copy.liveSyncOnStart != 'auto'); // 화면 언어 (환경 설정 > 화면 언어) i18n.init(controller, dataDir); - await i18n.apply(controller.settings.uiLanguage, save: false); + await i18n.apply(controller.settings.display.language, save: false); final bookmarks = BookmarksController(); await bookmarks.load(); @@ -259,7 +259,7 @@ Future main(List args) async { if (nav != null) unawaited(DownloadsPage.open(nav, downloads)); }, ); - await services.shell.setAppIcon(controller.settings.appIcon); + await services.shell.setAppIcon(controller.settings.display.appIcon); // 99: 시작 메뉴 바로 가기 (앱 ID) · jjmkvmaker:// 연결 - 알림이 JJ_MKVMaker 로 보이고 눌러서 열린다 (설정에서 끔) StartMenu.setProcessAppId(); final shell0 = services.shell; @@ -598,9 +598,9 @@ Future runAndroid(String dataDir) async { final live = LiveSync(controller)..start(hold: controller.settings.copy.liveSyncOnStart != 'auto'); // 화면 언어 (환경 설정 > 화면 언어) i18n.init(controller, dataDir); - await i18n.apply(controller.settings.uiLanguage, save: false); - await applyScreenOrientation(controller.settings.screenOrientation); - unawaited(services.shell.setAppIcon(controller.settings.appIcon)); + await i18n.apply(controller.settings.display.language, save: false); + await applyScreenOrientation(controller.settings.display.orientation); + unawaited(services.shell.setAppIcon(controller.settings.display.appIcon)); final bookmarks = BookmarksController(); await bookmarks.load(); final navigatorKey = GlobalKey(); @@ -649,7 +649,7 @@ Future runAndroid(String dataDir) async { var hadAccess = await AndroidAccess.hasAllFiles(); if (hadAccess) await allFilesGuideDue(controller, hasAccess: true); AppLifecycleListener(onResume: () async { - await applyScreenOrientation(controller.settings.screenOrientation); + await applyScreenOrientation(controller.settings.display.orientation); final now = await AndroidAccess.hasAllFiles(); if (now && !hadAccess) await controller.reanalyzeFailed(); if (now) await allFilesGuideDue(controller, hasAccess: true); @@ -772,7 +772,7 @@ Future runSecondWindow(List files, String dataDir) async { await setupNetwork(controller, dataDir, second: true); // 화면 언어 (환경 설정 > 화면 언어) i18n.init(controller, dataDir); - await i18n.apply(controller.settings.uiLanguage, save: false); + await i18n.apply(controller.settings.display.language, save: false); final store = SettingsStore(); controller.settings = await store.load(); final navigatorKey = GlobalKey(); @@ -893,7 +893,7 @@ class _BrightnessHostState extends State<_BrightnessHost> with WidgetsBindingObs } Brightness _want() => - resolveBrightness(widget.c.settings.uiBrightness, WidgetsBinding.instance.platformDispatcher.platformBrightness); + resolveBrightness(widget.c.settings.display.brightness, WidgetsBinding.instance.platformDispatcher.platformBrightness); @override void didChangePlatformBrightness() => _check(); diff --git a/lib/ui/app_actions.dart b/lib/ui/app_actions.dart index 48fc786..ec10c3f 100644 --- a/lib/ui/app_actions.dart +++ b/lib/ui/app_actions.dart @@ -203,8 +203,8 @@ class AppNavButtons extends StatelessWidget { /// 홈 화면 (환경 설정의 "홈 화면") 의 컴포넌트 id. 그 컴포넌트를 제거했으면 설치된 첫 화면. static String homeId(AppSettings s) { - final want = switch (s.startScreen) { 'browser' => 'browser', 'files' => 'explorer', 'tor' => 'tor', _ => 'mkv' }; - final pages = AppComponent.pages(s.components, s.navOrder); + final want = switch (s.display.startScreen) { 'browser' => 'browser', 'files' => 'explorer', 'tor' => 'tor', _ => 'mkv' }; + final pages = AppComponent.pages(s.display.components, s.display.navOrder); if (pages.any((p) => p.id == want)) return want; return pages.isEmpty ? 'mkv' : pages.first.id; } @@ -232,7 +232,7 @@ class AppNavButtons extends StatelessWidget { /// 이 창에서 열 수 있는 화면 (브라우저 · 다운로드가 없는 새 창에서는 그 둘을 뺀다) static List pagesOf(AppScope scope) => [ - for (final p in AppComponent.pages(scope.controller.settings.components, scope.controller.settings.navOrder)) + for (final p in AppComponent.pages(scope.controller.settings.display.components, scope.controller.settings.display.navOrder)) if ((p.id != 'browser' || scope.bookmarks != null) && (p.id != 'downloads' || scope.downloads != null)) p, ]; @@ -263,7 +263,7 @@ class AppNavButtons extends StatelessWidget { final j = i + step; // 102 · 66: 끝에서 처음으로 돌기를 끄면 끝에서 멈춘다 final wraps = j < 0 || j >= pages.length; - if (!scope.controller.settings.swipeWrap && wraps) { + if (!scope.controller.settings.display.swipeWrap && wraps) { _hint(context, step > 0 ? tr('마지막 화면입니다') : tr('첫 화면입니다')); return; } @@ -409,7 +409,7 @@ class AppNavButtons extends StatelessWidget { }; return Row(mainAxisSize: MainAxisSize.min, children: [ btn( - Image.asset(appIconButtonAsset(scope?.controller.settings.appIcon), + Image.asset(appIconButtonAsset(scope?.controller.settings.display.appIcon), width: labels ? 22 : 26, height: labels ? 22 : 26, filterQuality: FilterQuality.medium), trf('홈 화면 ({0}) · 환경 설정에서 바꿈', [homeName]), scope == null ? null : () => toHome(context), @@ -463,7 +463,7 @@ class _SwipeNavState extends State { onHorizontalDragEnd: (d) { final v = d.primaryVelocity ?? 0; // 켜고 끄기는 밀 때 확인 (이 위젯은 설정이 바뀌어도 다시 그려지지 않을 수 있음) - if (!scope.controller.settings.swipeNav || !_middle || v.abs() < 500) return; + if (!scope.controller.settings.display.swipeNav || !_middle || v.abs() < 500) return; // 왼쪽으로 밀면 다음 화면, 오른쪽으로 밀면 이전 화면 AppNavButtons.step(context, widget.current, v < 0 ? 1 : -1); }, @@ -536,13 +536,13 @@ class ScaleButtons extends StatelessWidget { final AppController c; const ScaleButtons({super.key, required this.c}); - void _set(double v) => c.updateSettings((s) => s.uiScale = AppSettings.clampUiScale(v)); + void _set(double v) => c.updateSettings((s) => s.display.scale = DisplaySettings.clampScale(v)); @override Widget build(BuildContext context) => ListenableBuilder( listenable: c, builder: (context, _) { - final s = c.settings.uiScale; + final s = c.settings.display.scale; Widget btn(IconData icon, String tip, VoidCallback? f) => SizedBox( width: 26, height: 32, @@ -557,12 +557,12 @@ class ScaleButtons extends StatelessWidget { return Padding( padding: const EdgeInsets.only(left: 6), child: Row(mainAxisSize: MainAxisSize.min, children: [ - btn(Icons.remove, tr('화면 작게'), s <= AppSettings.uiScaleMin + 0.001 ? null : () => _set(s - 0.1)), + btn(Icons.remove, tr('화면 작게'), s <= DisplaySettings.scaleMin + 0.001 ? null : () => _set(s - 0.1)), Tooltip( - message: trf('기본 크기 ({0}%) 로 · 기본 크기는 환경 설정 > 화면 에서', [(c.settings.uiScaleDefault * 100).round()]), + message: trf('기본 크기 ({0}%) 로 · 기본 크기는 환경 설정 > 화면 에서', [(c.settings.display.scaleDefault * 100).round()]), child: InkWell( borderRadius: BorderRadius.circular(4), - onTap: () => _set(c.settings.uiScaleDefault), + onTap: () => _set(c.settings.display.scaleDefault), child: SizedBox( width: 40, height: 28, @@ -573,14 +573,14 @@ class ScaleButtons extends StatelessWidget { ), ), ), - btn(Icons.add, tr('화면 크게'), s >= AppSettings.uiScaleMax - 0.001 ? null : () => _set(s + 0.1)), + btn(Icons.add, tr('화면 크게'), s >= DisplaySettings.scaleMax - 0.001 ? null : () => _set(s + 0.1)), ]), ); }, ); } -/// 화면 전체를 [AppSettings.uiScale] 배로 그린다 (안쪽은 그만큼 작은 · 큰 화면이라고 여기고 배치). +/// 화면 전체를 [AppSettings.display.scale] 배로 그린다 (안쪽은 그만큼 작은 · 큰 화면이라고 여기고 배치). /// 앱의 builder 에서 한 번 감싼다 - 대화 상자 · 메뉴 · 안내 글까지 모두 같은 배율. class UiScaler extends StatelessWidget { final AppController c; @@ -591,7 +591,7 @@ class UiScaler extends StatelessWidget { Widget build(BuildContext context) => ListenableBuilder( listenable: c, builder: (context, _) { - final s = c.settings.uiScale; + final s = c.settings.display.scale; if ((s - 1).abs() < 0.001) return child; return LayoutBuilder(builder: (context, box) { final size = box.biggest / s; @@ -655,11 +655,11 @@ class AppActions extends StatelessWidget { final s = controller?.settings; switch (v) { case 'bigger' when s != null: - controller!.updateSettings((x) => x.uiScale = AppSettings.clampUiScale(s.uiScale + 0.1)); + controller!.updateSettings((x) => x.display.scale = DisplaySettings.clampScale(s.display.scale + 0.1)); case 'smaller' when s != null: - controller!.updateSettings((x) => x.uiScale = AppSettings.clampUiScale(s.uiScale - 0.1)); + controller!.updateSettings((x) => x.display.scale = DisplaySettings.clampScale(s.display.scale - 0.1)); case 'default' when s != null: - controller!.updateSettings((x) => x.uiScale = s.uiScaleDefault); + controller!.updateSettings((x) => x.display.scale = s.display.scaleDefault); case 'exit': exit?.call(); } @@ -676,8 +676,8 @@ class AppActions extends StatelessWidget { value: 'default', child: ListTile( leading: const Icon(Icons.fit_screen_outlined), - title: Text(trf('기본 크기 ({0}%)', [(controller.settings.uiScaleDefault * 100).round()])), - subtitle: Text(trf('지금 {0}%', [(controller.settings.uiScale * 100).round()])), + title: Text(trf('기본 크기 ({0}%)', [(controller.settings.display.scaleDefault * 100).round()])), + subtitle: Text(trf('지금 {0}%', [(controller.settings.display.scale * 100).round()])), dense: true)), ], if (exit != null) ...[ diff --git a/lib/ui/browser_page.dart b/lib/ui/browser_page.dart index 6af82ac..2fc8e42 100644 --- a/lib/ui/browser_page.dart +++ b/lib/ui/browser_page.dart @@ -381,10 +381,10 @@ class _BrowserPageState extends State with RouteAware { if (s.browser.translate != _lastTranslate) { _lastTranslate = s.browser.translate; _setTranslate(_lastTranslate, force: false); - } else if (s.uiLanguage != _lastLang && _trOn) { + } else if (s.display.language != _lastLang && _trOn) { _translateNow(); // 다른 언어로 다시 번역 (스크립트가 앞의 번역을 되돌린 뒤 번역) } - _lastLang = s.uiLanguage; + _lastLang = s.display.language; } void _setTranslate(bool on, {required bool force}) { @@ -450,13 +450,13 @@ class _BrowserPageState extends State with RouteAware { final s = widget.c.settings; _lastTranslate = s.browser.translate; _lastJs = s.browser.javaScript; - _lastLang = s.uiLanguage; + _lastLang = s.display.language; _applyCookieExport(); widget.c.addListener(_onSettings); if (_trOn) _translateNow(); // 66-2: 브라우저 가운데는 페이지가 좌우로 밀리므로 화면 옮기기는 위쪽 막대에서 - 처음 한 번만 알린다. // 알림 (SnackBar) 은 다른 알림 (다운로드 등) 을 뒤로 밀어 기다리게 하므로 위쪽 막대 아래의 한 줄로 - if (s.swipeNav && !s.browser.swipeHinted) { + if (s.display.swipeNav && !s.browser.swipeHinted) { _swipeHint = true; WidgetsBinding.instance.addPostFrameCallback((_) => widget.c.updateSettings((x) => x.browser.swipeHinted = true)); } @@ -520,7 +520,7 @@ class _BrowserPageState extends State with RouteAware { /// Tor 브라우저로: 켜져 있으면 Tor 화면, 아니면 짧은 안내와 [환경 설정 > 컴포넌트] Future _openTor() async { - if (widget.c.settings.components.contains('tor')) { + if (widget.c.settings.display.components.contains('tor')) { await TorPage.open(Navigator.of(context), c: widget.c); return; } @@ -883,7 +883,7 @@ class _BrowserPageState extends State with RouteAware { Icons.translate, _trOn ? tr('원문 보기 (번역 끄기)') - : trf('이 페이지를 {0}(으)로 번역', [I18nController.nativeName(widget.c.settings.uiLanguage)]), + : trf('이 페이지를 {0}(으)로 번역', [I18nController.nativeName(widget.c.settings.display.language)]), () => _setTranslate(!_trOn, force: true), color: _trOn ? JjColors.accent : null), const SizedBox(width: 4), diff --git a/lib/ui/component_settings.dart b/lib/ui/component_settings.dart index 4960739..35d3c19 100644 --- a/lib/ui/component_settings.dart +++ b/lib/ui/component_settings.dart @@ -21,7 +21,7 @@ class ViewerSettings extends StatelessWidget { @override Widget build(BuildContext context) { final s = c.settings; - if (!s.components.contains('viewer')) return const SizedBox(); + if (!s.display.components.contains('viewer')) return const SizedBox(); return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ SettingTile( leading: const Icon(Icons.photo_library_outlined), @@ -115,7 +115,7 @@ Future _setDownloaded(BuildContext context, AppComponent x, bool on) async ); if (ok != true) return; await store.remove(x.id); - await c.updateSettings((v) => v.components = [for (final id in v.components) if (id != x.id) id]); + await c.updateSettings((v) => v.display.components = [for (final id in v.display.components) if (id != x.id) id]); return; } final step = ValueNotifier<(String, double?)>((tr('목록을 받는 중'), null)); @@ -157,7 +157,7 @@ Future _setDownloaded(BuildContext context, AppComponent x, bool on) async await dialog; step.dispose(); if (done) { - await c.updateSettings((v) => v.components = [for (final id in v.components) if (id != x.id) id, x.id]); + await c.updateSettings((v) => v.display.components = [for (final id in v.display.components) if (id != x.id) id, x.id]); } if (context.mounted) { ScaffoldMessenger.maybeOf(context)?.showSnackBar(SnackBar( @@ -174,8 +174,8 @@ class ComponentSettings extends StatelessWidget { @override Widget build(BuildContext context) { final s = c.settings; - final installed = s.components; - final pages = AppComponent.pages(installed, s.navOrder); + final installed = s.display.components; + final pages = AppComponent.pages(installed, s.display.navOrder); Future setInstalled(AppComponent x, bool on) async { if (x.needsDownload) return _setDownloaded(context, x, on); @@ -186,8 +186,8 @@ class ComponentSettings extends StatelessWidget { ?.showSnackBar(SnackBar(content: Text(tr('화면이 있는 컴포넌트는 하나 이상 남아 있어야 합니다.')))); return; } - await c.updateSettings((v) => v.components = [ - for (final id in v.components) if (id != x.id) id, + await c.updateSettings((v) => v.display.components = [ + for (final id in v.display.components) if (id != x.id) id, if (on) x.id, ]); } @@ -198,21 +198,21 @@ class ComponentSettings extends StatelessWidget { ids.insert(to, ids.removeAt(from)); // 아직 켜지 않은 Tor 는 순서에 넣지 않는다 (켜면 웹 브라우저 바로 뒤에 - AppComponent.fullOrder). // 225: 이 판이 모르는 화면 (다른 판의 것) 은 지우지 않고 그 자리에 - c.updateSettings((v) => v.navOrder = AppComponent.reorder(v.navOrder, + c.updateSettings((v) => v.display.navOrder = AppComponent.reorder(v.display.navOrder, [...ids, ...AppComponent.defaultOrder.where((x) => !ids.contains(x) && x != AppComponent.tor.id)])); } return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ SwitchListTile( - value: s.swipeNav, - onChanged: (v) => c.updateSettings((x) => x.swipeNav = v), + value: s.display.swipeNav, + onChanged: (v) => c.updateSettings((x) => x.display.swipeNav = v), title: Text(tr('좌우로 밀어 화면 이동')), subtitle: Text(tr('화면 가운데를 왼쪽으로 밀면 다음 화면, 오른쪽으로 밀면 이전 화면 (마지막 다음은 처음으로).')), ), // 102 · 66: 끝에서 처음으로 돌기 (기본 켜짐) SwitchListTile( - value: s.swipeWrap, - onChanged: s.swipeNav ? (v) => c.updateSettings((x) => x.swipeWrap = v) : null, + value: s.display.swipeWrap, + onChanged: s.display.swipeNav ? (v) => c.updateSettings((x) => x.display.swipeWrap = v) : null, title: Text(tr('끝에서 처음으로 돌기')), subtitle: Text(tr('마지막 화면에서 더 밀면 첫 화면으로 (끄면 끝에서 멈춤)')), ), @@ -302,7 +302,7 @@ Future _setTor(BuildContext context, AppController c, bool on) async { return; } } - await c.updateSettings((v) => v.components = [for (final id in v.components) if (id != TorBundle.id) id]); + await c.updateSettings((v) => v.display.components = [for (final id in v.display.components) if (id != TorBundle.id) id]); return; } if (Platform.isAndroid) { @@ -353,7 +353,7 @@ Future _setTor(BuildContext context, AppController c, bool on) async { return; } } - await c.updateSettings((v) => v.components = [for (final id in v.components) if (id != TorBundle.id) id, TorBundle.id]); + await c.updateSettings((v) => v.display.components = [for (final id in v.display.components) if (id != TorBundle.id) id, TorBundle.id]); if (context.mounted) { ScaffoldMessenger.maybeOf(context)?.showSnackBar(SnackBar(content: Text(trf('{0} 을(를) 설치했습니다.', [name])))); } diff --git a/lib/ui/explorer/item_menu.dart b/lib/ui/explorer/item_menu.dart index 3def05b..a4c5531 100644 --- a/lib/ui/explorer/item_menu.dart +++ b/lib/ui/explorer/item_menu.dart @@ -50,7 +50,7 @@ class ExplorerItemMenu { final audio = !e.isDir && isAudioFile(e.path); final dav = isDav(e.path); // 그림 → PDF: 표시한 것 중 그림 (누른 것이 표시에 없으면 누른 것만) - final viewer = ctl.c.settings.components.contains('viewer'); + final viewer = ctl.c.settings.display.components.contains('viewer'); final picked = pane.sel.has(e.path) ? pane.sel.marked : [e.path]; final images = [for (final x in picked) if (ctl.c.settings.imageExts.contains(extOf(x))) x]; final ext = extOf(e.path); diff --git a/lib/ui/explorer/open_flow.dart b/lib/ui/explorer/open_flow.dart index cabf2b1..d1db6b3 100644 --- a/lib/ui/explorer/open_flow.dart +++ b/lib/ui/explorer/open_flow.dart @@ -57,7 +57,7 @@ class ExplorerOpener { } // 여는 길은 file_opener (E5c) 가 고른다 switch (openRouteFor(path, - components: ctl.c.settings.components, + components: ctl.c.settings.display.components, imageExts: ctl.c.settings.imageExts, android: Platform.isAndroid, windows: Platform.isWindows, diff --git a/lib/ui/home_page.dart b/lib/ui/home_page.dart index 1c69348..0560584 100644 --- a/lib/ui/home_page.dart +++ b/lib/ui/home_page.dart @@ -95,7 +95,7 @@ class HomePage extends StatelessWidget { const Divider(height: 1), // 249: DNS 서버에 닿지 않는 동안 작업 기록 위에 (아래 안내를 놓쳐도 MKV 화면에서 보고 바꿀 수 있게) Padding(padding: const EdgeInsets.symmetric(horizontal: 12), child: DnsFailureLine(c: c, compact: isCompact(context))), - if (c.settings.showLog) + if (c.settings.display.showLog) SizedBox( // 화면이 낮은 기기 (휴대폰 가로) 에서 목록 · 자세히 보기를 가리지 않도록 화면 높이에 맞춘다. // 256: 폰 가로 (높이 500 미만) 는 더 작게 - 목록이 한 줄만 보였다 @@ -107,13 +107,13 @@ class HomePage extends StatelessWidget { : (MediaQuery.sizeOf(context).height * 0.22).clamp(80.0, 140.0), child: _LogPanel( logs: c.logs, - onClose: () => c.updateSettings((s) => s.showLog = false), + onClose: () => c.updateSettings((s) => s.display.showLog = false), ), ) else _LogStrip( last: c.logs.isEmpty ? '' : c.logs.last, - onOpen: () => c.updateSettings((s) => s.showLog = true), + onOpen: () => c.updateSettings((s) => s.display.showLog = true), ), ], ), diff --git a/lib/ui/reader/reader_ai.dart b/lib/ui/reader/reader_ai.dart index c03745b..5f5b41b 100644 --- a/lib/ui/reader/reader_ai.dart +++ b/lib/ui/reader/reader_ai.dart @@ -35,7 +35,7 @@ Future qualityReaderPage(BuildContext context, AppController c, ReaderSour /// 지금 보는 그림 파일 (이 기기의 그림 파일일 때, AI 그림 컴포넌트가 켜져 있을 때). [hasPages] 볼 장이 있는지 String? readerAiSource(AppController c, ReaderSource src, int page, {required bool hasPages}) { final s = src; - if (!c.settings.components.contains('aiimage') || s is! ImageFilesSource || !hasPages) return null; + if (!c.settings.display.components.contains('aiimage') || s is! ImageFilesSource || !hasPages) return null; final path = s.paths[page]; return isDav(path) ? null : path; } diff --git a/lib/ui/reader/reader_bar_items.dart b/lib/ui/reader/reader_bar_items.dart index c297afd..695455e 100644 --- a/lib/ui/reader/reader_bar_items.dart +++ b/lib/ui/reader/reader_bar_items.dart @@ -63,7 +63,7 @@ List readerBarItems(BuildContext context, AppController c, Reader final s = c.settings; final pdf = src is PdfSource ? src : null; final pictures = src is ImageFilesSource || src is ZipImagesSource; - final aiOn = c.settings.components.contains('aiimage'); + final aiOn = c.settings.display.components.contains('aiimage'); final playing = st.playing; final secs = s.reader.autoSeconds; ReaderBarItem one(ReaderAct a) => ReaderBarItem(48, [a]); diff --git a/lib/ui/settings/browser_section.dart b/lib/ui/settings/browser_section.dart index 3bffe6f..0ce5ee5 100644 --- a/lib/ui/settings/browser_section.dart +++ b/lib/ui/settings/browser_section.dart @@ -59,7 +59,7 @@ class _BrowserSettingsSectionState extends State { subtitle: Text(trf( '다른 언어로 된 페이지를 화면 언어 ({0}) 로 번역합니다. 페이지의 글을 Google 번역으로 보냅니다. ' '주소창 옆 번역 버튼으로 원문 · 번역을 바꿀 수 있습니다.', - [I18nController.nativeName(s.uiLanguage)])), + [I18nController.nativeName(s.display.language)])), ), SwitchListTile( value: s.browser.javaScript, diff --git a/lib/ui/settings/general_sections.dart b/lib/ui/settings/general_sections.dart index b7e5345..0af2da1 100644 --- a/lib/ui/settings/general_sections.dart +++ b/lib/ui/settings/general_sections.dart @@ -77,15 +77,15 @@ class _GeneralSettingsSectionState extends State { title: Text(tr('홈 화면')), subtitle: Text(tr('프로그램을 켰을 때, 그리고 위쪽 왼쪽 JJ 아이콘을 눌렀을 때 보일 화면')), trailing: DropdownButton( - value: s.startScreen, + value: s.display.startScreen, items: [ DropdownMenuItem(value: 'home', child: Text(tr('MKV 화면 (기본)'))), DropdownMenuItem(value: 'browser', child: Text(tr('웹 브라우저 (홈 주소)'))), DropdownMenuItem(value: 'files', child: Text(tr('파일 탐색기'))), - if (s.components.contains('tor') || s.startScreen == 'tor') + if (s.display.components.contains('tor') || s.display.startScreen == 'tor') DropdownMenuItem(value: 'tor', child: Text(tr('Tor 브라우저'))), ], - onChanged: (v) => c.updateSettings((x) => x.startScreen = v!), + onChanged: (v) => c.updateSettings((x) => x.display.startScreen = v!), ), ), ]); @@ -118,37 +118,37 @@ class _DisplaySettingsSectionState extends State { title: Text(tr('화면 방향')), subtitle: Text(tr('가로 고정 · 세로 고정, 또는 자동 (기기를 돌리는 대로, 기기의 자동 회전 설정을 따름)')), trailing: DropdownButton( - value: s.screenOrientation, + value: s.display.orientation, items: [ DropdownMenuItem(value: 'auto', child: Text(tr('자동 (기본)'))), DropdownMenuItem(value: 'landscape', child: Text(tr('가로 고정'))), DropdownMenuItem(value: 'portrait', child: Text(tr('세로 고정'))), ], onChanged: (v) { - c.updateSettings((x) => x.screenOrientation = v!); + c.updateSettings((x) => x.display.orientation = v!); applyScreenOrientation(v!); }, ), ), SettingTile( title: Text(tr('기본 화면 크기')), - subtitle: Text(trf('글자 · 버튼 크기입니다. 위쪽 막대의 − · + 로 그때그때 바꿀 수 있고, 가운데 숫자를 누르면 이 크기로 돌아갑니다 ' '(지금 {0}%)', [(s.uiScale * 100).round()])), + subtitle: Text(trf('글자 · 버튼 크기입니다. 위쪽 막대의 − · + 로 그때그때 바꿀 수 있고, 가운데 숫자를 누르면 이 크기로 돌아갑니다 ' '(지금 {0}%)', [(s.display.scale * 100).round()])), trailing: DropdownButton( - value: s.uiScaleDefault, + value: s.display.scaleDefault, items: [ - for (var v = AppSettings.uiScaleMin; v <= AppSettings.uiScaleMax + 0.001; v += 0.1) + for (var v = DisplaySettings.scaleMin; v <= DisplaySettings.scaleMax + 0.001; v += 0.1) DropdownMenuItem( - value: AppSettings.clampUiScale(v), + value: DisplaySettings.clampScale(v), child: Text('${(v * 100).round()}%${(v - 1).abs() < 0.001 ? tr(' (보통)') : ''}')), // 5% 단위로 저장된 값이 목록에 없을 때 - if (((s.uiScaleDefault * 100).round() % 10) != 0) + if (((s.display.scaleDefault * 100).round() % 10) != 0) DropdownMenuItem( - value: s.uiScaleDefault, child: Text('${(s.uiScaleDefault * 100).round()}%')), + value: s.display.scaleDefault, child: Text('${(s.display.scaleDefault * 100).round()}%')), ], // 기본 크기를 바꾸면 지금 크기도 그 크기로 onChanged: (v) => c.updateSettings((x) => x - ..uiScaleDefault = v! - ..uiScale = v), + ..display.scaleDefault = v! + ..display.scale = v), ), ), // X-plore 비교 5-1 ⑦ (관리자 지시 10/21): 화면 밝기 - 처음 값 어둡게 (예전 그대로), 다 끝날 때까지 "시험" @@ -164,13 +164,13 @@ class _DisplaySettingsSectionState extends State { ]), subtitle: Text(tr('밝은 화면 · 기기 따라는 아직 시험 중입니다 - 일부 화면의 색이 덜 맞을 수 있습니다')), trailing: DropdownButton( - value: s.uiBrightness, + value: s.display.brightness, items: [ DropdownMenuItem(value: 'dark', child: Text(tr('어두운 화면'))), DropdownMenuItem(value: 'light', child: Text(tr('밝은 화면'))), DropdownMenuItem(value: 'system', child: Text(tr('기기 따라'))), ], - onChanged: (v) => c.updateSettings((x) => x.uiBrightness = v!), + onChanged: (v) => c.updateSettings((x) => x.display.brightness = v!), ), ), ]); @@ -186,7 +186,7 @@ class AppIconTile extends StatelessWidget { @override Widget build(BuildContext context) { - final now = appIconOf(c.settings.appIcon); + final now = appIconOf(c.settings.display.appIcon); return SettingTile( title: Text(tr('앱 아이콘')), subtitle: Padding( @@ -205,7 +205,7 @@ class AppIconTile extends StatelessWidget { onTap: id == now ? null : () { - c.updateSettings((x) => x.appIcon = id); + c.updateSettings((x) => x.display.appIcon = id); c.services.shell.setAppIcon(id); }, child: Container( diff --git a/lib/ui/settings_page.dart b/lib/ui/settings_page.dart index 7a0c873..dd9f9a2 100644 --- a/lib/ui/settings_page.dart +++ b/lib/ui/settings_page.dart @@ -129,12 +129,12 @@ class _SettingsPageState extends State { _group('mkv', Icons.movie_creation_outlined, tr('MKV 만들기'), [MkvSettingsSection(c: c, desk: desk)]), _group('subtitle', Icons.subtitles_outlined, tr('자막 (AI · 인터넷)'), [SubtitleSettingsSection(c: c, desk: desk)]), // 123 · 121: 모델 받기 · 처리 장치 · 서버 · 서비스 - if (s.components.contains('aiimage')) + if (s.display.components.contains('aiimage')) _group('ai', Icons.auto_awesome_outlined, tr('AI 그림'), [AiSettings(c: c)]), _group('play', Icons.play_circle_outline, tr('재생'), [PlaySettingsSection(c: c, desk: desk)]), _group('browser', Icons.public, tr('웹 브라우저'), [BrowserSettingsSection(c: c, desk: desk)]), // 사용자 요청 10/10: Tor 브라우저 · DNS 강제 - if (s.components.contains('tor')) + if (s.display.components.contains('tor')) _group('tor', Icons.vpn_lock_outlined, tr('Tor 브라우저'), [TorSettings(c: c)]), _group('network', Icons.dns_outlined, tr('네트워크 · DNS'), [DnsSettings(c: c)]), _group('files', Icons.folder_copy_outlined, tr('파일 탐색기'), [FilesSettingsSection(c: c, desk: desk)]), @@ -187,8 +187,8 @@ class _SettingsPageState extends State { final chips = [ for (final id in _groupIds) if ((id != 'run' || desk) && - (id != 'ai' || c.settings.components.contains('aiimage')) && - (id != 'tor' || c.settings.components.contains('tor'))) + (id != 'ai' || c.settings.display.components.contains('aiimage')) && + (id != 'tor' || c.settings.display.components.contains('tor'))) ActionChip( label: Text(_groupTitle(id), style: const TextStyle(fontSize: 13)), onPressed: () { diff --git a/test/ai_image_test.dart b/test/ai_image_test.dart index 44a4c32..83fc032 100644 --- a/test/ai_image_test.dart +++ b/test/ai_image_test.dart @@ -477,7 +477,7 @@ void main() { final s = AppSettings(); expect((s.aiImage.engine, s.aiImage.device, s.aiImage.taesd, s.aiImage.width, s.aiImage.height, s.aiImage.steps, s.aiImage.cfg, s.aiImage.count), ('local', 'auto', true, 512, 512, 4, 1.0, 1)); - expect(s.components, contains('aiimage')); + expect(s.display.components, contains('aiimage')); final b = AppSettings.fromJson((AppSettings() ..aiImage.width = 768 ..aiImage.steps = 8 @@ -485,9 +485,9 @@ void main() { .toJson()); expect((b.aiImage.width, b.aiImage.steps, b.aiImage.taesd), (768, 8, false)); final old = AppSettings.fromJson({'components': ['mkv', 'browser']}); - expect(old.components, contains('aiimage')); + expect(old.display.components, contains('aiimage')); expect(old.migrated, contains('aiImage')); - final off = AppSettings.fromJson((AppSettings()..components = ['mkv']).toJson()); - expect(off.components, isNot(contains('aiimage')), reason: '사용자가 끈 것은 그대로'); + final off = AppSettings.fromJson((AppSettings()..display.components = ['mkv']).toJson()); + expect(off.display.components, isNot(contains('aiimage')), reason: '사용자가 끈 것은 그대로'); }); } diff --git a/test/app_icon_test.dart b/test/app_icon_test.dart index 802662c..ea02629 100644 --- a/test/app_icon_test.dart +++ b/test/app_icon_test.dart @@ -6,11 +6,11 @@ import 'package:jj_mkvmaker/services/app_shell.dart'; void main() { test('앱 아이콘 설정: 기본은 노랑, 저장 · 불러오기, 모르는 값은 기본으로', () { - expect(AppSettings().appIcon, 'yellow'); - final s = AppSettings()..appIcon = 'black'; - expect(AppSettings.fromJson(s.toJson()).appIcon, 'black'); - expect(AppSettings.fromJson({'appIcon': 'nope'}).appIcon, 'yellow'); - expect(AppSettings.fromJson({}).appIcon, 'yellow'); + expect(AppSettings().display.appIcon, 'yellow'); + final s = AppSettings()..display.appIcon = 'black'; + expect(AppSettings.fromJson(s.toJson()).display.appIcon, 'black'); + expect(AppSettings.fromJson({'appIcon': 'nope'}).display.appIcon, 'yellow'); + expect(AppSettings.fromJson({}).display.appIcon, 'yellow'); }); test('아이콘마다 그림 파일 (설정 미리보기 · 앱 안 버튼 · Windows ico · Android 런처) 이 있다', () { diff --git a/test/batch_test.dart b/test/batch_test.dart index ba11977..c60eca0 100644 --- a/test/batch_test.dart +++ b/test/batch_test.dart @@ -728,7 +728,7 @@ void main() { expect(find.byTooltip('MKV 화면 (지금 여기)'), findsOneWidget); // 홈 화면을 웹 브라우저로 정하면 JJ 아이콘이 브라우저를 연다 (MKV 버튼은 그대로 있음) - c.settings.startScreen = 'browser'; + c.settings.display.startScreen = 'browser'; await tester.tap(find.byTooltip('다운로드 목록')); await tester.pumpAndSettle(); await tester.tap(find.byTooltip(RegExp('^홈 화면 \\(웹 브라우저\\)'))); @@ -909,7 +909,7 @@ void main() { await tester.pump(); await tester.pump(); expect(tester.takeException(), isNull, reason: '두 줄로 바뀐 뒤에는 넘침 없음'); - final pct = tester.getRect(find.text('${(c.settings.uiScale * 100).round()}%')); + final pct = tester.getRect(find.text('${(c.settings.display.scale * 100).round()}%')); for (final tip in ['작업 취소', '결과 폴더 열기: ${outputDirFor(file)}']) { final f = find.byTooltip(tip); expect(f, findsWidgets, reason: tip); diff --git a/test/components_test.dart b/test/components_test.dart index aed8554..a3ba611 100644 --- a/test/components_test.dart +++ b/test/components_test.dart @@ -18,36 +18,36 @@ AppController _plain() => void main() { test('컴포넌트: 설치한 화면만 순서대로 · 홈 화면은 제거되면 첫 화면 · 설정 저장', () { final s = AppSettings(); - expect(s.components, containsAll(['mkv', 'browser', 'explorer', 'rsync', 'downloads', 'viewer'])); - expect(s.components, isNot(contains('docs'))); // 변환기를 받아야 하는 것은 처음엔 꺼짐 - expect([for (final p in AppComponent.pages(s.components, s.navOrder)) p.id], + expect(s.display.components, containsAll(['mkv', 'browser', 'explorer', 'rsync', 'downloads', 'viewer'])); + expect(s.display.components, isNot(contains('docs'))); // 변환기를 받아야 하는 것은 처음엔 꺼짐 + expect([for (final p in AppComponent.pages(s.display.components, s.display.navOrder)) p.id], ['mkv', 'browser', 'explorer', 'rsync', 'downloads', 'aiimage']); s - ..navOrder = ['explorer', 'mkv'] - ..components = ['mkv', 'explorer', 'downloads']; - expect([for (final p in AppComponent.pages(s.components, s.navOrder)) p.id], ['explorer', 'mkv', 'downloads']); - s.startScreen = 'browser'; // 제거한 화면 → 설치된 첫 화면 + ..display.navOrder = ['explorer', 'mkv'] + ..display.components = ['mkv', 'explorer', 'downloads']; + expect([for (final p in AppComponent.pages(s.display.components, s.display.navOrder)) p.id], ['explorer', 'mkv', 'downloads']); + s.display.startScreen = 'browser'; // 제거한 화면 → 설치된 첫 화면 expect(AppNavButtons.homeId(s), 'explorer'); final back = AppSettings.fromJson(s.toJson()); - expect(back.components, ['mkv', 'explorer', 'downloads']); - expect(back.navOrder, ['explorer', 'mkv']); + expect(back.display.components, ['mkv', 'explorer', 'downloads']); + expect(back.display.navOrder, ['explorer', 'mkv']); // 예전 설정 (컴포넌트 없음) 은 기본 - expect(AppSettings.fromJson({}).components, AppComponent.defaultInstalled); + expect(AppSettings.fromJson({}).display.components, AppComponent.defaultInstalled); }); test('225: 이 판이 모르는 구성 요소 · 화면 id 는 지우지 않는다 (예전 판을 거쳐 다시 올려도 켠 것이 남게)', () { final j = {'aiImageV1': true, 'components': ['mkv', 'browser', 'future_screen'], 'navOrder': ['browser', 'future_screen', 'mkv']}; final s = AppSettings.fromJson(j); - expect(s.components, ['mkv', 'browser', 'future_screen']); - expect(s.navOrder, ['browser', 'future_screen', 'mkv']); + expect(s.display.components, ['mkv', 'browser', 'future_screen']); + expect(s.display.navOrder, ['browser', 'future_screen', 'mkv']); // 화면 · 버튼에는 아는 것만 - expect([for (final p in AppComponent.pages(s.components, s.navOrder)) p.id], ['browser', 'mkv']); + expect([for (final p in AppComponent.pages(s.display.components, s.display.navOrder)) p.id], ['browser', 'mkv']); // 저장했다 다시 읽어도 그대로 final back = AppSettings.fromJson(s.toJson()); - expect(back.components, contains('future_screen')); - expect(back.navOrder, contains('future_screen')); + expect(back.display.components, contains('future_screen')); + expect(back.display.navOrder, contains('future_screen')); // 순서 바꾸기 (explorer 를 맨 앞으로): future_screen 은 둘째 자리 그대로 - expect(AppComponent.reorder(s.navOrder, ['explorer', 'mkv', 'browser']), ['explorer', 'future_screen', 'mkv', 'browser']); + expect(AppComponent.reorder(s.display.navOrder, ['explorer', 'mkv', 'browser']), ['explorer', 'future_screen', 'mkv', 'browser']); // 예전 순서에 없던 아는 화면은 뒤에 expect(AppComponent.reorder(['future_screen', 'mkv'], ['mkv', 'explorer']), ['future_screen', 'mkv', 'explorer']); }); @@ -67,7 +67,7 @@ void main() { g.failure.value = DnsFailure('192.0.2.1', 'x', DateTime(2026, 10, 12, 1, 35)); await tester.pump(); expect(find.text('DNS 서버 (192.0.2.1) 에 닿지 않음 (01:35부터)'), findsOneWidget); - await c.updateSettings((s) => s.showLog = !s.showLog); + await c.updateSettings((s) => s.display.showLog = !s.display.showLog); await tester.pump(); expect(find.text('DNS 서버 (192.0.2.1) 에 닿지 않음 (01:35부터)'), findsOneWidget); await tester.tap(find.text('시스템 DNS 로 바꾸기')); @@ -87,11 +87,11 @@ void main() { expect(find.byTooltip('MKV 화면 (지금 여기)'), findsOneWidget); expect(find.byTooltip('파일 탐색기'), findsOneWidget); expect(find.byTooltip('Rsync'), findsOneWidget); - await c.updateSettings((s) => s.components = ['mkv', 'explorer']); + await c.updateSettings((s) => s.display.components = ['mkv', 'explorer']); await tester.pump(); expect(find.byTooltip('Rsync'), findsNothing); expect(find.byTooltip('파일 탐색기'), findsOneWidget); - await c.updateSettings((s) => s.components = ['mkv']); + await c.updateSettings((s) => s.display.components = ['mkv']); await tester.pump(); expect(find.byTooltip('파일 탐색기'), findsNothing); expect(find.byTooltip('MKV 화면 (지금 여기)'), findsOneWidget); @@ -110,15 +110,15 @@ void main() { // 2번째 (웹 브라우저) 를 위로 → 맨 앞 await tester.tap(find.byTooltip('위로').at(1)); await tester.pump(); - expect(c.settings.navOrder.take(2), ['browser', 'mkv']); + expect(c.settings.display.navOrder.take(2), ['browser', 'mkv']); // 맨 앞 (웹 브라우저) 를 아래로 → 다시 2번째 await tester.tap(find.byTooltip('아래로').first); await tester.pump(); - expect(c.settings.navOrder.take(2), ['mkv', 'browser']); + expect(c.settings.display.navOrder.take(2), ['mkv', 'browser']); // 마지막 (AI 그림) 을 위로 → 5번째 await tester.tap(find.byTooltip('위로').last); await tester.pump(); - expect(c.settings.navOrder, ['mkv', 'browser', 'explorer', 'rsync', 'aiimage', 'downloads']); + expect(c.settings.display.navOrder, ['mkv', 'browser', 'explorer', 'rsync', 'aiimage', 'downloads']); }); testWidgets('화면 가운데를 좌우로 밀면 순서대로 다음 · 이전 화면 (끝 다음은 처음)', (tester) async { @@ -127,8 +127,8 @@ void main() { addTearDown(tester.view.reset); final c = _plain(); c.settings - ..components = ['mkv', 'explorer', 'rsync'] - ..navOrder = ['mkv', 'explorer', 'rsync']; + ..display.components = ['mkv', 'explorer', 'rsync'] + ..display.navOrder = ['mkv', 'explorer', 'rsync']; await tester.pumpWidget(MaterialApp( builder: (context, child) => AppScope(controller: c, child: child!), home: HomePage(c: c), @@ -157,7 +157,7 @@ void main() { await swipe(600); // 오른쪽으로 → 이전 (끝으로) expect(find.byTooltip('Rsync (지금 여기)'), findsOneWidget); // 102: 끝에서 처음으로 돌기를 끄면 끝에서 멈춘다 - await c.updateSettings((s) => s.swipeWrap = false); + await c.updateSettings((s) => s.display.swipeWrap = false); await tester.pump(); await swipe(-600); expect(find.byTooltip('Rsync (지금 여기)'), findsOneWidget); @@ -166,9 +166,9 @@ void main() { expect(find.byTooltip('파일 탐색기 (지금 여기)'), findsOneWidget); await swipe(-600); expect(find.byTooltip('Rsync (지금 여기)'), findsOneWidget); - await c.updateSettings((s) => s.swipeWrap = true); + await c.updateSettings((s) => s.display.swipeWrap = true); // 끄면 밀어도 그대로 - await c.updateSettings((s) => s.swipeNav = false); + await c.updateSettings((s) => s.display.swipeNav = false); await tester.pump(); await swipe(600); expect(find.byTooltip('Rsync (지금 여기)'), findsOneWidget); @@ -185,8 +185,8 @@ void main() { addTearDown(tester.view.reset); final c = _plain(); c.settings - ..components = ['mkv', 'explorer'] - ..navOrder = ['mkv', 'explorer']; + ..display.components = ['mkv', 'explorer'] + ..display.navOrder = ['mkv', 'explorer']; final navKey = GlobalKey(); await tester.pumpWidget(MaterialApp( navigatorKey: navKey, diff --git a/test/decisions_test.dart b/test/decisions_test.dart index 56d6be9..96fd32b 100644 --- a/test/decisions_test.dart +++ b/test/decisions_test.dart @@ -27,23 +27,23 @@ void main() { test('기본값 = 정한 답 · 저장 · 읽기', () { final s = AppSettings(); expect([ - s.run.startMenuShortcut, s.copy.syncStopToast, s.swipeWrap, s.migrationNotice, s.allowInnerToOuter, + s.run.startMenuShortcut, s.copy.syncStopToast, s.display.swipeWrap, s.migrationNotice, s.allowInnerToOuter, s.rememberPasswords, s.recycleOnDelete, s.reader.systemBars, ], everyElement(isTrue)); expect(s.cleanupPrechecked, ['work', 'download']); expect(s.tripleTapAction, 'move'); expect(s.remote.davExternalOpen, 'ask'); // 이미 있던 설정의 기본값도 결정대로 - expect([s.browser.pauseOnLeave, s.run.autoCheckUpdates, s.browser.adSkip, s.browser.adHide, s.run.inBackground, s.swipeNav], + expect([s.browser.pauseOnLeave, s.run.autoCheckUpdates, s.browser.adSkip, s.browser.adHide, s.run.inBackground, s.display.swipeNav], everyElement(isTrue)); // 탐색기 처음 배치 = 두 창 (규칙 7 · 관리자 결정 3, 10/22) 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]); + expect([DisplaySettings.scaleMin, DisplaySettings.scaleMax], [0.5, 2.5]); // 바꾼 값은 남는다 final b = AppSettings.fromJson((AppSettings() ..run.startMenuShortcut = false ..copy.syncStopToast = false - ..swipeWrap = false + ..display.swipeWrap = false ..migrationNotice = false ..allowInnerToOuter = false ..rememberPasswords = false @@ -52,7 +52,7 @@ void main() { ..tripleTapAction = 'none' ..remote.davExternalOpen = 'url') .toJson()); - expect([b.run.startMenuShortcut, b.copy.syncStopToast, b.swipeWrap, b.migrationNotice, b.allowInnerToOuter, b.rememberPasswords, + expect([b.run.startMenuShortcut, b.copy.syncStopToast, b.display.swipeWrap, b.migrationNotice, b.allowInnerToOuter, b.rememberPasswords, b.recycleOnDelete], everyElement(isFalse)); expect([b.cleanupPrechecked, b.tripleTapAction, b.remote.davExternalOpen], [['logs'], 'none', 'url']); }); diff --git a/test/i18n_test.dart b/test/i18n_test.dart index 8b8c684..7508695 100644 --- a/test/i18n_test.dart +++ b/test/i18n_test.dart @@ -86,7 +86,7 @@ void main() { test('언어 바꾸기: tr · trf · 선택 목록 이름 · 언어 이름이 그 언어로, 한국어로 되돌리기', () async { expect(tr('MKV 만들기'), 'MKV 만들기'); await i18n.apply('en'); - expect(c.settings.uiLanguage, 'en'); + expect(c.settings.display.language, 'en'); expect(tr('MKV 만들기'), 'Make MKV'); expect(trf('동영상 {0}개 · 전체 선택', [3]), '3 videos · select all'); expect(PlaylistMode.folder.label, 'All videos in the same folder'); @@ -96,7 +96,7 @@ void main() { expect(tr('환경 설정'), '環境設定'); await i18n.apply('ko'); expect(tr('환경 설정'), '환경 설정'); - expect(AppSettings.fromJson(c.settings.toJson()).uiLanguage, 'ko'); + expect(AppSettings.fromJson(c.settings.toJson()).display.language, 'ko'); }); test('58 · 102: 시스템 언어 따르기 - 설정에는 system, 화면은 기기 언어 (들어 있지 않으면 English), 처음 설치의 기본', () async { @@ -106,10 +106,10 @@ void main() { expect(I18nController.systemCode('zh_CN'), 'zh-Hans'); expect(I18nController.systemCode('en_US'), 'en'); expect(I18nController.systemCode('fr_FR'), 'en'); - expect(AppSettings().uiLanguage, I18nController.system); // 처음 설치 - expect(AppSettings.fromJson({'uiLanguage': 'ko'}).uiLanguage, 'ko'); // 고른 언어는 그대로 + expect(AppSettings().display.language, I18nController.system); // 처음 설치 + expect(AppSettings.fromJson({'uiLanguage': 'ko'}).display.language, 'ko'); // 고른 언어는 그대로 await i18n.apply(I18nController.system); - expect(c.settings.uiLanguage, I18nController.system); + expect(c.settings.display.language, I18nController.system); expect(uiLanguage, I18nController.systemCode()); await i18n.apply('ko'); }); @@ -130,14 +130,14 @@ void main() { expect(i18n.addable.map((l) => l.code), contains('fr')); expect(i18n.addable.map((l) => l.code), isNot(contains('en'))); await i18n.addLanguage(fr); - expect(c.settings.uiLanguagesAdded, ['fr']); + expect(c.settings.display.languagesAdded, ['fr']); expect(i18n.available, ['ko', 'en', 'ja', 'zh-Hans', 'fr']); expect(File(p.join(dir.path, 'l10n', 'fr.json')).existsSync(), isTrue); await i18n.apply('fr'); expect(tr('취소'), '[fra_Latn] Cancel'); expect(trf('{0}개 선택', [2]), '[fra_Latn] 2 selected'); await i18n.removeLanguage('fr'); - expect(c.settings.uiLanguagesAdded, isEmpty); + expect(c.settings.display.languagesAdded, isEmpty); expect(uiLanguage, 'ko', reason: '쓰던 언어를 지우면 한국어로'); expect(File(p.join(dir.path, 'l10n', 'fr.json')).existsSync(), isFalse); }); diff --git a/test/mkv_compact_list_test.dart b/test/mkv_compact_list_test.dart index ff2cdff..a36ea03 100644 --- a/test/mkv_compact_list_test.dart +++ b/test/mkv_compact_list_test.dart @@ -20,7 +20,7 @@ void main() { createTranslator: () => throw UnimplementedError(), )); c.settings - ..showLog = true + ..display.showLog = true ..mkvCompactList = size; for (var i = 0; i < n; i++) { c.videos.add(VideoItem('D:/v/v$i.mp4')); diff --git a/test/mkv_landscape_test.dart b/test/mkv_landscape_test.dart index a8198da..54c4fea 100644 --- a/test/mkv_landscape_test.dart +++ b/test/mkv_landscape_test.dart @@ -14,7 +14,7 @@ void main() { tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); - c.settings.showLog = true; + c.settings.display.showLog = true; for (var i = 1; i <= 6; i++) { c.videos.add(VideoItem('D:\\v\\영상$i.mp4')); } diff --git a/test/mkv_portrait_detail_test.dart b/test/mkv_portrait_detail_test.dart index c77bb65..dbc830a 100644 --- a/test/mkv_portrait_detail_test.dart +++ b/test/mkv_portrait_detail_test.dart @@ -22,7 +22,7 @@ void main() { createRecognizer: () => throw UnimplementedError(), createTranslator: () => throw UnimplementedError(), )); - c.settings.showLog = true; + c.settings.display.showLog = true; c.settings.moveTargets = [MoveTarget('보관', r'D:\보관')]; for (var i = 1; i <= 4; i++) { c.videos.add(VideoItem('D:/v/아주 긴 이름의 동영상 파일 $i.mp4')); diff --git a/test/player_test.dart b/test/player_test.dart index 6bb357a..c58a40a 100644 --- a/test/player_test.dart +++ b/test/player_test.dart @@ -163,14 +163,14 @@ void main() { test('Android 화면 방향: 기본 자동, 가로 · 세로 고정 저장, 예전 기본 (가로 고정) 은 한 번 자동으로', () { final s = AppSettings(); - expect(s.screenOrientation, 'auto'); + expect(s.display.orientation, 'auto'); for (final m in ['portrait', 'auto', 'landscape']) { - s.screenOrientation = m; - expect(AppSettings.fromJson(s.toJson()).screenOrientation, m); + s.display.orientation = m; + expect(AppSettings.fromJson(s.toJson()).display.orientation, m); } - expect(AppSettings.fromJson({'screenOrientation': 'sideways', 'orientationV2': true}).screenOrientation, 'auto'); + expect(AppSettings.fromJson({'screenOrientation': 'sideways', 'orientationV2': true}).display.orientation, 'auto'); // 예전 설정 파일 (기본 가로 고정 시절) → 자동 - expect(AppSettings.fromJson({'screenOrientation': 'landscape'}).screenOrientation, 'auto'); + expect(AppSettings.fromJson({'screenOrientation': 'landscape'}).display.orientation, 'auto'); }); }); diff --git a/test/queue_test.dart b/test/queue_test.dart index 34e48f5..820f133 100644 --- a/test/queue_test.dart +++ b/test/queue_test.dart @@ -200,11 +200,11 @@ void main() { expect(find.byTooltip('작업 기록 숨기기'), findsOneWidget); await tester.tap(find.byTooltip('작업 기록 숨기기')); await tester.pump(); - expect(c.settings.showLog, isFalse); + expect(c.settings.display.showLog, isFalse); expect(find.text('작업 기록 보기'), findsOneWidget); await tester.tap(find.text('작업 기록 보기')); await tester.pump(); - expect(c.settings.showLog, isTrue); + expect(c.settings.display.showLog, isTrue); expect(find.byTooltip('작업 기록 숨기기'), findsOneWidget); }); diff --git a/test/scale_menu_test.dart b/test/scale_menu_test.dart index b3bee38..74d5a81 100644 --- a/test/scale_menu_test.dart +++ b/test/scale_menu_test.dart @@ -25,16 +25,16 @@ class _Shell extends NoopShell { void main() { test('화면 크기 설정: 범위 · 5% 단위 · 저장', () { - expect(AppSettings.clampUiScale(null), 1.0); - expect(AppSettings.clampUiScale(0.1), 0.5); - expect(AppSettings.clampUiScale(9), 2.5); - expect(AppSettings.clampUiScale(2.5), 2.5); - expect(AppSettings.clampUiScale(1.1000000001), 1.1); + expect(DisplaySettings.clampScale(null), 1.0); + expect(DisplaySettings.clampScale(0.1), 0.5); + expect(DisplaySettings.clampScale(9), 2.5); + expect(DisplaySettings.clampScale(2.5), 2.5); + expect(DisplaySettings.clampScale(1.1000000001), 1.1); final s = AppSettings() - ..uiScale = 1.2 - ..uiScaleDefault = 0.9; + ..display.scale = 1.2 + ..display.scaleDefault = 0.9; final back = AppSettings.fromJson(s.toJson()); - expect([back.uiScale, back.uiScaleDefault], [1.2, 0.9]); + expect([back.display.scale, back.display.scaleDefault], [1.2, 0.9]); }); testWidgets('화면 크기 버튼: − · + 로 10% 씩, 숫자를 누르면 기본 크기로, 화면이 실제로 커진다', (tester) async { @@ -61,7 +61,7 @@ void main() { await tester.pump(); await tester.tap(find.byTooltip('화면 크게')); await tester.pump(); - expect(c.settings.uiScale, 1.2); + expect(c.settings.display.scale, 1.2); expect(find.text('120%'), findsOneWidget); // 화면에 그려지는 크기가 1.2 배 final r = tester.getRect(find.byKey(const Key('box'))); @@ -71,17 +71,17 @@ void main() { await tester.tap(find.byTooltip('화면 작게')); await tester.pump(); - expect(c.settings.uiScale, 1.1); + expect(c.settings.display.scale, 1.1); // 기본 크기 90% 로 정해 두고 숫자를 누르면 그 크기로 - c.settings.uiScaleDefault = 0.9; + c.settings.display.scaleDefault = 0.9; await tester.tap(find.text('110%')); await tester.pump(); - expect(c.settings.uiScale, 0.9); + expect(c.settings.display.scale, 0.9); expect(find.text('90%'), findsOneWidget); // 끝까지 줄이면 − 가 꺼진다 - c.settings.uiScale = AppSettings.uiScaleMin; + c.settings.display.scale = DisplaySettings.scaleMin; c.updateSettings((_) {}); await tester.pump(); expect(tester.widget(find.widgetWithIcon(IconButton, Icons.remove)).onPressed, isNull); diff --git a/test/settings_store_test.dart b/test/settings_store_test.dart index 9ae4f17..a7ee528 100644 --- a/test/settings_store_test.dart +++ b/test/settings_store_test.dart @@ -140,7 +140,7 @@ void main() { 'imageExts': ['jpg'], 'runInBackground': false, }); - expect(old.screenOrientation, 'auto'); + expect(old.display.orientation, 'auto'); // 규칙 7 · 결정 3 (10/22): 처음 배치가 두 창이 되어 예전 'dual' 은 그대로 (옮겼다는 알림 없음) expect(old.explorer.layout, 'dual'); expect(old.reader.zipComic, isFalse); @@ -149,10 +149,10 @@ void main() { // 다시 저장 · 읽기: 이제 사용자가 고른 값으로 보고 그대로 · 알림 없음 final again = AppSettings.fromJson((old ..explorer.layout = 'dual' - ..screenOrientation = 'landscape') + ..display.orientation = 'landscape') .toJson()); expect(again.explorer.layout, 'dual'); - expect(again.screenOrientation, 'landscape'); + expect(again.display.orientation, 'landscape'); expect(again.migrated, isEmpty); // 처음 설치 (빈 설정) 는 알릴 것 없음 expect(AppSettings.fromJson({}).migrated, isEmpty); diff --git a/test/theme_test.dart b/test/theme_test.dart index f6e352e..f2f3666 100644 --- a/test/theme_test.dart +++ b/test/theme_test.dart @@ -41,11 +41,11 @@ void main() { tearDown(() => JjColors.use(Brightness.dark)); test('처음 값은 어둡게 (예전 색 그대로) · 모르는 값 · 없는 값도 어둡게 · 고른 값은 그대로', () { - expect(AppSettings().uiBrightness, 'dark'); - expect(AppSettings.fromJson({}).uiBrightness, 'dark'); - expect(AppSettings.fromJson({'uiBrightness': 'neon'}).uiBrightness, 'dark'); - expect(AppSettings.fromJson({'uiBrightness': 'light'}).uiBrightness, 'light'); - expect(AppSettings.fromJson(AppSettings.fromJson({'uiBrightness': 'system'}).toJson()).uiBrightness, 'system'); + expect(AppSettings().display.brightness, 'dark'); + expect(AppSettings.fromJson({}).display.brightness, 'dark'); + expect(AppSettings.fromJson({'uiBrightness': 'neon'}).display.brightness, 'dark'); + expect(AppSettings.fromJson({'uiBrightness': 'light'}).display.brightness, 'light'); + expect(AppSettings.fromJson(AppSettings.fromJson({'uiBrightness': 'system'}).toJson()).display.brightness, 'system'); expect(JjColors.bg, const Color(0xFF141417)); expect(JjColors.accent, const Color(0xFF00C4CC)); expect(JjColors.textDim, const Color(0xFF8E8E99)); @@ -98,7 +98,7 @@ void main() { BuildContext ctx() => t.element(find.byType(Scaffold).first); expect(Theme.of(ctx()).brightness, Brightness.dark); expect(JjColors.isLight, isFalse); - await t.runAsync(() => c.updateSettings((x) => x.uiBrightness = 'light')); + await t.runAsync(() => c.updateSettings((x) => x.display.brightness = 'light')); await t.pump(); await t.pump(); expect(JjColors.isLight, isTrue); @@ -106,7 +106,7 @@ void main() { expect(Theme.of(ctx()).scaffoldBackgroundColor, JjPalette.light.bg); // 기기 따라 t.platformDispatcher.platformBrightnessTestValue = Brightness.dark; - await t.runAsync(() => c.updateSettings((x) => x.uiBrightness = 'system')); + await t.runAsync(() => c.updateSettings((x) => x.display.brightness = 'system')); await t.pump(); await t.pump(); expect(JjColors.isLight, isFalse); @@ -114,7 +114,7 @@ void main() { await t.pump(); await t.pump(); expect(JjColors.isLight, isTrue, reason: '기기 밝기가 바뀌면 따라감'); - await t.runAsync(() => c.updateSettings((x) => x.uiBrightness = 'dark')); + await t.runAsync(() => c.updateSettings((x) => x.display.brightness = 'dark')); await t.pump(); await t.pump(); expect(JjColors.isLight, isFalse); diff --git a/test/tor_test.dart b/test/tor_test.dart index 38168c8..56b1e75 100644 --- a/test/tor_test.dart +++ b/test/tor_test.dart @@ -70,9 +70,9 @@ void main() { final ids = [for (final p in AppComponent.pages(['mkv', 'browser', 'tor', 'explorer'], ['explorer', 'mkv', 'browser'])) p.id]; expect(ids, ['explorer', 'mkv', 'browser', 'tor']); expect([for (final p in AppComponent.pages(['mkv', 'browser', 'tor'], ['browser', 'mkv'])) p.id], ['browser', 'tor', 'mkv']); - final s = AppSettings()..startScreen = 'tor'; + final s = AppSettings()..display.startScreen = 'tor'; expect(AppNavButtons.homeId(s), 'mkv', reason: '설치하지 않았으면 첫 화면'); - s.components = [...s.components, 'tor']; + s.display.components = [...s.display.components, 'tor']; expect(AppNavButtons.homeId(s), 'tor'); }); @@ -165,7 +165,7 @@ void main() { final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings ..tor.noticeSeen = true - ..components = [...c.settings.components, 'tor']; + ..display.components = [...c.settings.display.components, 'tor']; await t.pumpWidget(MaterialApp( navigatorKey: navKey, builder: (context, child) => AppScope(controller: c, child: child!),