diff --git a/integration_test/magnet_real_test.dart b/integration_test/magnet_real_test.dart index 644cbfe..0e485ea 100644 --- a/integration_test/magnet_real_test.dart +++ b/integration_test/magnet_real_test.dart @@ -65,7 +65,7 @@ void main() { final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings - ..downloadRoot = dir.path + ..download.root = dir.path ..network.mode = forced ? 'custom' : 'system' ..network.primary = '8.8.8.8' ..network.secondary = '8.8.4.4'; diff --git a/integration_test/windows_cookie_shot_test.dart b/integration_test/windows_cookie_shot_test.dart index 10ba3d9..d119dbf 100644 --- a/integration_test/windows_cookie_shot_test.dart +++ b/integration_test/windows_cookie_shot_test.dart @@ -75,7 +75,7 @@ void main() { await settle(); await t.tap(find.text(tr('사용 안 함')).last); await settle(15); - expect(c.settings.ytCookiesBrowser, ''); + expect(c.settings.download.ytCookiesBrowser, ''); expect(cookie.existsSync(), isFalse, reason: '쓰지 않게 바꾸면 내보낸 쿠키 파일을 지움'); expect(find.text(tr('쿠키 파일 지우기')), findsNothing, reason: '파일이 없고 쓰지 않으면 안내도 숨김'); await shot('2_off'); diff --git a/integration_test/windows_dlcancel_shot_test.dart b/integration_test/windows_dlcancel_shot_test.dart index bfaf2fc..20a3080 100644 --- a/integration_test/windows_dlcancel_shot_test.dart +++ b/integration_test/windows_dlcancel_shot_test.dart @@ -58,7 +58,7 @@ void main() { final tmp = testTempDir('jj_dlcancel_'); final d = DownloadManager( backends: [_Backend(DownloadKind.video), _Backend(DownloadKind.torrent)], - settings: () => AppSettings()..downloadRoot = tmp.path, + settings: () => AppSettings()..download.root = tmp.path, readClipboard: () async => null, ); final shotKey = GlobalKey(); diff --git a/integration_test/windows_magnet_shot_test.dart b/integration_test/windows_magnet_shot_test.dart index 81b7565..f42bd5a 100644 --- a/integration_test/windows_magnet_shot_test.dart +++ b/integration_test/windows_magnet_shot_test.dart @@ -57,7 +57,7 @@ void main() { addTearDown(() => i18n.apply('ko', save: false)); final d = DownloadManager( backends: [_Quiet(DownloadKind.video), _Quiet(DownloadKind.torrent)], - settings: () => AppSettings()..downloadRoot = r'D:\dl', + settings: () => AppSettings()..download.root = r'D:\dl', readClipboard: () async => null, ); // ① 메타데이터를 받는 중 (dn 이름 · 움직이는 "준비 중…") ② 멈춤 (이유 후보 · [다시 시도]) ③ 이름이 없는 마그넷 diff --git a/lib/app/app_controller.dart b/lib/app/app_controller.dart index a7016ff..cca845c 100644 --- a/lib/app/app_controller.dart +++ b/lib/app/app_controller.dart @@ -94,7 +94,7 @@ class AppController extends ChangeNotifier { /// 다운로드 폴더가 있는 드라이브 (예: "M:\\"). 설정이 바뀔 때만 다시 알아낸다. String? _diskFor, _diskRoot; String _downloadDisk() { - final key = settings.downloadRoot ?? ''; + final key = settings.download.root ?? ''; if (_diskRoot == null || _diskFor != key) { _diskFor = key; try { diff --git a/lib/app/download_manager.dart b/lib/app/download_manager.dart index 41707b8..3c66d09 100644 --- a/lib/app/download_manager.dart +++ b/lib/app/download_manager.dart @@ -126,7 +126,7 @@ class DownloadManager extends ChangeNotifier { /// 클립보드가 바뀌었으면 주소를 찾아 다운로드 추가 Future> checkClipboard() async { - if (!settings().clipboardWatch) return const []; + if (!settings().download.clipboardWatch) return const []; final text = await _safeRead(); if (text == null || text == _lastClip) return const []; _lastClip = text; @@ -158,7 +158,7 @@ class DownloadManager extends ChangeNotifier { tasks.insert(0, t); _added.add(t); // YouTube 재생목록: 목록을 읽어 영상마다 한 줄씩 바꿔 넣는다 - if (link.kind == DownloadKind.video && s.ytExpandPlaylists && youtubePlaylistId(link.url) != null) { + if (link.kind == DownloadKind.video && s.download.ytExpandPlaylists && youtubePlaylistId(link.url) != null) { t ..expanding = true ..title = trf('재생목록 불러오는 중… {0}', [link.url]); @@ -270,7 +270,7 @@ class DownloadManager extends ChangeNotifier { /// 동시 다운로드 수 안에서 대기 중인 작업을 들어온 순서대로 시작 void _pump() { - final limit = settings().maxParallelDownloads; + final limit = settings().download.maxParallel; var running = tasks.where((t) => t.state == DownloadState.downloading).length; final waiting = tasks.where((t) => t.state == DownloadState.queued && !t.expanding).toList() ..sort((a, b) => int.parse(a.id).compareTo(int.parse(b.id))); diff --git a/lib/app/settings.dart b/lib/app/settings.dart index 1a414ef..4df1fc1 100644 --- a/lib/app/settings.dart +++ b/lib/app/settings.dart @@ -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/download_settings.dart'; import 'settings/reader_settings.dart'; import 'settings/ai_image_settings.dart'; import 'settings/copy_settings.dart'; @@ -25,6 +26,7 @@ import 'settings/explorer_settings.dart'; export 'settings/records.dart'; export 'settings/settings_group.dart'; export 'settings/settings_store.dart'; +export 'settings/download_settings.dart'; export 'settings/reader_settings.dart'; export 'settings/ai_image_settings.dart'; export 'settings/copy_settings.dart'; @@ -53,11 +55,7 @@ class AppSettings { /// 웹 브라우저 위에 다른 화면이 올라오거나 다른 화면으로 가면 페이지의 동영상 · 소리를 멈춤 (끄면 계속 들림) bool webPauseOnLeave = true; - /// 다운로드 위치 (아래에 jj_yt-dlp, jj_aria2 생성). null 이면 프로그램 폴더 - String? downloadRoot; - /// Ctrl+C 로 복사한 주소를 자동으로 다운로드 - bool clipboardWatch = true; /// 최소화하면 작업 표시줄 대신 트레이로 bool minimizeToTray = true; @@ -72,36 +70,18 @@ class AppSettings { /// AI 자막: 매번 설정 창을 띄울지 (끄면 마지막 설정으로 바로 시작) bool askAiOptions = true; - /// 다운로드가 끝난 동영상을 편집 목록(MKV 만들기)에 자동으로 추가 - bool addFinishedDownloads = true; - /// 마그넷 · 토렌트가 이 초 동안 메타데이터 · 피어를 못 받으면 그 줄에 이유 후보 · [다시 시도] (0 = 알리지 않음, 관리자 지시 10/20) - int torrentStallSeconds = 60; /// OpenSubtitles (인터넷 자막 검색) - 키는 필수, 아이디·비밀번호는 선택 (받기 횟수 늘어남) String openSubtitlesKey = ''; String openSubtitlesUser = ''; String openSubtitlesPassword = ''; - /// YouTube 받을 형식 · 화질 (기본: MP4 최고 화질) - YtContainer ytContainer = YtContainer.mp4; - YtQuality ytQuality = YtQuality.best; - /// 36: 같은 크기면 H.264 를 먼저 (휴대폰 · 태블릿은 AV1 · VP9 을 하드웨어로 못 푸는 일이 많음). 기본: Android 켜짐, PC 꺼짐 - bool ytPreferH264 = Platform.isAndroid; - /// 재생목록 주소면 목록 전체를 받기 - bool ytExpandPlaylists = true; - /// YouTube 로봇 확인 대응: 쿠키를 가져올 브라우저 (빈 값 = 사용 안 함) 또는 cookies.txt 경로 - String ytCookiesBrowser = internalBrowserCookies; // 기본: 앱 안 브라우저 로그인 사용 - /// 53: 앱 안 브라우저 로그인 쿠키를 넘길 사이트 (처음은 모두 - 28) - List loginCookieSites = [...loginCookieDomains]; - String ytCookiesFile = ''; - /// 동시에 받는 다운로드 수 (0 = 무제한) - int maxParallelDownloads = 3; /// 동시에 만드는 MKV 수 (0 = 무제한) int maxParallelJobs = 5; @@ -420,8 +400,11 @@ class AppSettings { /// 보기 화면 (그림 · 만화 · PDF · 문서) final reader = ReaderSettings(); + /// 다운로드 · YouTube (yt-dlp · aria2) + final download = DownloadSettings(); + /// 설정 분야들 (설정 A) - 저마다 자기 키를 읽고 쓴다. toJson · fromJson 은 이 목록을 돌며 합치기만 한다 - List get groups => [explorer, network, tor, copy, aiImage, reader]; + List get groups => [explorer, network, tor, copy, aiImage, reader, download]; /// [plainSecrets]: 안전 저장소를 쓸 수 없을 때만 (예전 파일에 있던 비밀 값을 잃지 않게) 예전처럼 파일에 둔다 Map toJson({bool plainSecrets = false}) { @@ -434,14 +417,10 @@ class AppSettings { 'youtubeAdSkip': youtubeAdSkip, 'youtubeAdHide': youtubeAdHide, 'webPauseOnLeave': webPauseOnLeave, - 'downloadRoot': downloadRoot, - 'clipboardWatch': clipboardWatch, 'minimizeToTray': minimizeToTray, 'closeAction': closeAction, 'showHotkey': showHotkey, 'askAiOptions': askAiOptions, - 'addFinishedDownloads': addFinishedDownloads, - 'torrentStallSeconds': torrentStallSeconds, // 비밀 값은 안전 저장소에 (54). 파일에는 저장해 두었는지만 'openSubtitlesSaved': { 'key': openSubtitlesKey.isNotEmpty, @@ -453,15 +432,7 @@ class AppSettings { 'openSubtitlesUser': openSubtitlesUser, 'openSubtitlesPassword': openSubtitlesPassword, }, - 'ytContainer': ytContainer.name, - 'ytQuality': ytQuality.name, - 'ytPreferH264': ytPreferH264, - 'ytExpandPlaylists': ytExpandPlaylists, - 'ytCookiesBrowser': ytCookiesBrowser, - 'loginCookieSites': loginCookieSites, 'cookieScopeV2': true, - 'ytCookiesFile': ytCookiesFile, - 'maxParallelDownloads': maxParallelDownloads, 'maxParallelJobs': maxParallelJobs, 'showLog': showLog, 'mkvCompactList': mkvCompactList, @@ -574,27 +545,13 @@ class AppSettings { ..youtubeAdSkip = j['youtubeAdSkip'] as bool? ?? true ..youtubeAdHide = j['youtubeAdHide'] as bool? ?? true ..webPauseOnLeave = j['webPauseOnLeave'] as bool? ?? true - ..downloadRoot = j['downloadRoot'] as String? - ..clipboardWatch = j['clipboardWatch'] as bool? ?? true ..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 - ..addFinishedDownloads = j['addFinishedDownloads'] as bool? ?? true - ..torrentStallSeconds = const [0, 30, 60, 120, 300].contains(j['torrentStallSeconds']) ? j['torrentStallSeconds'] as int : 60 ..openSubtitlesKey = j['openSubtitlesKey'] as String? ?? '' ..openSubtitlesUser = j['openSubtitlesUser'] as String? ?? '' ..openSubtitlesPassword = j['openSubtitlesPassword'] as String? ?? '' - ..ytContainer = pick(YtContainer.values, j['ytContainer'], YtContainer.mp4) - ..ytPreferH264 = j['ytPreferH264'] is bool ? j['ytPreferH264'] as bool : Platform.isAndroid - ..ytQuality = pick(YtQuality.values, j['ytQuality'], YtQuality.best) - ..ytExpandPlaylists = j['ytExpandPlaylists'] as bool? ?? true - ..ytCookiesBrowser = j['ytCookiesBrowser'] as String? ?? internalBrowserCookies - ..loginCookieSites = j['loginCookieSites'] is List - ? [for (final x in j['loginCookieSites'] as List) if (loginCookieDomains.contains(x)) '$x'] - : [...loginCookieDomains] - ..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 ..mkvCompactList = const ['auto', 'small', 'large'].contains(j['mkvCompactList']) ? j['mkvCompactList'] as String : 'auto' @@ -803,7 +760,7 @@ class AppSettings { /// 다운로드 기본 위치: 프로그램 폴더 (쓸 수 없으면 사용자 다운로드 폴더) String resolvedDownloadRoot() { - if (downloadRoot != null && downloadRoot!.isNotEmpty) return downloadRoot!; + if (download.root != null && download.root!.isNotEmpty) return download.root!; // Android: 프로그램 폴더가 없으므로 내장 저장소의 Download\JJ_MKVMaker if (Platform.isAndroid) return '/storage/emulated/0/Download/JJ_MKVMaker'; final appDir = p.dirname(Platform.resolvedExecutable); diff --git a/lib/app/settings/download_settings.dart b/lib/app/settings/download_settings.dart new file mode 100644 index 0000000..75454fb --- /dev/null +++ b/lib/app/settings/download_settings.dart @@ -0,0 +1,76 @@ +import 'dart:io'; + +import '../../core/download_detect.dart'; +import 'settings_group.dart'; + +/// 약속 (설정 A): 환경 설정 > 다운로드 · YouTube (yt-dlp · aria2) - 자기 키 (이름 · 처음 값 · 읽기 · 쓰기) 를 스스로 갖는다. +/// 설정 하나를 더할 때는 이 파일만 고친다. 저장 파일의 키 이름은 예전 그대로 (평평한 키) +class DownloadSettings implements SettingsGroup { + /// 다운로드 위치 (아래에 jj_yt-dlp, jj_aria2 생성). null 이면 프로그램 폴더 + String? root; + + /// Ctrl+C 로 복사한 주소를 자동으로 다운로드 + bool clipboardWatch = true; + + /// 다운로드가 끝난 동영상을 편집 목록(MKV 만들기)에 자동으로 추가 + bool addFinished = true; + + /// 마그넷 · 토렌트가 이 초 동안 메타데이터 · 피어를 못 받으면 그 줄에 이유 후보 · [다시 시도] (0 = 알리지 않음, 관리자 지시 10/20) + int torrentStallSeconds = 60; + + /// YouTube 받을 형식 · 화질 (기본: MP4 최고 화질) + YtContainer ytContainer = YtContainer.mp4; + + YtQuality ytQuality = YtQuality.best; + + /// 36: 같은 크기면 H.264 를 먼저 (휴대폰 · 태블릿은 AV1 · VP9 을 하드웨어로 못 푸는 일이 많음). 기본: Android 켜짐, PC 꺼짐 + bool ytPreferH264 = Platform.isAndroid; + + /// 재생목록 주소면 목록 전체를 받기 + bool ytExpandPlaylists = true; + + /// YouTube 로봇 확인 대응: 쿠키를 가져올 브라우저 (빈 값 = 사용 안 함) 또는 cookies.txt 경로 + String ytCookiesBrowser = internalBrowserCookies; // 기본: 앱 안 브라우저 로그인 사용 + + /// 53: 앱 안 브라우저 로그인 쿠키를 넘길 사이트 (처음은 모두 - 28) + List loginCookieSites = [...loginCookieDomains]; + + String ytCookiesFile = ''; + + /// 동시에 받는 다운로드 수 (0 = 무제한) + int maxParallel = 3; + + @override + void write(Map j, {required bool plainSecrets}) { + j['downloadRoot'] = root; + j['clipboardWatch'] = clipboardWatch; + j['addFinishedDownloads'] = addFinished; + j['torrentStallSeconds'] = torrentStallSeconds; + j['ytContainer'] = ytContainer.name; + j['ytQuality'] = ytQuality.name; + j['ytPreferH264'] = ytPreferH264; + j['ytExpandPlaylists'] = ytExpandPlaylists; + j['ytCookiesBrowser'] = ytCookiesBrowser; + j['loginCookieSites'] = loginCookieSites; + j['ytCookiesFile'] = ytCookiesFile; + j['maxParallelDownloads'] = maxParallel; + } + + @override + void read(Map j) { + root = j['downloadRoot'] as String?; + clipboardWatch = j['clipboardWatch'] as bool? ?? true; + addFinished = j['addFinishedDownloads'] as bool? ?? true; + torrentStallSeconds = const [0, 30, 60, 120, 300].contains(j['torrentStallSeconds']) ? j['torrentStallSeconds'] as int : 60; + ytContainer = pickEnum(YtContainer.values, j['ytContainer'], YtContainer.mp4); + ytPreferH264 = j['ytPreferH264'] is bool ? j['ytPreferH264'] as bool : Platform.isAndroid; + ytQuality = pickEnum(YtQuality.values, j['ytQuality'], YtQuality.best); + ytExpandPlaylists = j['ytExpandPlaylists'] as bool? ?? true; + ytCookiesBrowser = j['ytCookiesBrowser'] as String? ?? internalBrowserCookies; + loginCookieSites = j['loginCookieSites'] is List + ? [for (final x in j['loginCookieSites'] as List) if (loginCookieDomains.contains(x)) '$x'] + : [...loginCookieDomains]; + ytCookiesFile = j['ytCookiesFile'] as String? ?? ''; + maxParallel = (j['maxParallelDownloads'] as num?)?.toInt() ?? 3; + } +} diff --git a/lib/app/settings/settings_group.dart b/lib/app/settings/settings_group.dart index 0be6743..91613cd 100644 --- a/lib/app/settings/settings_group.dart +++ b/lib/app/settings/settings_group.dart @@ -8,3 +8,7 @@ abstract class SettingsGroup { /// 설정 파일 [j] 에서 읽는다 - 없거나 틀린 값은 처음 값 (또는 고친 값) void read(Map j); } + +/// 저장된 이름으로 열거형 값 고르기 (모르는 이름 · 없으면 [fallback]) - 분야들의 읽기가 같이 쓴다 +T pickEnum(List values, Object? name, T fallback) => + values.firstWhere((v) => v.name == name, orElse: () => fallback); diff --git a/lib/main.dart b/lib/main.dart index 9470c74..bbe1180 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -298,7 +298,7 @@ Future main(List args) async { downloads.addListener(() => services.shell.setTooltip(syncAlert.tooltip())); // 다 받은 동영상 → 편집 목록 (설정에서 끌 수 있음) downloads.onFinished.listen((t) async { - if (!controller.settings.addFinishedDownloads) return; + if (!controller.settings.download.addFinished) return; final files = DownloadManager.videoFilesOf(t); if (files.isEmpty) return; final n = await controller.addDownloaded(files); @@ -308,7 +308,7 @@ Future main(List args) async { showAddedToMkvNotice(messengerKey, navigatorKey, controller, title: t.title, files: files, added: n); }); downloads.setAutoAdd = (on) async { - await controller.updateSettings((x) => x.addFinishedDownloads = on); + await controller.updateSettings((x) => x.download.addFinished = on); downloads.refresh(); }; downloads.useSystemDns = () async { @@ -619,7 +619,7 @@ Future runAndroid(String dataDir) async { watchDnsFailure(messengerKey, controller); // 다 받은 동영상 → 편집 목록 (설정에서 끌 수 있음) downloads.onFinished.listen((t) async { - if (!controller.settings.addFinishedDownloads) return; + if (!controller.settings.download.addFinished) return; final files = DownloadManager.videoFilesOf(t); if (files.isEmpty) return; final n = await controller.addDownloaded(files); @@ -628,7 +628,7 @@ Future runAndroid(String dataDir) async { showAddedToMkvNotice(messengerKey, navigatorKey, controller, title: t.title, files: files, added: n); }); downloads.setAutoAdd = (on) async { - await controller.updateSettings((x) => x.addFinishedDownloads = on); + await controller.updateSettings((x) => x.download.addFinished = on); downloads.refresh(); }; downloads.useSystemDns = () async { diff --git a/lib/platform/android/android_download_tools.dart b/lib/platform/android/android_download_tools.dart index b7d8091..f770a1b 100644 --- a/lib/platform/android/android_download_tools.dart +++ b/lib/platform/android/android_download_tools.dart @@ -71,10 +71,10 @@ class AndroidDownloadTools { // --ffmpeg-location 은 파일 경로도 받는다 (libffmpeg.so = ffmpeg 실행 파일) ffmpegDir: t.ffmpeg, jsRuntime: 'quickjs:${t.quickjs}', - formatArgs: () => ytDlpFormatArgs(s().ytContainer, s().ytQuality, preferH264: s().ytPreferH264), + formatArgs: () => ytDlpFormatArgs(s().download.ytContainer, s().download.ytQuality, preferH264: s().download.ytPreferH264), cookieArgs: () => ytDlpCookieArgs( - browser: s().ytCookiesBrowser, - file: s().ytCookiesFile, + browser: s().download.ytCookiesBrowser, + file: s().download.ytCookiesFile, internalCookieFile: s().internalCookieFile, ), fallbackCookieArgs: () => @@ -84,7 +84,7 @@ class AndroidDownloadTools { _Deferred(DownloadKind.torrent, () async { final t = await ready(); return Aria2Backend(aria2c: t.aria2c, environment: t.environment, extraArgs: ['--ca-certificate=${t.cert}'], - stallAfter: () => Duration(seconds: s().torrentStallSeconds)); + stallAfter: () => Duration(seconds: s().download.torrentStallSeconds)); }), ]; } diff --git a/lib/services/platform_services.dart b/lib/services/platform_services.dart index 3ca7c5e..1071e85 100644 --- a/lib/services/platform_services.dart +++ b/lib/services/platform_services.dart @@ -97,17 +97,17 @@ class PlatformServices { createDownloadBackends: (s) => [ YtDlpBackend( ffmpegDir: ffmpegDir, - formatArgs: () => ytDlpFormatArgs(s().ytContainer, s().ytQuality, preferH264: s().ytPreferH264), + formatArgs: () => ytDlpFormatArgs(s().download.ytContainer, s().download.ytQuality, preferH264: s().download.ytPreferH264), cookieArgs: () => ytDlpCookieArgs( - browser: s().ytCookiesBrowser, - file: s().ytCookiesFile, + browser: s().download.ytCookiesBrowser, + file: s().download.ytCookiesFile, internalProfile: s().internalCookieProfile, internalCookieFile: s().internalCookieFile), // 고른 브라우저의 쿠키를 못 읽으면 앱 안 브라우저의 쿠키로 fallbackCookieArgs: () => ytDlpCookieArgs( browser: internalBrowserCookies, internalCookieFile: s().internalCookieFile), ), - Aria2Backend(stallAfter: () => Duration(seconds: s().torrentStallSeconds)), + Aria2Backend(stallAfter: () => Duration(seconds: s().download.torrentStallSeconds)), ], shell: DesktopShell(), updater: GitHubUpdater(), diff --git a/lib/ui/browser_page.dart b/lib/ui/browser_page.dart index 850b140..901d2d6 100644 --- a/lib/ui/browser_page.dart +++ b/lib/ui/browser_page.dart @@ -545,8 +545,8 @@ class _BrowserPageState extends State with RouteAware { /// 53: 쿠키를 쓰도록 정했을 때만 · 고른 사이트만 내보낸다 void _applyCookieExport() { final s = widget.c.settings; - CookieExport.enabled = s.ytCookiesBrowser == internalBrowserCookies; - CookieExport.sites = s.loginCookieSites; + CookieExport.enabled = s.download.ytCookiesBrowser == internalBrowserCookies; + CookieExport.sites = s.download.loginCookieSites; } /// 53: 쿠키 · 방문 기록 지우기 (확인 뒤). 다운로드용으로 내보낸 쿠키 파일도 지운다 diff --git a/lib/ui/downloads_page.dart b/lib/ui/downloads_page.dart index f883617..f6feae5 100644 --- a/lib/ui/downloads_page.dart +++ b/lib/ui/downloads_page.dart @@ -164,11 +164,11 @@ class _DownloadsPageState extends State { message: tr('다 받으면 MKV 만들기의 동영상 목록에 자동으로 넣고, 이 다운로드 목록에서는 뺍니다 (받은 파일은 그대로)'), child: InkWell( borderRadius: BorderRadius.circular(4), - onTap: () => d.setAutoAdd!(!d.settings().addFinishedDownloads), + onTap: () => d.setAutoAdd!(!d.settings().download.addFinished), child: Row(mainAxisSize: MainAxisSize.min, children: [ Checkbox( visualDensity: VisualDensity.compact, - value: d.settings().addFinishedDownloads, + value: d.settings().download.addFinished, onChanged: (v) => d.setAutoAdd!(v ?? false), ), Padding( diff --git a/lib/ui/settings/transfer_sections.dart b/lib/ui/settings/transfer_sections.dart index 9cf9de3..6769543 100644 --- a/lib/ui/settings/transfer_sections.dart +++ b/lib/ui/settings/transfer_sections.dart @@ -76,14 +76,14 @@ class _DownloadSettingsSectionState extends State { return Column(crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ if (desk) SwitchListTile( - value: s.clipboardWatch, - onChanged: (v) => c.updateSettings((x) => x.clipboardWatch = v), + value: s.download.clipboardWatch, + onChanged: (v) => c.updateSettings((x) => x.download.clipboardWatch = v), title: Text(tr('복사(Ctrl+C)한 주소 자동 다운로드')), subtitle: Text(tr('YouTube → yt-dlp, 마그넷 · .torrent → aria2 (완료 후 올려 주기 안 함)')), ), SwitchListTile( - value: s.addFinishedDownloads, - onChanged: (v) => c.updateSettings((x) => x.addFinishedDownloads = v), + value: s.download.addFinished, + onChanged: (v) => c.updateSettings((x) => x.download.addFinished = v), title: Text(tr('완료 시 자동 동영상 추가')), subtitle: Text(tr('다 받는 대로 MKV 만들기의 동영상 목록에 넣고, 다운로드 목록에서는 뺍니다 (다운로드 화면의 체크 상자와 같음)')), ), @@ -92,59 +92,59 @@ class _DownloadSettingsSectionState extends State { title: Text(tr('마그넷 · 토렌트가 시작되지 않으면 알리기')), subtitle: Text(tr('메타데이터나 피어를 이 시간 동안 못 받으면 그 줄에 이유 후보와 [다시 시도] 를 보입니다')), trailing: DropdownButton( - value: s.torrentStallSeconds, + value: s.download.torrentStallSeconds, items: [ for (final v in const [30, 60, 120, 300]) DropdownMenuItem(value: v, child: Text(trf('{0}초', [v]))), DropdownMenuItem(value: 0, child: Text(tr('알리지 않음'))), ], - onChanged: (v) => c.updateSettings((x) => x.torrentStallSeconds = v!), + onChanged: (v) => c.updateSettings((x) => x.download.torrentStallSeconds = v!), ), ), SettingTile( title: Text(tr('YouTube 받을 형식')), subtitle: Text(tr('기본: MP4. 음성만 받으면 MP3 · M4A 로 저장')), trailing: DropdownButton( - value: s.ytContainer, + value: s.download.ytContainer, items: [for (final v in YtContainer.values) DropdownMenuItem(value: v, child: Text(v.label))], - onChanged: (v) => c.updateSettings((x) => x.ytContainer = v!), + onChanged: (v) => c.updateSettings((x) => x.download.ytContainer = v!), ), ), SettingTile( - enabled: !s.ytContainer.audioOnly, + enabled: !s.download.ytContainer.audioOnly, title: Text(tr('YouTube 화질')), subtitle: Text(tr('고른 크기 이하에서 가장 좋은 화질 (영상에 없는 크기면 가까운 것)')), trailing: DropdownButton( - value: s.ytQuality, + value: s.download.ytQuality, items: [for (final v in YtQuality.values) DropdownMenuItem(value: v, child: Text(v.label))], - onChanged: s.ytContainer.audioOnly ? null : (v) => c.updateSettings((x) => x.ytQuality = v!), + onChanged: s.download.ytContainer.audioOnly ? null : (v) => c.updateSettings((x) => x.download.ytQuality = v!), ), ), // 36: 같은 크기면 H.264 먼저 (Android 기본 켜짐) SwitchListTile( - value: s.ytPreferH264, - onChanged: s.ytContainer.audioOnly ? null : (v) => c.updateSettings((x) => x.ytPreferH264 = v), + value: s.download.ytPreferH264, + onChanged: s.download.ytContainer.audioOnly ? null : (v) => c.updateSettings((x) => x.download.ytPreferH264 = v), title: Text(tr('같은 화질이면 H.264 먼저 받기')), subtitle: Text(tr('휴대폰 · 태블릿은 AV1 · VP9 을 하드웨어로 풀지 못해 끊기거나 배터리를 많이 쓰는 일이 있습니다. ' 'YouTube 의 H.264 는 1080p 까지라 더 큰 화질을 고르면 크기가 먼저입니다.')), ), SwitchListTile( - value: s.ytExpandPlaylists, - onChanged: (v) => c.updateSettings((x) => x.ytExpandPlaylists = v), + value: s.download.ytExpandPlaylists, + onChanged: (v) => c.updateSettings((x) => x.download.ytExpandPlaylists = v), title: Text(tr('재생목록 주소면 목록 전체 받기')), subtitle: Text(tr('영상마다 한 줄씩 보여 주고 jj_yt-dlp\\재생목록 이름\\ 에 저장 (믹스 목록 제외)')), ), SettingTile( title: Text(tr('YouTube 쿠키 (로봇 확인이 나올 때)')), - subtitle: Text(s.ytCookiesFile.isNotEmpty - ? 'cookies.txt: ${s.ytCookiesFile}' - : s.ytCookiesBrowser == internalBrowserCookies + subtitle: Text(s.download.ytCookiesFile.isNotEmpty + ? 'cookies.txt: ${s.download.ytCookiesFile}' + : s.download.ytCookiesBrowser == internalBrowserCookies ? tr('앱 안 브라우저의 로그인 사용 (브라우저 화면에서 YouTube 에 로그인하세요)') - : s.ytCookiesBrowser.isNotEmpty - ? trf('{0} 브라우저의 YouTube 로그인 쿠키 사용 (Chrome · Edge 는 브라우저를 닫아야 읽힐 수 있음)', [s.ytCookiesBrowser]) + : s.download.ytCookiesBrowser.isNotEmpty + ? trf('{0} 브라우저의 YouTube 로그인 쿠키 사용 (Chrome · Edge 는 브라우저를 닫아야 읽힐 수 있음)', [s.download.ytCookiesBrowser]) : tr('사용 안 함. "Sign in to confirm you\'re not a bot" 오류가 나오면 설정하세요')), trailing: Row(mainAxisSize: MainAxisSize.min, children: [ DropdownButton( - value: s.ytCookiesFile.isNotEmpty ? '*file' : s.ytCookiesBrowser, + value: s.download.ytCookiesFile.isNotEmpty ? '*file' : s.download.ytCookiesBrowser, items: [ DropdownMenuItem(value: '', child: Text(tr('사용 안 함'))), DropdownMenuItem(value: internalBrowserCookies, child: Text(tr('앱 안 브라우저 (권장)'))), @@ -162,16 +162,16 @@ class _DownloadSettingsSectionState extends State { allowedExtensions: const ['txt']); if (f?.path != null) { await c.updateSettings((x) => x - ..ytCookiesFile = f!.path! - ..ytCookiesBrowser = ''); + ..download.ytCookiesFile = f!.path! + ..download.ytCookiesBrowser = ''); } } else { await c.updateSettings((x) => x - ..ytCookiesBrowser = v ?? '' - ..ytCookiesFile = ''); + ..download.ytCookiesBrowser = v ?? '' + ..download.ytCookiesFile = ''); } // 131: 앱 안 브라우저 로그인을 더 쓰지 않으면 내보내 둔 평문 쿠키 파일을 지운다 - if (c.settings.ytCookiesBrowser != internalBrowserCookies) { + if (c.settings.download.ytCookiesBrowser != internalBrowserCookies) { await deleteExportedCookies(c.settings.webViewDataDir); if (mounted) setState(() {}); } @@ -181,7 +181,7 @@ class _DownloadSettingsSectionState extends State { ), // 53: 앱 안 브라우저 쿠키 - 어디에 어떻게 저장되는지 · 넘길 사이트 고르기 · 지우기 // 131: 쓰지 않게 바꿨어도 파일이 남아 있으면 지우는 버튼은 보인다 - if (s.ytCookiesBrowser == internalBrowserCookies || s.internalCookieFile.isNotEmpty) + if (s.download.ytCookiesBrowser == internalBrowserCookies || s.internalCookieFile.isNotEmpty) Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -195,9 +195,9 @@ class _DownloadSettingsSectionState extends State { for (final d in loginCookieDomains) FilterChip( label: Text(d), - selected: s.loginCookieSites.contains(d), - onSelected: (on) => c.updateSettings((x) => x.loginCookieSites = [ - for (final y in x.loginCookieSites) if (y != d) y, + selected: s.download.loginCookieSites.contains(d), + onSelected: (on) => c.updateSettings((x) => x.download.loginCookieSites = [ + for (final y in x.download.loginCookieSites) if (y != d) y, if (on) d, ]), ), @@ -220,19 +220,19 @@ class _DownloadSettingsSectionState extends State { title: Text(tr('동시 다운로드 수')), subtitle: Text(tr('나머지는 대기했다가 차례로 받습니다')), trailing: CountSelector( - value: s.maxParallelDownloads, - onChanged: (n) => c.updateSettings((x) => x.maxParallelDownloads = n), + value: s.download.maxParallel, + onChanged: (n) => c.updateSettings((x) => x.download.maxParallel = n), ), ), settingsFolderTile( title: tr('다운로드 (jj_yt-dlp · jj_aria2)'), - value: s.downloadRoot, + value: s.download.root, defaultText: trf('{0} (기본): {1}', [desk ? tr('프로그램 폴더') : tr('내장 저장소'), s.resolvedDownloadRoot()]), onPick: () async { final d = await pickFolder(context, tr('다운로드 위치'), s.resolvedDownloadRoot()); - if (d != null) await c.updateSettings((x) => x.downloadRoot = d); + if (d != null) await c.updateSettings((x) => x.download.root = d); }, - onReset: () => c.updateSettings((x) => x.downloadRoot = null), + onReset: () => c.updateSettings((x) => x.download.root = null), ), ]); } diff --git a/test/aria2_stall_test.dart b/test/aria2_stall_test.dart index 33c886a..761c4e3 100644 --- a/test/aria2_stall_test.dart +++ b/test/aria2_stall_test.dart @@ -54,9 +54,9 @@ void main() { }); test('설정: 시작되지 않으면 알리기 - 처음 60초, 모르는 값은 60', () { - expect(AppSettings().torrentStallSeconds, 60); - expect(AppSettings.fromJson({'torrentStallSeconds': 0}).torrentStallSeconds, 0); - expect(AppSettings.fromJson({'torrentStallSeconds': 7}).torrentStallSeconds, 60); + expect(AppSettings().download.torrentStallSeconds, 60); + expect(AppSettings.fromJson({'torrentStallSeconds': 0}).download.torrentStallSeconds, 0); + expect(AppSettings.fromJson({'torrentStallSeconds': 7}).download.torrentStallSeconds, 60); }); testWidgets('멈춘 마그넷 줄: "트래커 · 피어에 닿지 않습니다" · 이유 후보 · [다시 시도] (지금 것을 지우고 새로 넣음)', (tester) async { diff --git a/test/batch_test.dart b/test/batch_test.dart index f2f41de..ba11977 100644 --- a/test/batch_test.dart +++ b/test/batch_test.dart @@ -516,7 +516,7 @@ void main() { File(file).writeAsStringSync('x'); final d = DownloadManager( - backends: [_Backend()], settings: () => AppSettings()..downloadRoot = dir.path, readClipboard: () async => null); + backends: [_Backend()], settings: () => AppSettings()..download.root = dir.path, readClipboard: () async => null); final added = []; d.addToEditList = (files) async { added.addAll(files); @@ -552,11 +552,11 @@ void main() { tester.view.physicalSize = const Size(1500, 900); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); - final s = AppSettings()..addFinishedDownloads = false; + final s = AppSettings()..download.addFinished = false; final d = DownloadManager(backends: [_Backend()], settings: () => s, readClipboard: () async => null); d.addToEditList = (files) async => files.length; d.setAutoAdd = (on) async { - s.addFinishedDownloads = on; + s.download.addFinished = on; d.refresh(); }; final t = d.addPage('https://vimeo.com/1')!..title = '영상'; @@ -567,7 +567,7 @@ void main() { expect(box().value, isFalse); await tester.tap(find.text('완료 시 자동 동영상 추가')); await tester.pump(); - expect(s.addFinishedDownloads, isTrue); + expect(s.download.addFinished, isTrue); expect(box().value, isTrue); // 다 받으면 목록에서 뺀다 (앱이 onFinished 에서 부름) @@ -593,7 +593,7 @@ void main() { final d = DownloadManager(backends: [_Backend()], settings: () => s, readClipboard: () async => null); addTearDown(d.dispose); d.addToEditList = (files) async => files.length; - d.setAutoAdd = (on) async => s.addFinishedDownloads = on; + d.setAutoAdd = (on) async => s.download.addFinished = on; d.addPage('https://vimeo.com/1')!.title = '영상'; await tester.pumpWidget(MaterialApp(home: DownloadsPage(d: d))); await tester.pump(); diff --git a/test/browser_test.dart b/test/browser_test.dart index 18deb1f..81aba13 100644 --- a/test/browser_test.dart +++ b/test/browser_test.dart @@ -187,7 +187,7 @@ void main() { expect(isCookieReadError('ERROR: Failed to decrypt with DPAPI. See cookies issue'), isTrue); expect(isCookieReadError('ERROR: Video unavailable'), isFalse); expect(ytDlpCookieArgs(browser: internalBrowserCookies), isEmpty); // 아직 로그인 전 - expect(AppSettings().ytCookiesBrowser, internalBrowserCookies); // 새 설치 기본값 + expect(AppSettings().download.ytCookiesBrowser, internalBrowserCookies); // 새 설치 기본값 }); test('cookies.txt 형식 (HttpOnly · 중복 제거 · 쿠키 파일 우선)', () { final txt = toNetscapeCookies(const [ @@ -208,7 +208,7 @@ void main() { test('브라우저 페이지 받기 (YouTube 외 사이트도)', () { final v = _Backend(DownloadKind.video); - final d = DownloadManager(backends: [v], settings: () => AppSettings()..downloadRoot = r'D:\dl'); + final d = DownloadManager(backends: [v], settings: () => AppSettings()..download.root = r'D:\dl'); expect(d.addPage('https://vimeo.com/12345')!.kind, DownloadKind.video); expect(d.addPage('https://vimeo.com/12345'), isNull); // 이미 받는 중 expect(d.addPage('about:blank'), isNull); @@ -218,7 +218,7 @@ void main() { test('다 받은 영상 → 편집 목록 알림 (한 번만, 조각 파일 제외)', () async { final v = _Backend(DownloadKind.video); - final d = DownloadManager(backends: [v], settings: () => AppSettings()..downloadRoot = r'D:\dl'); + final d = DownloadManager(backends: [v], settings: () => AppSettings()..download.root = r'D:\dl'); final got = []; d.onFinished.listen((t) => got.add(t.id)); final t = d.addPage('https://vimeo.com/1')!; diff --git a/test/cleanup_test.dart b/test/cleanup_test.dart index 9812c9d..9823caa 100644 --- a/test/cleanup_test.dart +++ b/test/cleanup_test.dart @@ -56,7 +56,7 @@ void main() { storage: _Storage(tmp), models: ModelStore(models), )); - c.settings.downloadRoot = p.join(root.path, 'dl'); + c.settings.download.root = p.join(root.path, 'dl'); c.logFile = p.join(logs, 'app.log'); // 지울 것 diff --git a/test/download_test.dart b/test/download_test.dart index 7528ea0..ba0e5c9 100644 --- a/test/download_test.dart +++ b/test/download_test.dart @@ -137,18 +137,18 @@ https://example.com/page.html final dir = Directory.systemTemp.createTempSync('jj_set_'); addTearDown(() => dir.deleteSync(recursive: true)); final store = SettingsStore(p.join(dir.path, 'settings.json')); - expect((await store.load()).clipboardWatch, isTrue); // 파일 없으면 기본값 + expect((await store.load()).download.clipboardWatch, isTrue); // 파일 없으면 기본값 final s = AppSettings() ..mkvOutputRoot = r'D:\out' - ..downloadRoot = r'D:\dl' - ..clipboardWatch = false + ..download.root = r'D:\dl' + ..download.clipboardWatch = false ..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.downloadRoot, l.clipboardWatch, l.showHotkey, l.askAiOptions], + expect([l.mkvOutputRoot, l.download.root, l.download.clipboardWatch, l.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); @@ -166,7 +166,7 @@ https://example.com/page.html setUp(() { video = _FakeBackend(DownloadKind.video); torrent = _FakeBackend(DownloadKind.torrent); - settings = AppSettings()..downloadRoot = r'D:\dl'; + settings = AppSettings()..download.root = r'D:\dl'; clip = 'https://youtu.be/already-there'; d = DownloadManager( backends: [video, torrent], settings: () => settings, readClipboard: () async => clip); @@ -193,7 +193,7 @@ https://example.com/page.html expect(video.started, ['https://www.youtube.com/watch?v=AAA111']); expect(torrent.started, hasLength(1)); - settings.clipboardWatch = false; + settings.download.clipboardWatch = false; clip = 'https://youtu.be/BBB222'; expect(await d.checkClipboard(), isEmpty); }); diff --git a/test/downloads_ui_test.dart b/test/downloads_ui_test.dart index f146f53..eb46e68 100644 --- a/test/downloads_ui_test.dart +++ b/test/downloads_ui_test.dart @@ -45,7 +45,7 @@ class _Quiet extends _Backend { void main() { DownloadManager make() => DownloadManager( backends: [_Backend(DownloadKind.video), _Backend(DownloadKind.torrent)], - settings: () => AppSettings()..downloadRoot = r'D:\dl', + settings: () => AppSettings()..download.root = r'D:\dl', readClipboard: () async => null, ); @@ -213,7 +213,7 @@ void main() { addTearDown(tester.view.reset); final d = DownloadManager( backends: [_Quiet(DownloadKind.video), _Quiet(DownloadKind.torrent)], - settings: () => AppSettings()..downloadRoot = r'D:\dl', + settings: () => AppSettings()..download.root = r'D:\dl', readClipboard: () async => null, ); await tester.pumpWidget(MaterialApp(home: DownloadsPage(d: d))); diff --git a/test/queue_test.dart b/test/queue_test.dart index 86b50fb..34e48f5 100644 --- a/test/queue_test.dart +++ b/test/queue_test.dart @@ -113,8 +113,8 @@ void main() { setUp(() { video = _Backend(DownloadKind.video); settings = AppSettings() - ..downloadRoot = r'D:\dl' - ..maxParallelDownloads = 2; + ..download.root = r'D:\dl' + ..download.maxParallel = 2; d = DownloadManager(backends: [video], settings: () => settings, readClipboard: () async => null); }); tearDown(() => d.dispose()); @@ -132,7 +132,7 @@ void main() { expect(ts[3].state, DownloadState.downloading); await d.resume([ts[1]]); expect(ts[1].state, DownloadState.queued); - settings.maxParallelDownloads = 0; // 무한 + settings.download.maxParallel = 0; // 무한 await d.resume(const []); expect(ts.where((t) => t.state == DownloadState.downloading), hasLength(4)); }); @@ -153,7 +153,7 @@ void main() { expect(d.tasks.first.dir, p.join(r'D:\dl', 'jj_yt-dlp', 'My Playlist')); expect(d.tasks.contains(holder), isFalse); - settings.ytExpandPlaylists = false; + settings.download.ytExpandPlaylists = false; final single = d.add('https://www.youtube.com/watch?v=z&list=PLother')!; expect(single.expanding, isFalse); }); diff --git a/test/security_bits_test.dart b/test/security_bits_test.dart index 7c9d714..99d9890 100644 --- a/test/security_bits_test.dart +++ b/test/security_bits_test.dart @@ -95,15 +95,15 @@ void main() { }); test('고른 사이트의 쿠키만 넘긴다 (처음은 모두)', () { - expect(AppSettings().loginCookieSites, loginCookieDomains); + expect(AppSettings().download.loginCookieSites, loginCookieDomains); expect(isLoginCookieDomain('.instagram.com'), isTrue); CookieExport.sites = ['youtube.com', 'google.com']; expect(isLoginCookieDomain('.youtube.com'), isTrue); expect(isLoginCookieDomain('.instagram.com'), isFalse); expect(isLoginCookieDomain('netflix.com'), isFalse); - final b = AppSettings.fromJson((AppSettings()..loginCookieSites = ['x.com']).toJson()); - expect(b.loginCookieSites, ['x.com']); - expect(AppSettings.fromJson({'loginCookieSites': ['x.com', 'evil.com']}).loginCookieSites, ['x.com']); + final b = AppSettings.fromJson((AppSettings()..download.loginCookieSites = ['x.com']).toJson()); + expect(b.download.loginCookieSites, ['x.com']); + expect(AppSettings.fromJson({'loginCookieSites': ['x.com', 'evil.com']}).download.loginCookieSites, ['x.com']); }); test('앱 안 브라우저 쿠키를 쓰던 사람에게 넘기는 사이트가 넓어진 것을 한 번 알린다', () { diff --git a/test/small_items_test.dart b/test/small_items_test.dart index 889f359..9100a01 100644 --- a/test/small_items_test.dart +++ b/test/small_items_test.dart @@ -33,9 +33,9 @@ void main() { }); test('36: 같은 화질이면 H.264 먼저 - 설정 (PC 기본 꺼짐) · yt-dlp 인수', () { - expect(AppSettings.fromJson({}).ytPreferH264, isFalse); // 시험은 PC (Android 는 기본 켜짐) - expect(AppSettings.fromJson({'ytPreferH264': true}).ytPreferH264, isTrue); - expect(AppSettings.fromJson((AppSettings()..ytPreferH264 = true).toJson()).ytPreferH264, isTrue); + expect(AppSettings.fromJson({}).download.ytPreferH264, isFalse); // 시험은 PC (Android 는 기본 켜짐) + expect(AppSettings.fromJson({'ytPreferH264': true}).download.ytPreferH264, isTrue); + expect(AppSettings.fromJson((AppSettings()..download.ytPreferH264 = true).toJson()).download.ytPreferH264, isTrue); expect(ytDlpFormatArgs(YtContainer.mp4, YtQuality.best, preferH264: true).join(' '), contains('vcodec:h264')); expect(ytDlpFormatArgs(YtContainer.mp4, YtQuality.best).join(' '), isNot(contains('vcodec:h264'))); });