JJ_MKVMaker/lib/ui/settings/app_sections.dart

237 lines
11 KiB
Dart

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<RunSettingsSection> createState() => _RunSettingsSectionState();
}
class _RunSettingsSectionState extends State<RunSettingsSection> {
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<void> _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<String>(
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<CleanupSettingsSection> createState() => _CleanupSettingsSectionState();
}
class _CleanupSettingsSectionState extends State<CleanupSettingsSection> {
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<AboutSettingsSection> createState() => _AboutSettingsSectionState();
}
class _AboutSettingsSectionState extends State<AboutSettingsSection> {
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<String>(
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('패키지 라이선스')),
),
]),
),
]);
}
}