import 'dart:async'; import 'package:flutter/material.dart'; import '../../app/app_controller.dart'; import '../../services/app_shell.dart'; import '../setup_dialog.dart'; import '../update_dialog.dart'; import '../../platform/windows/desktop_shell.dart' show DesktopShell; import '../app_actions.dart'; import '../cleanup_dialog.dart'; import '../theme.dart'; import '../../l10n/tr.dart'; import '../setting_tile.dart'; import '../license_texts.dart'; import '../jj_chip.dart'; import '../../core/picked_list.dart'; // 약속 (설정 B): 환경 설정의 분야 - 실행 · 종료 · 저장 공간 정리 · 프로그램 정보. 바꾸면 바로 설정에 저장 (c.updateSettings) /// 환경 설정 > 실행 · 종료 class RunSettingsSection extends StatefulWidget { const RunSettingsSection({super.key, required this.c, required this.desk}); final AppController c; /// PC (Android 가 아님) - 창 · 트레이 · 탐색기 · 다운로드 · 내장 Chrome 설정 final bool desk; @override State createState() => _RunSettingsSectionState(); } class _RunSettingsSectionState extends State { AppController get c => widget.c; bool get desk => widget.desk; late final _hotkey = TextEditingController(text: c.settings.run.showHotkey); String? _hotkeyError; @override void dispose() { _hotkey.dispose(); super.dispose(); } Future _applyHotkey() async { final text = _hotkey.text.trim(); if (parseHotkey(text) == null) { setState(() => _hotkeyError = tr('예: Ctrl+Shift+X (Ctrl·Shift·Alt·Win + 영문자·숫자·F1~F12)')); return; } final ok = await c.services.shell.setHotkey(text); setState(() => _hotkeyError = ok ? null : tr('이 단축키를 등록할 수 없습니다 (다른 프로그램이 사용 중일 수 있음)')); if (ok) await c.updateSettings((s) => s.run.showHotkey = text); } @override Widget build(BuildContext context) { final s = c.settings; return Column(crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ if (desk) SwitchListTile( value: s.run.startMenuShortcut, onChanged: (v) async { await c.updateSettings((x) => x.run.startMenuShortcut = v); if (v) { final ok = await c.services.startMenu.ensure(); final sh = c.services.shell; if (sh is DesktopShell) sh.toastAppReady = ok; } else { await c.services.startMenu.remove(); final sh = c.services.shell; if (sh is DesktopShell) sh.toastAppReady = false; } }, title: Text(tr('시작 메뉴에 등록')), subtitle: Text(c.services.startMenu.userAreaLocked() ? tr('시험용 데이터 폴더 (JJ_MKVMAKER_DATA) 로 켜져 시작 메뉴 바로 가기 · jjmkvmaker:// 연결은 건드리지 않습니다') : tr('알림이 JJ_MKVMaker 이름으로 보이고, 누르면 앱이 열립니다 (관리자 권한 없이 지금 사용자에게만)')), ), SettingTile( title: Text(tr('종료 (창 닫기 ✕ · 종료 버튼) 를 누르면')), subtitle: Text(trf('백그라운드: 창만 숨기고 다운로드 · 변환은 계속합니다. {0} 또는 트레이 아이콘으로 다시 엽니다.\n' '완전히 끝내려면 트레이 아이콘 오른쪽 클릭 > 종료 (어느 설정이든 항상 종료)', [s.run.showHotkey])), isThreeLine: true, trailing: DropdownButton( 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.run.closeAction = v!), ), ), SwitchListTile( value: s.run.minimizeToTray, onChanged: (v) => c.updateSettings((x) => x.run.minimizeToTray = v), title: Text(tr('최소화하면 트레이로 (백그라운드 실행)')), subtitle: Text(tr('트레이 아이콘을 누르거나 아래 단축키로 다시 엽니다')), ), SettingTile( title: Text(tr('창 보이기 / 숨기기 단축키')), subtitle: _hotkeyError == null ? Text(tr('어디서나 누르면 JJ_MKVMaker 창이 나타납니다')) : Text(_hotkeyError!, style: TextStyle(color: JjColors.danger)), trailing: SizedBox( width: 260, child: Row(children: [ Expanded( child: TextField( controller: _hotkey, decoration: const InputDecoration(isDense: true, border: OutlineInputBorder()), onSubmitted: (_) => _applyHotkey(), ), ), const SizedBox(width: 8), OutlinedButton(onPressed: _applyHotkey, child: Text(tr('적용'))), ]), ), ), ]); } } /// 환경 설정 > 저장 공간 정리 class CleanupSettingsSection extends StatefulWidget { const CleanupSettingsSection({super.key, required this.c, required this.desk}); final AppController c; /// PC (Android 가 아님) - 창 · 트레이 · 탐색기 · 다운로드 · 내장 Chrome 설정 final bool desk; @override State createState() => _CleanupSettingsSectionState(); } class _CleanupSettingsSectionState extends State { AppController get c => widget.c; bool get desk => widget.desk; @override Widget build(BuildContext context) { final s = c.settings; return Column(crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ SettingTile( title: Text(tr('정리 창에서 미리 체크할 것')), subtitle: Wrap(spacing: 6, runSpacing: 4, children: [ for (final (id, label) in [ ('work', tr('작업 임시 파일')), ('download', tr('받다 만 다운로드')), ('models', tr('받다 만 AI 모델')), ('update', tr('업데이트 · 설치하고 남은 파일')), ('logs', tr('지난 작업 기록')), ]) JjChip( multi: true, label: label, selected: s.general.cleanupPrechecked.contains(id), onSelected: (on) => c.updateSettings((x) => x.general.cleanupPrechecked = withPicked(x.general.cleanupPrechecked, id, on)), ), ]), ), SettingTile( title: Text(tr('임시 파일 · 남은 조각 정리')), subtitle: Text(tr('작업하다 남은 임시 파일 · 받다 만 다운로드 (.part 등) · 받다 만 AI 모델 · ' '업데이트하고 남은 파일 · 지난 작업 기록을 찾아 지웁니다. ' '받은 동영상 · 만든 MKV · 자막은 지우지 않고, 진행 중인 작업 · 다운로드가 쓰는 것은 건너뜁니다')), isThreeLine: true, trailing: FilledButton.icon( onPressed: () => showCleanup(context, c, AppScope.maybeOf(context)?.downloads), icon: const Icon(Icons.cleaning_services_outlined, size: 18), label: Text(tr('정리…')), ), ), ]); } } /// 환경 설정 > 프로그램 정보 class AboutSettingsSection extends StatefulWidget { const AboutSettingsSection({super.key, required this.c, required this.desk}); final AppController c; /// PC (Android 가 아님) - 창 · 트레이 · 탐색기 · 다운로드 · 내장 Chrome 설정 final bool desk; @override State createState() => _AboutSettingsSectionState(); } class _AboutSettingsSectionState extends State { AppController get c => widget.c; bool get desk => widget.desk; @override Widget build(BuildContext context) { return Column(crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ // 164: 지금 버전과 [최신 버전 확인] (결과는 버튼 옆에) SettingTile( leading: Icon(Icons.info_outline, color: JjColors.accent), title: FutureBuilder( future: c.services.updater?.currentVersion() ?? Future.value(''), builder: (_, v) => Text(v.data == null || v.data!.isEmpty ? 'JJ_MKVMaker' : 'JJ_MKVMaker v${v.data}'), ), trailing: c.services.updater == null ? null : UpdateCheckButton(c: c), ), if (desk) ...[ SettingTile( title: Text(tr('필수 프로그램 점검')), subtitle: Text(tr('FFmpeg · yt-dlp · aria2 · Deno 가 없으면 내려받아 설치합니다')), trailing: OutlinedButton( onPressed: () => checkRequiredTools(context, c.services.shell, quietIfOk: false), child: Text(tr('점검')), ), ), ], SettingTile( title: Text(tr('오픈 소스 라이선스')), subtitle: Text(tr('포함된 구성 요소와 라이선스 (GPL · LGPL 원문 포함)')), // 64: Android 에서도 (앱 안에서 연다) trailing: Wrap(spacing: 6, runSpacing: 6, children: [ OutlinedButton( onPressed: () => showNoticesDocument(context), child: Text(tr('고지 문서')), ), OutlinedButton( onPressed: () => showLicensePage(context: context, applicationName: 'JJ_MKVMaker'), child: Text(tr('패키지 라이선스')), ), ]), ), ]); } }