import 'dart:convert'; import 'dart:io'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import '../core/download_detect.dart'; import '../core/encode_options.dart'; import '../core/playlist.dart'; /// 환경 설정 (settings.json 에 저장) class AppSettings { /// MKV·자막 저장 위치. null 이면 동영상이 있는 폴더 아래 jj_mkv String? mkvOutputRoot; /// 다운로드 위치 (아래에 jj_yt-dlp, jj_aria2 생성). null 이면 프로그램 폴더 String? downloadRoot; /// Ctrl+C 로 복사한 주소를 자동으로 다운로드 bool clipboardWatch = true; /// 최소화하면 작업 표시줄 대신 트레이로 bool minimizeToTray = true; /// 창 보이기 단축키 (예: Ctrl+Shift+X) String showHotkey = 'Ctrl+Shift+X'; /// AI 자막: 매번 설정 창을 띄울지 (끄면 마지막 설정으로 바로 시작) bool askAiOptions = true; /// OpenSubtitles (인터넷 자막 검색) - 키는 필수, 아이디·비밀번호는 선택 (받기 횟수 늘어남) String openSubtitlesKey = ''; String openSubtitlesUser = ''; String openSubtitlesPassword = ''; /// YouTube 받을 형식 · 화질 (기본: MP4 최고 화질) YtContainer ytContainer = YtContainer.mp4; YtQuality ytQuality = YtQuality.best; /// 재생목록 주소면 목록 전체를 받기 bool ytExpandPlaylists = true; /// YouTube 로봇 확인 대응: 쿠키를 가져올 브라우저 (빈 값 = 사용 안 함) 또는 cookies.txt 경로 String ytCookiesBrowser = ''; String ytCookiesFile = ''; /// 동시에 받는 다운로드 수 (0 = 무제한) int maxParallelDownloads = 3; /// 동시에 만드는 MKV 수 (0 = 무제한) int maxParallelJobs = 5; /// 메인 화면 아래 작업 기록 창 보이기 bool showLog = true; /// 시작할 때 새 버전 확인 (하루 한 번) bool autoCheckUpdates = true; String lastUpdateCheck = ''; /// "이 버전 건너뛰기" 한 버전 String skippedVersion = ''; /// 동영상 하나를 재생할 때 재생 목록 (기본: 같은 시리즈) PlaylistMode playlistMode = PlaylistMode.series; /// 확장자별 재생 프로그램 (없으면 내장 플레이어): 'system' 또는 실행 파일 경로 Map externalPlayers = {}; /// 마지막 인코딩 설정 EncodeSettings encode = const EncodeSettings(); /// 마지막 AI 자막 설정 String aiSource = 'und'; List aiTargets = ['ko', 'en', 'ja']; String aiWhisper = 'whisper-base'; AppSettings(); Map toJson() => { 'mkvOutputRoot': mkvOutputRoot, 'downloadRoot': downloadRoot, 'clipboardWatch': clipboardWatch, 'minimizeToTray': minimizeToTray, 'showHotkey': showHotkey, 'askAiOptions': askAiOptions, 'openSubtitlesKey': openSubtitlesKey, 'openSubtitlesUser': openSubtitlesUser, 'openSubtitlesPassword': openSubtitlesPassword, 'ytContainer': ytContainer.name, 'ytQuality': ytQuality.name, 'ytExpandPlaylists': ytExpandPlaylists, 'ytCookiesBrowser': ytCookiesBrowser, 'ytCookiesFile': ytCookiesFile, 'maxParallelDownloads': maxParallelDownloads, 'maxParallelJobs': maxParallelJobs, 'showLog': showLog, 'autoCheckUpdates': autoCheckUpdates, 'lastUpdateCheck': lastUpdateCheck, 'skippedVersion': skippedVersion, 'playlistMode': playlistMode.name, 'externalPlayers': externalPlayers, 'encode': { 'codec': encode.codec.name, 'resolution': encode.resolution.name, 'quality': encode.quality.name, }, 'aiSource': aiSource, 'aiTargets': aiTargets, 'aiWhisper': aiWhisper, }; factory AppSettings.fromJson(Map j) { T pick(List values, Object? name, T fallback) => values.firstWhere((v) => v.name == name, orElse: () => fallback); final e = (j['encode'] as Map?) ?? const {}; return AppSettings() ..mkvOutputRoot = j['mkvOutputRoot'] as String? ..downloadRoot = j['downloadRoot'] as String? ..clipboardWatch = j['clipboardWatch'] as bool? ?? true ..minimizeToTray = j['minimizeToTray'] as bool? ?? true ..showHotkey = j['showHotkey'] as String? ?? 'Ctrl+Shift+X' ..askAiOptions = j['askAiOptions'] as bool? ?? true ..openSubtitlesKey = j['openSubtitlesKey'] as String? ?? '' ..openSubtitlesUser = j['openSubtitlesUser'] as String? ?? '' ..openSubtitlesPassword = j['openSubtitlesPassword'] as String? ?? '' ..ytContainer = pick(YtContainer.values, j['ytContainer'], YtContainer.mp4) ..ytQuality = pick(YtQuality.values, j['ytQuality'], YtQuality.best) ..ytExpandPlaylists = j['ytExpandPlaylists'] as bool? ?? true ..ytCookiesBrowser = j['ytCookiesBrowser'] as String? ?? '' ..ytCookiesFile = j['ytCookiesFile'] as String? ?? '' ..maxParallelDownloads = (j['maxParallelDownloads'] as num?)?.toInt() ?? 3 ..maxParallelJobs = (j['maxParallelJobs'] as num?)?.toInt() ?? 5 ..showLog = j['showLog'] as bool? ?? true ..autoCheckUpdates = j['autoCheckUpdates'] as bool? ?? true ..lastUpdateCheck = j['lastUpdateCheck'] as String? ?? '' ..skippedVersion = j['skippedVersion'] as String? ?? '' ..playlistMode = pick(PlaylistMode.values, j['playlistMode'], PlaylistMode.series) ..externalPlayers = ((j['externalPlayers'] as Map?) ?? const {}).cast() ..encode = EncodeSettings( codec: pick(VideoCodecChoice.values, e['codec'], VideoCodecChoice.copy), resolution: pick(ResolutionChoice.values, e['resolution'], ResolutionChoice.original), quality: pick(QualityChoice.values, e['quality'], QualityChoice.normal), ) ..aiSource = j['aiSource'] as String? ?? 'und' ..aiTargets = (j['aiTargets'] as List?)?.cast() ?? ['ko', 'en', 'ja'] ..aiWhisper = j['aiWhisper'] as String? ?? 'whisper-base'; } /// 다운로드 기본 위치: 프로그램 폴더 (쓸 수 없으면 사용자 다운로드 폴더) String resolvedDownloadRoot() { if (downloadRoot != null && downloadRoot!.isNotEmpty) return downloadRoot!; final appDir = p.dirname(Platform.resolvedExecutable); try { final probe = File(p.join(appDir, '.jj_write_test')); probe.writeAsStringSync('x'); probe.deleteSync(); return appDir; } catch (_) { final home = Platform.environment['USERPROFILE'] ?? Platform.environment['HOME'] ?? appDir; return p.join(home, 'Downloads'); } } String get ytDlpDir => p.join(resolvedDownloadRoot(), 'jj_yt-dlp'); String get aria2Dir => p.join(resolvedDownloadRoot(), 'jj_aria2'); } /// 설정 파일 읽기·쓰기 class SettingsStore { final String? _path; SettingsStore([this._path]); Future _file() async => _path ?? p.join((await getApplicationSupportDirectory()).path, 'settings.json'); Future load() async { try { final f = File(await _file()); // 이전 이름(jj_capcut) 의 설정 파일이 있으면 옮겨 온다 final legacy = File(p.join(p.dirname(f.parent.path), 'jj_capcut', 'settings.json')); if (_path == null && !await f.exists() && await legacy.exists()) { await f.parent.create(recursive: true); await legacy.copy(f.path); } if (await f.exists()) { return AppSettings.fromJson(jsonDecode(await f.readAsString()) as Map); } } catch (_) {} return AppSettings(); } Future save(AppSettings s) async { final f = File(await _file()); await f.parent.create(recursive: true); await f.writeAsString(const JsonEncoder.withIndent(' ').convert(s.toJson())); } }