240 lines
15 KiB
Dart
240 lines
15 KiB
Dart
import 'package:file_picker/file_picker.dart';
|
|
import 'package:flutter/material.dart';
|
|
import '../../app/app_controller.dart';
|
|
import '../../core/download_detect.dart';
|
|
import '../folder_picker.dart';
|
|
import '../copy_sync_settings.dart';
|
|
import '../../l10n/tr.dart';
|
|
import '../setting_tile.dart';
|
|
import '../browser_page.dart' show deleteExportedCookies;
|
|
import '../toast_status.dart';
|
|
import 'settings_tiles.dart';
|
|
|
|
// 약속 (설정 B): 환경 설정의 분야 - Rsync · 다운로드. 바꾸면 바로 설정에 저장 (c.updateSettings)
|
|
|
|
/// 환경 설정 > Rsync
|
|
class RsyncSettingsSection extends StatefulWidget {
|
|
const RsyncSettingsSection({super.key, required this.c, required this.desk});
|
|
|
|
final AppController c;
|
|
|
|
/// PC (Android 가 아님) - 창 · 트레이 · 탐색기 · 다운로드 · 내장 Chrome 설정
|
|
final bool desk;
|
|
|
|
@override
|
|
State<RsyncSettingsSection> createState() => _RsyncSettingsSectionState();
|
|
}
|
|
|
|
class _RsyncSettingsSectionState extends State<RsyncSettingsSection> {
|
|
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: [
|
|
CopySyncSettings(c: c, rsync: true),
|
|
SwitchListTile(
|
|
value: s.allowInnerToOuter,
|
|
onChanged: (v) => c.updateSettings((x) => x.allowInnerToOuter = v),
|
|
title: Text(tr('원본이 대상 안에 있어도 맞추기 (안쪽 → 바깥)')),
|
|
subtitle: Text(tr('확인 창에서 알린 뒤 허용합니다. 지우기 (--delete) 가 있으면 늘 막습니다. 끄면 늘 막습니다')),
|
|
),
|
|
if (desk)
|
|
SwitchListTile(
|
|
value: s.copy.syncStopToast,
|
|
onChanged: (v) => c.updateSettings((x) => x.copy.syncStopToast = v),
|
|
title: Text(tr('동기화가 멈추면 Windows 알림')),
|
|
subtitle: Text(tr('창을 트레이에 둔 채 실시간 동기화가 원본을 못 읽어 멈추면 알림을 띄웁니다 (트레이 툴팁 경고는 늘)')),
|
|
),
|
|
// Windows 에서 알림이 꺼져 있으면 알리고 설정을 열어 준다 (바꾸지는 않음)
|
|
if (desk && s.copy.syncStopToast) const ToastStatusHint(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
/// 환경 설정 > 다운로드
|
|
class DownloadSettingsSection extends StatefulWidget {
|
|
const DownloadSettingsSection({super.key, required this.c, required this.desk});
|
|
|
|
final AppController c;
|
|
|
|
/// PC (Android 가 아님) - 창 · 트레이 · 탐색기 · 다운로드 · 내장 Chrome 설정
|
|
final bool desk;
|
|
|
|
@override
|
|
State<DownloadSettingsSection> createState() => _DownloadSettingsSectionState();
|
|
}
|
|
|
|
class _DownloadSettingsSectionState extends State<DownloadSettingsSection> {
|
|
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: [
|
|
if (desk)
|
|
SwitchListTile(
|
|
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.download.addFinished,
|
|
onChanged: (v) => c.updateSettings((x) => x.download.addFinished = v),
|
|
title: Text(tr('완료 시 자동 동영상 추가')),
|
|
subtitle: Text(tr('다 받는 대로 MKV 만들기의 동영상 목록에 넣고, 다운로드 목록에서는 뺍니다 (다운로드 화면의 체크 상자와 같음)')),
|
|
),
|
|
// 관리자 지시 (10/20): 시작되지 않는 마그넷 · 토렌트 알림 (처음 60초)
|
|
SettingTile(
|
|
title: Text(tr('마그넷 · 토렌트가 시작되지 않으면 알리기')),
|
|
subtitle: Text(tr('메타데이터나 피어를 이 시간 동안 못 받으면 그 줄에 이유 후보와 [다시 시도] 를 보입니다')),
|
|
trailing: DropdownButton<int>(
|
|
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.download.torrentStallSeconds = v!),
|
|
),
|
|
),
|
|
SettingTile(
|
|
title: Text(tr('YouTube 받을 형식')),
|
|
subtitle: Text(tr('기본: MP4. 음성만 받으면 MP3 · M4A 로 저장')),
|
|
trailing: DropdownButton<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.download.ytContainer = v!),
|
|
),
|
|
),
|
|
SettingTile(
|
|
enabled: !s.download.ytContainer.audioOnly,
|
|
title: Text(tr('YouTube 화질')),
|
|
subtitle: Text(tr('고른 크기 이하에서 가장 좋은 화질 (영상에 없는 크기면 가까운 것)')),
|
|
trailing: DropdownButton<YtQuality>(
|
|
value: s.download.ytQuality,
|
|
items: [for (final v in YtQuality.values) DropdownMenuItem(value: v, child: Text(v.label))],
|
|
onChanged: s.download.ytContainer.audioOnly ? null : (v) => c.updateSettings((x) => x.download.ytQuality = v!),
|
|
),
|
|
),
|
|
// 36: 같은 크기면 H.264 먼저 (Android 기본 켜짐)
|
|
SwitchListTile(
|
|
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.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.download.ytCookiesFile.isNotEmpty
|
|
? 'cookies.txt: ${s.download.ytCookiesFile}'
|
|
: s.download.ytCookiesBrowser == internalBrowserCookies
|
|
? tr('앱 안 브라우저의 로그인 사용 (브라우저 화면에서 YouTube 에 로그인하세요)')
|
|
: 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<String>(
|
|
value: s.download.ytCookiesFile.isNotEmpty ? '*file' : s.download.ytCookiesBrowser,
|
|
items: [
|
|
DropdownMenuItem(value: '', child: Text(tr('사용 안 함'))),
|
|
DropdownMenuItem(value: internalBrowserCookies, child: Text(tr('앱 안 브라우저 (권장)'))),
|
|
// PC 브라우저의 쿠키는 Windows 에서만
|
|
if (desk)
|
|
for (final b in const ['firefox', 'chrome', 'edge', 'brave', 'whale', 'opera'])
|
|
DropdownMenuItem(value: b, child: Text(b[0].toUpperCase() + b.substring(1))),
|
|
DropdownMenuItem(value: '*file', child: Text(tr('cookies.txt 파일…'))),
|
|
],
|
|
onChanged: (v) async {
|
|
if (v == '*file') {
|
|
final f = await FilePicker.pickFile(
|
|
dialogTitle: tr('cookies.txt (Netscape 형식)'),
|
|
type: FileType.custom,
|
|
allowedExtensions: const ['txt']);
|
|
if (f?.path != null) {
|
|
await c.updateSettings((x) => x
|
|
..download.ytCookiesFile = f!.path!
|
|
..download.ytCookiesBrowser = '');
|
|
}
|
|
} else {
|
|
await c.updateSettings((x) => x
|
|
..download.ytCookiesBrowser = v ?? ''
|
|
..download.ytCookiesFile = '');
|
|
}
|
|
// 131: 앱 안 브라우저 로그인을 더 쓰지 않으면 내보내 둔 평문 쿠키 파일을 지운다
|
|
if (c.settings.download.ytCookiesBrowser != internalBrowserCookies) {
|
|
await deleteExportedCookies(c.settings.webViewDataDir);
|
|
if (mounted) setState(() {});
|
|
}
|
|
},
|
|
),
|
|
]),
|
|
),
|
|
// 53: 앱 안 브라우저 쿠키 - 어디에 어떻게 저장되는지 · 넘길 사이트 고르기 · 지우기
|
|
// 131: 쓰지 않게 바꿨어도 파일이 남아 있으면 지우는 버튼은 보인다
|
|
if (s.download.ytCookiesBrowser == internalBrowserCookies || s.internalCookieFile.isNotEmpty)
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
|
Text(
|
|
tr('로그인 쿠키는 다운로드 (yt-dlp) 가 쓰도록 이 기기의 앱 폴더에 cookies_youtube.txt 로 저장됩니다 (평문). '
|
|
'이 파일을 가진 사람은 그 계정에 로그인할 수 있으니 기기를 남에게 줄 때는 지우세요. 고른 사이트의 쿠키만 넘깁니다:'),
|
|
// 관리자 지시 (10/21, 131 확인): 12px 흐린 회색은 읽기 힘들었다 - 위아래 설명 글 (ListTile 부제목) 과 같은 크기 · 색으로
|
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant)),
|
|
const SizedBox(height: 6),
|
|
Wrap(spacing: 6, runSpacing: 6, children: [
|
|
for (final d in loginCookieDomains)
|
|
FilterChip(
|
|
label: Text(d),
|
|
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,
|
|
]),
|
|
),
|
|
OutlinedButton.icon(
|
|
icon: const Icon(Icons.cookie_outlined, size: 18),
|
|
label: Text(tr('쿠키 파일 지우기')),
|
|
onPressed: () async {
|
|
await deleteExportedCookies(s.webViewDataDir);
|
|
if (!context.mounted) return;
|
|
setState(() {});
|
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
|
content: Text(tr('내보낸 쿠키 파일을 지웠습니다. 브라우저의 로그인까지 지우려면 '
|
|
'웹 브라우저 화면의 [쿠키 · 방문 기록 지우기] 를 누르세요.'))));
|
|
},
|
|
),
|
|
]),
|
|
]),
|
|
),
|
|
SettingTile(
|
|
title: Text(tr('동시 다운로드 수')),
|
|
subtitle: Text(tr('나머지는 대기했다가 차례로 받습니다')),
|
|
trailing: CountSelector(
|
|
value: s.download.maxParallel,
|
|
onChanged: (n) => c.updateSettings((x) => x.download.maxParallel = n),
|
|
),
|
|
),
|
|
settingsFolderTile(
|
|
title: tr('다운로드 (jj_yt-dlp · jj_aria2)'),
|
|
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.download.root = d);
|
|
},
|
|
onReset: () => c.updateSettings((x) => x.download.root = null),
|
|
),
|
|
]);
|
|
}
|
|
}
|