설정 A 분야 9: 실행 · 종료 · 업데이트 (RunSettings) - 키 11 · backgroundRun 함께
- 도구 고침: setter 안의 ' closeAction = v' 줄을 선언으로 잡던 것 (타입은 글자로 시작 · 첫 선언만) - 문서 주석 [AppSettings.run.x] → [RunSettings.x] Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>modular
parent
7c6320165a
commit
056ca186d9
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<SettingsGroup> get groups => [explorer, network, tor, copy, aiImage, reader, download, browser];
|
||||
List<SettingsGroup> get groups => [explorer, network, tor, copy, aiImage, reader, download, browser, run];
|
||||
|
||||
/// [plainSecrets]: 안전 저장소를 쓸 수 없을 때만 (예전 파일에 있던 비밀 값을 잃지 않게) 예전처럼 파일에 둔다
|
||||
Map<String, Object?> 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<String, String>()
|
||||
..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: 예전 판을 쓰던 사람 (설정 파일이 있는데 이 항목이 없음) 에게 복사 · 이동이 받는 쪽의 더 새 파일을 건너뛰게 바뀐 것을 한 번
|
||||
|
|
|
|||
|
|
@ -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<String, Object?> 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<String, dynamic> 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? ?? '';
|
||||
}
|
||||
}
|
||||
|
|
@ -172,7 +172,7 @@ Future<void> main(List<String> args) async {
|
|||
Future<bool> 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<void> main(List<String> 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<void> main(List<String> 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<void> main(List<String> 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;
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ class _RunSettingsSectionState extends State<RunSettingsSection> {
|
|||
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<RunSettingsSection> {
|
|||
}
|
||||
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<RunSettingsSection> {
|
|||
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<RunSettingsSection> {
|
|||
),
|
||||
SettingTile(
|
||||
title: Text(tr('종료 (창 닫기 ✕ · 종료 버튼) 를 누르면')),
|
||||
subtitle: Text(trf('백그라운드: 창만 숨기고 다운로드 · 변환은 계속합니다. {0} 또는 트레이 아이콘으로 다시 엽니다.\n' '완전히 끝내려면 트레이 아이콘 오른쪽 클릭 > 종료 (어느 설정이든 항상 종료)', [s.showHotkey])),
|
||||
subtitle: Text(trf('백그라운드: 창만 숨기고 다운로드 · 변환은 계속합니다. {0} 또는 트레이 아이콘으로 다시 엽니다.\n' '완전히 끝내려면 트레이 아이콘 오른쪽 클릭 > 종료 (어느 설정이든 항상 종료)', [s.run.showHotkey])),
|
||||
isThreeLine: true,
|
||||
trailing: DropdownButton<String>(
|
||||
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('트레이 아이콘을 누르거나 아래 단축키로 다시 엽니다')),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -68,8 +68,8 @@ class _GeneralSettingsSectionState extends State<GeneralSettingsSection> {
|
|||
]),
|
||||
),
|
||||
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('시작할 때 새 버전 확인 (하루 한 번)')),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ Future<void> 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<void> 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<void> 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<bool>(
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ Future<void> 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<void> 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<void> 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':
|
||||
|
|
|
|||
|
|
@ -50,11 +50,11 @@ Future<void> 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<bool>(
|
||||
|
|
@ -80,7 +80,7 @@ Future<void> 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<String> servers, DateTime? saved});
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ void main() {
|
|||
|
||||
Future<AppController> 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<NavigatorState>();
|
||||
|
|
|
|||
|
|
@ -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, '');
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue