237 lines
11 KiB
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 '../../platform/windows/start_menu.dart';
|
|
import '../app_actions.dart';
|
|
import '../cleanup_dialog.dart';
|
|
import '../theme.dart';
|
|
import '../../l10n/tr.dart';
|
|
import '../setting_tile.dart';
|
|
import '../license_texts.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.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.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.startMenuShortcut,
|
|
onChanged: (v) async {
|
|
await c.updateSettings((x) => x.startMenuShortcut = v);
|
|
if (v) {
|
|
final ok = await StartMenu.ensure();
|
|
final sh = c.services.shell;
|
|
if (sh is DesktopShell) sh.toastAppReady = ok;
|
|
} else {
|
|
await StartMenu.remove();
|
|
final sh = c.services.shell;
|
|
if (sh is DesktopShell) sh.toastAppReady = false;
|
|
}
|
|
},
|
|
title: Text(tr('시작 메뉴에 등록')),
|
|
subtitle: Text(StartMenu.userAreaLocked()
|
|
? tr('시험용 데이터 폴더 (JJ_MKVMAKER_DATA) 로 켜져 시작 메뉴 바로 가기 · jjmkvmaker:// 연결은 건드리지 않습니다')
|
|
: tr('알림이 JJ_MKVMaker 이름으로 보이고, 누르면 앱이 열립니다 (관리자 권한 없이 지금 사용자에게만)')),
|
|
),
|
|
SettingTile(
|
|
title: Text(tr('종료 (창 닫기 ✕ · 종료 버튼) 를 누르면')),
|
|
subtitle: Text(trf('백그라운드: 창만 숨기고 다운로드 · 변환은 계속합니다. {0} 또는 트레이 아이콘으로 다시 엽니다.\n' '완전히 끝내려면 트레이 아이콘 오른쪽 클릭 > 종료 (어느 설정이든 항상 종료)', [s.showHotkey])),
|
|
isThreeLine: true,
|
|
trailing: DropdownButton<String>(
|
|
value: s.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!),
|
|
),
|
|
),
|
|
SwitchListTile(
|
|
value: s.minimizeToTray,
|
|
onChanged: (v) => c.updateSettings((x) => x.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('지난 작업 기록')),
|
|
])
|
|
FilterChip(
|
|
label: Text(label),
|
|
selected: s.cleanupPrechecked.contains(id),
|
|
onSelected: (on) => c.updateSettings((x) => x.cleanupPrechecked = [
|
|
for (final y in x.cleanupPrechecked) if (y != id) y,
|
|
if (on) id,
|
|
]),
|
|
),
|
|
]),
|
|
),
|
|
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('패키지 라이선스')),
|
|
),
|
|
]),
|
|
),
|
|
]);
|
|
}
|
|
}
|