diff --git a/integration_test/update_path_real_test.dart b/integration_test/update_path_real_test.dart index 6b47b5e..c2cd281 100644 --- a/integration_test/update_path_real_test.dart +++ b/integration_test/update_path_real_test.dart @@ -39,7 +39,7 @@ void main() { mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService(), updater: up), settingsStore: SettingsStore()); await t.runAsync(c.init); - expect(c.settings.lastRunVersion, from, reason: '예전 판이 쓴 데이터를 읽음'); + expect(c.settings.run.lastRunVersion, from, reason: '예전 판이 쓴 데이터를 읽음'); final r = (await t.runAsync(up.latest))!; expect(r.name, contains('시험 길')); t.view.physicalSize = const Size(1200, 900); diff --git a/lib/app/settings.dart b/lib/app/settings.dart index eb62827..6591ead 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/run_settings.dart'; import 'settings/browser_settings.dart'; import 'settings/download_settings.dart'; import 'settings/reader_settings.dart'; @@ -27,6 +28,7 @@ import 'settings/explorer_settings.dart'; export 'settings/records.dart'; export 'settings/settings_group.dart'; export 'settings/settings_store.dart'; +export 'settings/run_settings.dart'; export 'settings/browser_settings.dart'; export 'settings/download_settings.dart'; export 'settings/reader_settings.dart'; @@ -53,15 +55,8 @@ class AppSettings { - /// 최소화하면 작업 표시줄 대신 트레이로 - bool minimizeToTray = true; - /// 종료 (창 X · 종료 버튼) 를 눌렀을 때: - /// 'background' 창만 숨기고 다운로드 · 변환 계속 / 'quit' 묻지 않고 모두 종료 / 'ask' 매번 고르기 - String closeAction = 'background'; - /// 창 보이기 단축키 (예: Ctrl+Shift+X) - String showHotkey = 'Ctrl+Shift+X'; /// AI 자막: 매번 설정 창을 띄울지 (끄면 마지막 설정으로 바로 시작) bool askAiOptions = true; @@ -229,8 +224,6 @@ class AppSettings { - /// 99 (Windows): 시작 메뉴에 등록 - 알림이 JJ_MKVMaker 로 보이고 눌러서 열린다 - bool startMenuShortcut = true; // ── 102: 사용자에게 물어 정한 것 · 관리자 1차 판단 (기본값 = 그 답) ── @@ -312,42 +305,18 @@ class AppSettings { - /// 백그라운드로 실행 (Android): 켜면 ← 로 닫거나 최근 앱에서 밀어도 동기화 · MKV 만들기 · 다운로드를 계속한다 (알림에 표시). - /// 끄면 지금처럼 닫을 때 끝난다. Windows 는 [closeAction] 'background' 가 같은 뜻 ([backgroundRun]). - bool runInBackground = true; - /// 백그라운드로 실행 (설정 화면의 체크 하나). Windows: 창 ✕ 를 누르면 트레이로 (끄면 종료), Android: [runInBackground] - bool get backgroundRun => Platform.isWindows ? closeAction == 'background' : runInBackground; - set backgroundRun(bool v) { - if (Platform.isWindows) { - closeAction = v ? 'background' : 'quit'; - } else { - runInBackground = v; - } - } /// 앱 안 브라우저의 데이터 폴더 (로그인 · 쿠키). 실행 중에 정해지며 저장하지 않음 String webViewDataDir = ''; - /// 시작할 때 새 버전 확인 (하루 한 번) - bool autoCheckUpdates = true; - String lastUpdateCheck = ''; - /// [lastUpdateCheck] 를 한 버전 (같은 설정 파일을 쓰는 다른 버전이 확인했으면 다시 확인) - String lastUpdateCheckVersion = ''; - /// 마지막으로 이 설정을 쓴 (실행한) 버전. 예전 버전은 이 항목을 몰라 저장하면서 지운다 → - /// 돌아온 버전이 자기 것이 아니면 보관해 둔 그 버전의 설정을 되살릴지 묻는다 (VersionSnapshot) - String lastRunVersion = ''; - /// P0 (Android 되돌리기): 받은 AI 모델을 공용 폴더에 옮겨 두었다가 되살릴지. '' = 남은 공간이 모델의 2배 이상이면 켬 · 'on' · 'off' - String rollbackKeepModels = ''; - /// "이 버전 건너뛰기" 한 버전 - String skippedVersion = ''; /// 동영상 하나를 재생할 때 재생 목록 (기본: 같은 시리즈) PlaylistMode playlistMode = PlaylistMode.series; @@ -389,8 +358,11 @@ class AppSettings { /// 웹 브라우저 (앱 안 · 외부) final browser = BrowserSettings(); + /// 실행 · 종료 · 업데이트 + final run = RunSettings(); + /// 설정 분야들 (설정 A) - 저마다 자기 키를 읽고 쓴다. toJson · fromJson 은 이 목록을 돌며 합치기만 한다 - List get groups => [explorer, network, tor, copy, aiImage, reader, download, browser]; + List get groups => [explorer, network, tor, copy, aiImage, reader, download, browser, run]; /// [plainSecrets]: 안전 저장소를 쓸 수 없을 때만 (예전 파일에 있던 비밀 값을 잃지 않게) 예전처럼 파일에 둔다 Map toJson({bool plainSecrets = false}) { @@ -400,9 +372,6 @@ class AppSettings { 'mkvOutputRoot': mkvOutputRoot, 'moveTargets': [for (final t in moveTargets) t.toJson()], 'folderBookmarks': [for (final b in folderBookmarks) b.toJson()], - 'minimizeToTray': minimizeToTray, - 'closeAction': closeAction, - 'showHotkey': showHotkey, 'askAiOptions': askAiOptions, // 비밀 값은 안전 저장소에 (54). 파일에는 저장해 두었는지만 'openSubtitlesSaved': { @@ -447,7 +416,6 @@ class AppSettings { 'navOrder': navOrder, 'swipeNav': swipeNav, 'imageExts': imageExts, - 'startMenuShortcut': startMenuShortcut, 'swipeWrap': swipeWrap, 'cleanupPrechecked': cleanupPrechecked, 'migrationNotice': migrationNotice, @@ -469,14 +437,6 @@ class AppSettings { 'smbRequireSecure': smbRequireSecure, 'remoteIdleSeconds': remoteIdleSeconds, 'ftpServers': [for (final x in ftpServers) x.toJson()], - 'runInBackground': runInBackground, - 'backgroundV2': true, - 'autoCheckUpdates': autoCheckUpdates, - 'lastUpdateCheck': lastUpdateCheck, - 'lastUpdateCheckVersion': lastUpdateCheckVersion, - 'lastRunVersion': lastRunVersion, - 'rollbackKeepModels': rollbackKeepModels, - 'skippedVersion': skippedVersion, 'playlistMode': playlistMode.name, 'externalPlayers': externalPlayers, 'encode': { @@ -518,9 +478,6 @@ class AppSettings { // 예전 판의 이동 폴더 하나 → "이동" 버튼으로 if (j['moveTargets'] == null && j['moveTargetDir'] is String) MoveTarget('이동', j['moveTargetDir'] as String), ] - ..minimizeToTray = j['minimizeToTray'] as bool? ?? true - ..closeAction = const ['background', 'quit', 'ask'].contains(j['closeAction']) ? j['closeAction'] as String : 'background' - ..showHotkey = j['showHotkey'] as String? ?? 'Ctrl+Shift+X' ..askAiOptions = j['askAiOptions'] as bool? ?? true ..openSubtitlesKey = j['openSubtitlesKey'] as String? ?? '' ..openSubtitlesUser = j['openSubtitlesUser'] as String? ?? '' @@ -567,7 +524,6 @@ class AppSettings { ..imageExts = j['imageExts'] is List ? [for (final x in j['imageExts'] as List) '$x'.toLowerCase()] : [...defaultImageExtensions] - ..startMenuShortcut = j['startMenuShortcut'] != false ..swipeWrap = j['swipeWrap'] != false ..cleanupPrechecked = j['cleanupPrechecked'] is List ? [for (final x in j['cleanupPrechecked'] as List) '$x'] @@ -599,14 +555,6 @@ class AppSettings { for (final x in (j['ftpServers'] as List?) ?? const []) if (x is Map) FtpServer.fromJson(x), ] - // 사용자 결정 (10/8): 기본 켜짐. 예전 기본 (꺼짐) 은 한 번 켜짐으로 - ..runInBackground = j['backgroundV2'] == true ? j['runInBackground'] != false : true - ..autoCheckUpdates = j['autoCheckUpdates'] as bool? ?? true - ..lastUpdateCheck = j['lastUpdateCheck'] as String? ?? '' - ..lastUpdateCheckVersion = j['lastUpdateCheckVersion'] as String? ?? '' - ..lastRunVersion = j['lastRunVersion'] as String? ?? '' - ..rollbackKeepModels = const ['on', 'off'].contains(j['rollbackKeepModels']) ? j['rollbackKeepModels'] as String : '' - ..skippedVersion = j['skippedVersion'] as String? ?? '' ..playlistMode = pick(PlaylistMode.values, j['playlistMode'], PlaylistMode.series) ..externalPlayers = ((j['externalPlayers'] as Map?) ?? const {}).cast() ..encode = EncodeSettings( @@ -693,7 +641,7 @@ class AppSettings { if (j['cookieScopeV2'] != true && j.isNotEmpty && (j['ytCookiesBrowser'] ?? internalBrowserCookies) == internalBrowserCookies) 'cookieScope', 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) + if (j['backgroundV2'] != true && j['runInBackground'] == false && s.run.inBackground && Platform.isAndroid) 'background', // 193: 그 설정이 생기기 전 판 (설정 파일은 있는데 그 항목이 없음 - 예: 10.03) 에서 올라온 사람이 바로 겪는 동작 변화. // 예전 판에는 키가 없어 위의 "예전 값 → 새 값" 조건에 걸리지 않았다 (P11: 탐색기에서 한 번 눌렀는데 바로 열려 놀람) @@ -706,7 +654,7 @@ class AppSettings { 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('runInBackground') && s.runInBackground && Platform.isAndroid) 'background', + 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', // 193 · 229: 예전 판을 쓰던 사람 (설정 파일이 있는데 이 항목이 없음) 에게 복사 · 이동이 받는 쪽의 더 새 파일을 건너뛰게 바뀐 것을 한 번 diff --git a/lib/app/settings/run_settings.dart b/lib/app/settings/run_settings.dart new file mode 100644 index 0000000..7121c6c --- /dev/null +++ b/lib/app/settings/run_settings.dart @@ -0,0 +1,84 @@ +import 'dart:io'; + +import 'settings_group.dart'; + +/// 약속 (설정 A): 환경 설정 > 실행 · 종료 · 업데이트 - 자기 키 (이름 · 처음 값 · 읽기 · 쓰기) 를 스스로 갖는다. +/// 설정 하나를 더할 때는 이 파일만 고친다. 저장 파일의 키 이름은 예전 그대로 (평평한 키) +class RunSettings implements SettingsGroup { + /// 최소화하면 작업 표시줄 대신 트레이로 + bool minimizeToTray = true; + + /// 종료 (창 X · 종료 버튼) 를 눌렀을 때: + /// 'background' 창만 숨기고 다운로드 · 변환 계속 / 'quit' 묻지 않고 모두 종료 / 'ask' 매번 고르기 + String closeAction = 'background'; + + /// 창 보이기 단축키 (예: Ctrl+Shift+X) + String showHotkey = 'Ctrl+Shift+X'; + + /// 99 (Windows): 시작 메뉴에 등록 - 알림이 JJ_MKVMaker 로 보이고 눌러서 열린다 + bool startMenuShortcut = true; + + /// 백그라운드로 실행 (Android): 켜면 ← 로 닫거나 최근 앱에서 밀어도 동기화 · MKV 만들기 · 다운로드를 계속한다 (알림에 표시). + /// 끄면 지금처럼 닫을 때 끝난다. Windows 는 [closeAction] 'background' 가 같은 뜻 ([backgroundRun]). + bool inBackground = true; + + /// 시작할 때 새 버전 확인 (하루 한 번) + bool autoCheckUpdates = true; + + String lastUpdateCheck = ''; + + /// [lastUpdateCheck] 를 한 버전 (같은 설정 파일을 쓰는 다른 버전이 확인했으면 다시 확인) + String lastUpdateCheckVersion = ''; + + /// 마지막으로 이 설정을 쓴 (실행한) 버전. 예전 버전은 이 항목을 몰라 저장하면서 지운다 → + /// 돌아온 버전이 자기 것이 아니면 보관해 둔 그 버전의 설정을 되살릴지 묻는다 (VersionSnapshot) + String lastRunVersion = ''; + + /// P0 (Android 되돌리기): 받은 AI 모델을 공용 폴더에 옮겨 두었다가 되살릴지. '' = 남은 공간이 모델의 2배 이상이면 켬 · 'on' · 'off' + String rollbackKeepModels = ''; + + /// "이 버전 건너뛰기" 한 버전 + String skippedVersion = ''; + + /// 백그라운드로 실행 (설정 화면의 체크 하나). Windows: 창 ✕ 를 누르면 트레이로 (끄면 종료), Android: [inBackground] + bool get backgroundRun => Platform.isWindows ? closeAction == 'background' : inBackground; + set backgroundRun(bool v) { + if (Platform.isWindows) { + closeAction = v ? 'background' : 'quit'; + } else { + inBackground = v; + } + } + + @override + void write(Map j, {required bool plainSecrets}) { + j['minimizeToTray'] = minimizeToTray; + j['closeAction'] = closeAction; + j['showHotkey'] = showHotkey; + j['runInBackground'] = inBackground; + j['startMenuShortcut'] = startMenuShortcut; + j['autoCheckUpdates'] = autoCheckUpdates; + j['lastUpdateCheck'] = lastUpdateCheck; + j['lastUpdateCheckVersion'] = lastUpdateCheckVersion; + j['lastRunVersion'] = lastRunVersion; + j['rollbackKeepModels'] = rollbackKeepModels; + j['skippedVersion'] = skippedVersion; + j['backgroundV2'] = true; + } + + @override + void read(Map j) { + minimizeToTray = j['minimizeToTray'] as bool? ?? true; + closeAction = const ['background', 'quit', 'ask'].contains(j['closeAction']) ? j['closeAction'] as String : 'background'; + showHotkey = j['showHotkey'] as String? ?? 'Ctrl+Shift+X'; + startMenuShortcut = j['startMenuShortcut'] != false; + // 사용자 결정 (10/8): 기본 켜짐. 예전 기본 (꺼짐) 은 한 번 켜짐으로 + inBackground = j['backgroundV2'] == true ? j['runInBackground'] != false : true; + autoCheckUpdates = j['autoCheckUpdates'] as bool? ?? true; + lastUpdateCheck = j['lastUpdateCheck'] as String? ?? ''; + lastUpdateCheckVersion = j['lastUpdateCheckVersion'] as String? ?? ''; + lastRunVersion = j['lastRunVersion'] as String? ?? ''; + rollbackKeepModels = const ['on', 'off'].contains(j['rollbackKeepModels']) ? j['rollbackKeepModels'] as String : ''; + skippedVersion = j['skippedVersion'] as String? ?? ''; + } +} diff --git a/lib/main.dart b/lib/main.dart index 1175f86..2c9dfe1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -172,7 +172,7 @@ Future main(List args) async { Future confirmQuit({bool force = false}) async { final shell = services.shell; final desktop = shell is DesktopShell ? shell : null; - var mode = force || (desktop?.closeFromTray ?? false) ? 'force' : controller.settings.closeAction; + var mode = force || (desktop?.closeFromTray ?? false) ? 'force' : controller.settings.run.closeAction; // 트레이 아이콘이 없으면 숨긴 창을 끝낼 방법이 없다 → 고르게 한다 if (mode == 'background' && !(desktop?.hasTray ?? false)) mode = 'ask'; if (mode == 'ask') { @@ -186,9 +186,9 @@ Future main(List args) async { if ((CopyCenter.peekOf(controller)?.activeCount ?? 0) > 0) trf('복사 · 이동 {0}개', [CopyCenter.peekOf(controller)!.activeCount]), ].join(' · ').ifEmptyText(tr('진행 중인 작업 없음')), - hotkey: controller.settings.showHotkey); + hotkey: controller.settings.run.showHotkey); if (pick == null) return false; - if (pick.$2) controller.updateSettings((x) => x.closeAction = pick.$1); + if (pick.$2) controller.updateSettings((x) => x.run.closeAction = pick.$1); mode = pick.$1; } if (mode == 'background') { @@ -251,8 +251,8 @@ Future main(List args) async { } await services.shell.init( - hotkey: controller.settings.showHotkey, - minimizeToTray: () => controller.settings.minimizeToTray, + hotkey: controller.settings.run.showHotkey, + minimizeToTray: () => controller.settings.run.minimizeToTray, onCloseRequested: confirmQuit, onDownloadsRequested: () { final nav = navigatorKey.currentState; @@ -266,7 +266,7 @@ Future main(List args) async { if (StartMenu.userAreaLocked()) { // 관리자 지시 (10/20): 시험판 · 시험 (따로 데이터) - 사용자 바로 가기 · 주소 연결은 그대로 (StartMenu 가 막음), 작업 기록에 한 줄 controller.note(tr('시험용 데이터 폴더 (JJ_MKVMAKER_DATA) 로 켜져 시작 메뉴 바로 가기 · jjmkvmaker:// 연결은 건드리지 않습니다')); - } else if (controller.settings.startMenuShortcut) { + } else if (controller.settings.run.startMenuShortcut) { unawaited(StartMenu.ensure().then((ok) { if (shell0 is DesktopShell) shell0.toastAppReady = ok; })); diff --git a/lib/platform/android/android_keep_alive.dart b/lib/platform/android/android_keep_alive.dart index 27c5020..e9f3686 100644 --- a/lib/platform/android/android_keep_alive.dart +++ b/lib/platform/android/android_keep_alive.dart @@ -15,7 +15,7 @@ import '../../l10n/tr.dart'; /// Android: 다운로드 · MKV 만들기 · AI 자막이 진행 중이면 포그라운드 서비스 (MainActivity.kt · KeepAliveService.kt) /// 를 띄워 앱이 화면에서 내려가도 계속하게 하고, 알림에 진행 상황을 보여 준다. 모두 끝나면 내린다. -/// [AppSettings.runInBackground] 를 켜면 실시간 동기화를 지켜보는 동안에도 띄워 두고, 앱을 닫아도 (← · 최근 앱에서 밀기) +/// [RunSettings.inBackground] 를 켜면 실시간 동기화를 지켜보는 동안에도 띄워 두고, 앱을 닫아도 (← · 최근 앱에서 밀기) /// Dart 엔진을 살려 둔다 (MainActivity.setBackground). class AndroidKeepAlive { static const _ch = MethodChannel('jj_mkvmaker/android'); @@ -72,7 +72,7 @@ class AndroidKeepAlive { _textsKey = textsKey; } catch (_) {} } - final bg = c.settings.runInBackground; + final bg = c.settings.run.inBackground; if (bg != _background) { _background = bg; try { diff --git a/lib/ui/copy_sync_settings.dart b/lib/ui/copy_sync_settings.dart index d0aab86..76ada1c 100644 --- a/lib/ui/copy_sync_settings.dart +++ b/lib/ui/copy_sync_settings.dart @@ -510,15 +510,15 @@ class LiveSyncRunOptions extends StatelessWidget { ], onChanged: (v) => c.updateSettings((x) => x.copy.liveSyncOnStart = v!), ); - void setBackground(bool? v) => c.updateSettings((x) => x.backgroundRun = v ?? false); + void setBackground(bool? v) => c.updateSettings((x) => x.run.backgroundRun = v ?? false); if (dense) { return Wrap(spacing: 12, runSpacing: 4, crossAxisAlignment: WrapCrossAlignment.center, children: [ Tooltip( message: backgroundHelp(), child: InkWell( - onTap: () => setBackground(!s.backgroundRun), + onTap: () => setBackground(!s.run.backgroundRun), child: Row(mainAxisSize: MainAxisSize.min, children: [ - Checkbox(value: s.backgroundRun, onChanged: setBackground), + Checkbox(value: s.run.backgroundRun, onChanged: setBackground), Text(tr('백그라운드로 실행'), style: const TextStyle(fontSize: 13)), ]), ), @@ -533,7 +533,7 @@ class LiveSyncRunOptions extends StatelessWidget { return Column(children: [ CheckboxListTile( dense: true, - value: s.backgroundRun, + value: s.run.backgroundRun, onChanged: setBackground, title: Text(tr('백그라운드로 실행')), subtitle: Text(backgroundHelp()), diff --git a/lib/ui/exit_guard.dart b/lib/ui/exit_guard.dart index bfc03e0..132cec8 100644 --- a/lib/ui/exit_guard.dart +++ b/lib/ui/exit_guard.dart @@ -61,7 +61,7 @@ class ExitGuard extends StatelessWidget { ]; } - bool get _guard => (Platform.isAndroid || forceAndroid) && !c.settings.runInBackground && _running().isNotEmpty; + bool get _guard => (Platform.isAndroid || forceAndroid) && !c.settings.run.inBackground && _running().isNotEmpty; @override Widget build(BuildContext context) => ListenableBuilder( diff --git a/lib/ui/settings/app_sections.dart b/lib/ui/settings/app_sections.dart index 554f2bf..48a6e8c 100644 --- a/lib/ui/settings/app_sections.dart +++ b/lib/ui/settings/app_sections.dart @@ -32,7 +32,7 @@ class _RunSettingsSectionState extends State { AppController get c => widget.c; bool get desk => widget.desk; - late final _hotkey = TextEditingController(text: c.settings.showHotkey); + late final _hotkey = TextEditingController(text: c.settings.run.showHotkey); String? _hotkeyError; @override @@ -49,7 +49,7 @@ class _RunSettingsSectionState extends State { } final ok = await c.services.shell.setHotkey(text); setState(() => _hotkeyError = ok ? null : tr('이 단축키를 등록할 수 없습니다 (다른 프로그램이 사용 중일 수 있음)')); - if (ok) await c.updateSettings((s) => s.showHotkey = text); + if (ok) await c.updateSettings((s) => s.run.showHotkey = text); } @override @@ -58,9 +58,9 @@ class _RunSettingsSectionState extends State { return Column(crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ if (desk) SwitchListTile( - value: s.startMenuShortcut, + value: s.run.startMenuShortcut, onChanged: (v) async { - await c.updateSettings((x) => x.startMenuShortcut = v); + await c.updateSettings((x) => x.run.startMenuShortcut = v); if (v) { final ok = await StartMenu.ensure(); final sh = c.services.shell; @@ -78,21 +78,21 @@ class _RunSettingsSectionState extends State { ), SettingTile( title: Text(tr('종료 (창 닫기 ✕ · 종료 버튼) 를 누르면')), - subtitle: Text(trf('백그라운드: 창만 숨기고 다운로드 · 변환은 계속합니다. {0} 또는 트레이 아이콘으로 다시 엽니다.\n' '완전히 끝내려면 트레이 아이콘 오른쪽 클릭 > 종료 (어느 설정이든 항상 종료)', [s.showHotkey])), + subtitle: Text(trf('백그라운드: 창만 숨기고 다운로드 · 변환은 계속합니다. {0} 또는 트레이 아이콘으로 다시 엽니다.\n' '완전히 끝내려면 트레이 아이콘 오른쪽 클릭 > 종료 (어느 설정이든 항상 종료)', [s.run.showHotkey])), isThreeLine: true, trailing: DropdownButton( - value: s.closeAction, + value: s.run.closeAction, items: [ DropdownMenuItem(value: 'background', child: Text(tr('백그라운드로 계속 (기본)'))), DropdownMenuItem(value: 'quit', child: Text(tr('다운로드 · 변환도 모두 종료 (묻지 않음)'))), DropdownMenuItem(value: 'ask', child: Text(tr('매번 고르기'))), ], - onChanged: (v) => c.updateSettings((x) => x.closeAction = v!), + onChanged: (v) => c.updateSettings((x) => x.run.closeAction = v!), ), ), SwitchListTile( - value: s.minimizeToTray, - onChanged: (v) => c.updateSettings((x) => x.minimizeToTray = v), + value: s.run.minimizeToTray, + onChanged: (v) => c.updateSettings((x) => x.run.minimizeToTray = v), title: Text(tr('최소화하면 트레이로 (백그라운드 실행)')), subtitle: Text(tr('트레이 아이콘을 누르거나 아래 단축키로 다시 엽니다')), ), diff --git a/lib/ui/settings/general_sections.dart b/lib/ui/settings/general_sections.dart index 5fc47f6..b7e5345 100644 --- a/lib/ui/settings/general_sections.dart +++ b/lib/ui/settings/general_sections.dart @@ -68,8 +68,8 @@ class _GeneralSettingsSectionState extends State { ]), ), SwitchListTile( - value: s.autoCheckUpdates, - onChanged: (v) => c.updateSettings((x) => x.autoCheckUpdates = v), + value: s.run.autoCheckUpdates, + onChanged: (v) => c.updateSettings((x) => x.run.autoCheckUpdates = v), title: Text(tr('시작할 때 새 버전 확인 (하루 한 번)')), ), ], diff --git a/lib/ui/update/update_install.dart b/lib/ui/update/update_install.dart index 7280f95..f33dfa8 100644 --- a/lib/ui/update/update_install.dart +++ b/lib/ui/update/update_install.dart @@ -56,7 +56,7 @@ Future saveVersionSnapshot(AppController c, String current) async { final snap = VersionSnapshot.instance; if (snap == null) return; try { - await c.updateSettings((x) => x.lastRunVersion = current); // 보관본이 "이 버전이 쓴 설정" 이 되게 + await c.updateSettings((x) => x.run.lastRunVersion = current); // 보관본이 "이 버전이 쓴 설정" 이 되게 await snap.save(current); c.note(trf('지금 버전 (v{0}) 의 설정을 보관했습니다: {1}', [current, snap.dirOf(current)])); } catch (e) { @@ -219,7 +219,7 @@ Future androidRollback(BuildContext context, AppController c, Updater up, probe = p.dirname(probe); } final free = (await diskSpace(probe))?.$1; - final setting = c.settings.rollbackKeepModels; + final setting = c.settings.run.rollbackKeepModels; var keep = modelSize > 0 && (setting == 'on' || (setting == '' && free != null && free >= modelSize * 2)); var keepTouched = false; // 옮겨 둔 모델을 되살리는 기능은 이 판 다음부터 들어 있다 (설정 되살리기는 v2026.10.08_001 부터) @@ -292,7 +292,7 @@ Future androidRollback(BuildContext context, AppController c, Updater up, ); if (ok != true || !context.mounted) return; // 체크를 바꿨으면 이제 설정에 (자동이던 것은 건드리지 않으면 자동 그대로) - if (keepTouched) await c.updateSettings((x) => x.rollbackKeepModels = keep ? 'on' : 'off'); + if (keepTouched) await c.updateSettings((x) => x.run.rollbackKeepModels = keep ? 'on' : 'off'); if (!context.mounted) return; // 사라지는 것이 있으니 한 번 더 (비밀번호 · 로그인은 늘 사라진다) final sure = await showDialog( diff --git a/lib/ui/update_dialog.dart b/lib/ui/update_dialog.dart index 59cf2f8..38403bb 100644 --- a/lib/ui/update_dialog.dart +++ b/lib/ui/update_dialog.dart @@ -30,8 +30,8 @@ Future checkForUpdate(BuildContext context, AppController c, {bool manual return; } if (!manual && - (!s.autoCheckUpdates || - !updateCheckDue(s.lastUpdateCheck, DateTime.now(), checkedBy: s.lastUpdateCheckVersion, current: current))) { + (!s.run.autoCheckUpdates || + !updateCheckDue(s.run.lastUpdateCheck, DateTime.now(), checkedBy: s.run.lastUpdateCheckVersion, current: current))) { return; } @@ -52,15 +52,15 @@ Future checkForUpdate(BuildContext context, AppController c, {bool manual return; } await c.updateSettings((x) => x - ..lastUpdateCheck = DateTime.now().toIso8601String() - ..lastUpdateCheckVersion = current); + ..run.lastUpdateCheck = DateTime.now().toIso8601String() + ..run.lastUpdateCheckVersion = current); // 작업 기록에 남긴다 (업데이트가 안 될 때 원인을 찾을 수 있게) c.note(trf('새 버전 확인: 지금 v{0} · 최신 v{1}', [current, r?.version ?? '-'])); if (r == null || !r.isNewerThan(current)) { if (manual) snack(trf('최신 버전입니다 (v{0})', [current])); return; } - if (!manual && s.skippedVersion == r.version) return; + if (!manual && s.run.skippedVersion == r.version) return; onResult?.call(trf('새 버전 v{0} 이 있습니다', [r.version])); if (!context.mounted) return; @@ -97,7 +97,7 @@ Future checkForUpdate(BuildContext context, AppController c, {bool manual if (!context.mounted) return; switch (choice) { case 'skip': - await c.updateSettings((x) => x.skippedVersion = r!.version); + await c.updateSettings((x) => x.run.skippedVersion = r!.version); case 'versions': await chooseVersion(context, c); case 'update': diff --git a/lib/ui/version_restore.dart b/lib/ui/version_restore.dart index 8135320..7a8f247 100644 --- a/lib/ui/version_restore.dart +++ b/lib/ui/version_restore.dart @@ -50,11 +50,11 @@ Future checkVersionRestore( message = trf('앱을 새로 설치해 설정이 없습니다. 저장해 둔 v{0} 의 설정 (동영상 목록 · 즐겨찾기 포함) 을 되살릴까요?\n' '(자막 API 키 · 비밀번호는 저장하지 않았으니 다시 넣어 주세요)', [p.basename(from)]); } - } else if (snap.shouldOffer(current, c.settings.lastRunVersion)) { + } else if (snap.shouldOffer(current, c.settings.run.lastRunVersion)) { from = snap.dirOf(current); message = trf('다른 버전 ({0}) 을 쓰다가 v{1} 로 돌아왔습니다. 그 버전은 이 버전의 설정 일부를 모를 수 있습니다.\n' 'v{1} 를 마지막으로 쓸 때 보관해 둔 설정 (동영상 목록 · 즐겨찾기 포함) 으로 되돌릴까요?', - [c.settings.lastRunVersion.isEmpty ? tr('예전 버전') : 'v${c.settings.lastRunVersion}', current]); + [c.settings.run.lastRunVersion.isEmpty ? tr('예전 버전') : 'v${c.settings.run.lastRunVersion}', current]); } if (from != null && message != null && context.mounted) { final ok = await showDialog( @@ -80,7 +80,7 @@ Future checkVersionRestore( } } } - if (c.settings.lastRunVersion != current) await c.updateSettings((x) => x.lastRunVersion = current); + if (c.settings.run.lastRunVersion != current) await c.updateSettings((x) => x.run.lastRunVersion = current); } typedef SettingsSummary = ({int videos, List servers, DateTime? saved}); diff --git a/test/decisions_test.dart b/test/decisions_test.dart index 891381f..360828b 100644 --- a/test/decisions_test.dart +++ b/test/decisions_test.dart @@ -27,21 +27,21 @@ void main() { test('기본값 = 정한 답 · 저장 · 읽기', () { final s = AppSettings(); expect([ - s.startMenuShortcut, s.copy.syncStopToast, s.swipeWrap, s.migrationNotice, s.allowInnerToOuter, + s.run.startMenuShortcut, s.copy.syncStopToast, s.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.davExternalOpen, 'ask'); // 이미 있던 설정의 기본값도 결정대로 - expect([s.browser.pauseOnLeave, s.autoCheckUpdates, s.browser.adSkip, s.browser.adHide, s.runInBackground, s.swipeNav], + expect([s.browser.pauseOnLeave, s.run.autoCheckUpdates, s.browser.adSkip, s.browser.adHide, s.run.inBackground, s.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]); // 바꾼 값은 남는다 final b = AppSettings.fromJson((AppSettings() - ..startMenuShortcut = false + ..run.startMenuShortcut = false ..copy.syncStopToast = false ..swipeWrap = false ..migrationNotice = false @@ -52,7 +52,7 @@ void main() { ..tripleTapAction = 'none' ..davExternalOpen = 'url') .toJson()); - expect([b.startMenuShortcut, b.copy.syncStopToast, b.swipeWrap, b.migrationNotice, b.allowInnerToOuter, b.rememberPasswords, + expect([b.run.startMenuShortcut, b.copy.syncStopToast, b.swipeWrap, b.migrationNotice, b.allowInnerToOuter, b.rememberPasswords, b.recycleOnDelete], everyElement(isFalse)); expect([b.cleanupPrechecked, b.tripleTapAction, b.davExternalOpen], [['logs'], 'none', 'url']); }); diff --git a/test/download_test.dart b/test/download_test.dart index ba0e5c9..e26be3a 100644 --- a/test/download_test.dart +++ b/test/download_test.dart @@ -142,13 +142,13 @@ https://example.com/page.html ..mkvOutputRoot = r'D:\out' ..download.root = r'D:\dl' ..download.clipboardWatch = false - ..showHotkey = 'Alt+F9' + ..run.showHotkey = 'Alt+F9' ..askAiOptions = false ..encode = const EncodeSettings(codec: VideoCodecChoice.vp9, resolution: ResolutionChoice.p720) ..aiTargets = ['ko', 'fr']; await store.save(s); final l = await store.load(); - expect([l.mkvOutputRoot, l.download.root, l.download.clipboardWatch, l.showHotkey, l.askAiOptions], + expect([l.mkvOutputRoot, l.download.root, l.download.clipboardWatch, l.run.showHotkey, l.askAiOptions], [r'D:\out', r'D:\dl', false, 'Alt+F9', false]); expect(l.encode.codec, VideoCodecChoice.vp9); expect(l.encode.resolution, ResolutionChoice.p720); diff --git a/test/exit_guard_test.dart b/test/exit_guard_test.dart index 4273865..63f6712 100644 --- a/test/exit_guard_test.dart +++ b/test/exit_guard_test.dart @@ -12,7 +12,7 @@ void main() { Future pump(WidgetTester tester, {required bool background, required bool busy}) async { final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); - c.settings.runInBackground = background; + c.settings.run.inBackground = background; c.busy = busy; c.currentJob = busy ? 'MKV 만들기 3/10' : null; final nav = GlobalKey(); diff --git a/test/model_backup_test.dart b/test/model_backup_test.dart index a5c2dce..58423b1 100644 --- a/test/model_backup_test.dart +++ b/test/model_backup_test.dart @@ -75,9 +75,9 @@ void main() { }); test('설정: 기본은 자동 (남은 공간으로), 고른 값은 남는다', () { - expect(AppSettings().rollbackKeepModels, ''); - final b = AppSettings.fromJson((AppSettings()..rollbackKeepModels = 'off').toJson()); - expect(b.rollbackKeepModels, 'off'); - expect(AppSettings.fromJson({'rollbackKeepModels': 'weird'}).rollbackKeepModels, ''); + expect(AppSettings().run.rollbackKeepModels, ''); + final b = AppSettings.fromJson((AppSettings()..run.rollbackKeepModels = 'off').toJson()); + expect(b.run.rollbackKeepModels, 'off'); + expect(AppSettings.fromJson({'rollbackKeepModels': 'weird'}).run.rollbackKeepModels, ''); }); } diff --git a/test/monitor_test.dart b/test/monitor_test.dart index 13d14bb..0638bb1 100644 --- a/test/monitor_test.dart +++ b/test/monitor_test.dart @@ -190,21 +190,21 @@ void main() { group('백그라운드 · 다시 켤 때 동기화', () { test('설정 저장 · 읽기, Windows 는 종료 동작과 같은 값', () { final s = AppSettings() - ..runInBackground = true + ..run.inBackground = true ..copy.liveSyncOnStart = 'ask'; final r = AppSettings.fromJson(s.toJson()); - expect([r.runInBackground, r.copy.liveSyncOnStart], [true, 'ask']); + expect([r.run.inBackground, r.copy.liveSyncOnStart], [true, 'ask']); expect(AppSettings.fromJson({'liveSyncOnStart': 'x'}).copy.liveSyncOnStart, 'auto'); // 사용자 결정 (10/8): Android 백그라운드로 실행은 기본 켜짐. 사용자가 끈 값 (새 형식) 은 그대로 - expect(AppSettings.fromJson({}).runInBackground, isTrue); - expect(AppSettings.fromJson({'runInBackground': false, 'backgroundV2': true}).runInBackground, isFalse); - final w = AppSettings()..backgroundRun = false; + expect(AppSettings.fromJson({}).run.inBackground, isTrue); + expect(AppSettings.fromJson({'runInBackground': false, 'backgroundV2': true}).run.inBackground, isFalse); + final w = AppSettings()..run.backgroundRun = false; if (Platform.isWindows) { - expect(w.closeAction, 'quit'); - w.backgroundRun = true; - expect([w.closeAction, w.backgroundRun], ['background', true]); + expect(w.run.closeAction, 'quit'); + w.run.backgroundRun = true; + expect([w.run.closeAction, w.run.backgroundRun], ['background', true]); } else { - expect(w.runInBackground, isFalse); + expect(w.run.inBackground, isFalse); } }); diff --git a/test/player_test.dart b/test/player_test.dart index bfffaf2..6bb357a 100644 --- a/test/player_test.dart +++ b/test/player_test.dart @@ -441,9 +441,9 @@ void main() { }); testWidgets('종료를 누르면: 설정 기본값 · 고르기 창 (백그라운드로 / 모두 종료 / 묻지 않기)', (tester) async { - expect(AppSettings().closeAction, 'background'); - expect(AppSettings.fromJson((AppSettings()..closeAction = 'quit').toJson()).closeAction, 'quit'); - expect(AppSettings.fromJson({'closeAction': 'x'}).closeAction, 'background'); + expect(AppSettings().run.closeAction, 'background'); + expect(AppSettings.fromJson((AppSettings()..run.closeAction = 'quit').toJson()).run.closeAction, 'quit'); + expect(AppSettings.fromJson({'closeAction': 'x'}).run.closeAction, 'background'); late BuildContext ctx; await tester.pumpWidget(MaterialApp(home: Builder(builder: (x) { diff --git a/test/update_modules_test.dart b/test/update_modules_test.dart index baa7f79..8f34785 100644 --- a/test/update_modules_test.dart +++ b/test/update_modules_test.dart @@ -66,9 +66,9 @@ void main() { VersionSnapshot.instance = null; addTearDown(() => VersionSnapshot.instance = was); final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); - final before = c.settings.lastRunVersion; + final before = c.settings.run.lastRunVersion; await saveVersionSnapshot(c, '9.9.9'); - expect(c.settings.lastRunVersion, before); + expect(c.settings.run.lastRunVersion, before); }); testWidgets('update_dialog (버전 고르기): 바뀐 내용이 비면 "(설명 없음)" · 표는 글자 그대로', (t) async { diff --git a/test/update_ui_test.dart b/test/update_ui_test.dart index f4dead9..bafcec7 100644 --- a/test/update_ui_test.dart +++ b/test/update_ui_test.dart @@ -182,8 +182,8 @@ void main() { await open(tester); await tester.tap(find.text('이 버전 건너뛰기')); await tester.pumpAndSettle(); - expect(c.settings.skippedVersion, '1.1.0'); - c.settings.lastUpdateCheck = ''; + expect(c.settings.run.skippedVersion, '1.1.0'); + c.settings.run.lastUpdateCheck = ''; await open(tester, manual: false); expect(find.text('새 버전 v1.1.0'), findsNothing); }); @@ -337,7 +337,7 @@ void main() { // 체크를 바꿔도 [취소] 하면 설정은 그대로 (자동) await tester.tap(find.text('취소')); await tester.pumpAndSettle(); - expect(c.settings.rollbackKeepModels, ''); + expect(c.settings.run.rollbackKeepModels, ''); await openPicker(tester); await tester.tap(find.text('v1.0.0').first); await tester.pumpAndSettle(); @@ -348,10 +348,10 @@ void main() { await tester.pumpAndSettle(); await tester.tap(find.byType(Checkbox)); await tester.pumpAndSettle(); - expect(c.settings.rollbackKeepModels, ''); + expect(c.settings.run.rollbackKeepModels, ''); await tester.tap(find.text('저장하고 계속')); await tester.pumpAndSettle(); - expect(c.settings.rollbackKeepModels, 'on'); + expect(c.settings.run.rollbackKeepModels, 'on'); await tester.tap(find.text('되돌리기 계속')); await settleIo(tester, until: () => find.text('저장했습니다').evaluate().isNotEmpty); expect(find.text('저장했습니다'), findsOneWidget);