567 lines
21 KiB
Dart
567 lines
21 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:path/path.dart' as p;
|
|
|
|
import '../core/reader_sources.dart' show defaultImageExtensions;
|
|
import '../core/download_detect.dart';
|
|
import '../core/encode_options.dart';
|
|
import '../core/playlist.dart';
|
|
import 'settings/records.dart';
|
|
import 'settings/settings_group.dart';
|
|
import 'settings/display_settings.dart';
|
|
import 'settings/remote_settings.dart';
|
|
import 'settings/explorer_tool_settings.dart';
|
|
import 'settings/vault_settings.dart';
|
|
import 'settings/run_settings.dart';
|
|
import 'settings/browser_settings.dart';
|
|
import 'settings/download_settings.dart';
|
|
import 'settings/reader_settings.dart';
|
|
import 'settings/ai_image_settings.dart';
|
|
import 'settings/copy_settings.dart';
|
|
import 'settings/tor_settings.dart';
|
|
import 'settings/network_settings.dart';
|
|
import 'settings/explorer_settings.dart';
|
|
|
|
export 'settings/records.dart';
|
|
export 'settings/settings_group.dart';
|
|
export 'settings/settings_store.dart';
|
|
export 'settings/display_settings.dart';
|
|
export 'settings/remote_settings.dart';
|
|
export 'settings/explorer_tool_settings.dart';
|
|
export 'settings/vault_settings.dart';
|
|
export 'settings/run_settings.dart';
|
|
export 'settings/browser_settings.dart';
|
|
export 'settings/download_settings.dart';
|
|
export 'settings/reader_settings.dart';
|
|
export 'settings/ai_image_settings.dart';
|
|
export 'settings/copy_settings.dart';
|
|
export 'settings/tor_settings.dart';
|
|
export 'settings/network_settings.dart';
|
|
export 'settings/explorer_settings.dart';
|
|
|
|
/// 환경 설정 (settings.json 에 저장)
|
|
class AppSettings {
|
|
/// MKV·자막 저장 위치. null 이면 동영상이 있는 폴더 아래 jj_mkv
|
|
String? mkvOutputRoot;
|
|
|
|
/// MKV 세부 정보 오른쪽 아래의 이동 버튼들 (표시 이름 · 폴더). 세 번 누르기는 첫 번째 버튼의 폴더로.
|
|
/// 비어 있으면 [이동] 을 처음 누를 때 폴더를 골라 하나 만든다.
|
|
List<MoveTarget> moveTargets = [];
|
|
|
|
/// 73: 파일 탐색기 · Rsync 즐겨찾기 (폴더, Rsync 는 짝)
|
|
List<FolderBookmark> folderBookmarks = [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// AI 자막: 매번 설정 창을 띄울지 (끄면 마지막 설정으로 바로 시작)
|
|
bool askAiOptions = true;
|
|
|
|
|
|
|
|
/// OpenSubtitles (인터넷 자막 검색) - 키는 필수, 아이디·비밀번호는 선택 (받기 횟수 늘어남)
|
|
String openSubtitlesKey = '';
|
|
String openSubtitlesUser = '';
|
|
String openSubtitlesPassword = '';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// 동시에 만드는 MKV 수 (0 = 무제한)
|
|
int maxParallelJobs = 5;
|
|
|
|
|
|
/// 폰 세로 (좁은 화면) MKV 목록 크기: 'auto' 목록은 내용만큼 · 자세히 칸에 최소 높이 (처음 값) · 'small' · 'large' (관리자 판단 10/17)
|
|
String mkvCompactList = 'auto';
|
|
|
|
|
|
|
|
|
|
/// 탐색기에서 동영상을 열었을 때 (더블클릭 · 연결 프로그램): 'play' 바로 재생 / 'add' 편집 목록에 추가
|
|
String openFileAction = 'play';
|
|
|
|
|
|
|
|
|
|
|
|
/// 프로그램이 켜져 있을 때 탐색기에서 연 동영상 재생: 'same' 켜져 있는 창에서 / 'new' 새 재생 창
|
|
String openFileWindow = 'same';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ── 네트워크 · DNS (사용자 요청 10/10: "DNS 주소를 강제로 내가 설정한 주소로", 기본 8.8.8.8 · 보조 8.8.4.4 - 관리자 판단) ──
|
|
|
|
|
|
// ── Tor 브라우저 (사용자 요청 10/10) ──
|
|
|
|
|
|
|
|
|
|
|
|
// ── 파일 탐색기 (X-plore 참고) ──
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ── 복사 · 이동 (파일 탐색기) · rsync · lsync (Rsync 화면) ──
|
|
|
|
|
|
/// P0: MKV 를 만들 때 결과 MKV 가 이미 있으면 - 'ask' 묻기 (처음 값) · 'overwrite' 덮어쓰기 · 'skip' 건너뛰기 · 'rename' 다른 이름.
|
|
/// 덮어쓰기여도 다 만든 뒤에만 바꿔 넣는다 (도중에 취소 · 실패하면 예전 것 그대로)
|
|
String mkvExisting = 'ask';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// 그림 보기 (만화 보기) 대상 확장자 (파일 탐색기에서 두 번 누르면 보기로 연다)
|
|
List<String> imageExts = [...defaultImageExtensions];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ── 102: 사용자에게 물어 정한 것 · 관리자 1차 판단 (기본값 = 그 답) ──
|
|
|
|
|
|
|
|
/// 저장 공간 정리 창에서 미리 체크할 묶음 (작업 임시 파일 · 받다 만 다운로드)
|
|
List<String> cleanupPrechecked = ['work', 'download'];
|
|
|
|
/// 19: 업데이트로 바뀐 기본값을 켤 때 한 번 알림
|
|
bool migrationNotice = true;
|
|
|
|
/// 34: Rsync · 동기화에서 원본이 대상 안 (안쪽 → 바깥) 을 허용 (지우기가 있을 때만 막음). 끄면 늘 막음
|
|
bool allowInnerToOuter = true;
|
|
|
|
/// 40 · 54: 비밀번호를 이 기기의 안전 저장소에 기억 (끄면 저장하지 않고 앱을 켤 때마다 다시 넣음)
|
|
bool rememberPasswords = true;
|
|
|
|
|
|
/// 65 (Windows): 탐색기에서 지우면 휴지통으로 (끄면 늘 영구 삭제로 묻는다)
|
|
bool recycleOnDelete = true;
|
|
|
|
/// 47: MKV 목록에서 세 번 누르면 'move' (확인 뒤 이동 폴더로) · 'none' (아무것도 안 함)
|
|
String tripleTapAction = 'move';
|
|
|
|
/// 61: 기본 자막 언어 (MKV 의 기본 자막 트랙 · 플레이어가 먼저 켜는 자막). '' = 화면 언어 따르기, 그 밖은 자막 언어 코드 (ko · ja …)
|
|
String preferredSubtitleLanguage = '';
|
|
|
|
|
|
/// 39: 켤 때 "모든 파일 접근" 안내를 한 번 보였는지 (그 뒤로는 파일 탐색기 안의 안내로)
|
|
bool allFilesHintShown = false;
|
|
|
|
/// 내장 플레이어 자막 글자 크기 배율 (0.5 ~ 2.5, 기본 1)
|
|
double subtitleScale = 1.0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ───────── 123 · 121: AI 그림 · 해상도 올리기 ─────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 121: 해상도 올리기
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// 앱 안 브라우저의 데이터 폴더 (로그인 · 쿠키). 실행 중에 정해지며 저장하지 않음
|
|
String webViewDataDir = '';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// 동영상 하나를 재생할 때 재생 목록 (기본: 같은 시리즈)
|
|
PlaylistMode playlistMode = PlaylistMode.series;
|
|
|
|
/// 확장자별 재생 프로그램 (없으면 내장 플레이어): 'system' 또는 실행 파일 경로
|
|
Map<String, String> externalPlayers = {};
|
|
|
|
/// 마지막 인코딩 설정
|
|
EncodeSettings encode = const EncodeSettings();
|
|
|
|
/// 마지막 AI 자막 설정
|
|
String aiSource = 'und';
|
|
List<String> aiTargets = ['ko', 'en', 'ja'];
|
|
String aiWhisper = 'whisper-base';
|
|
|
|
AppSettings();
|
|
|
|
/// 파일 탐색기 (X-plore 참고)
|
|
final explorer = ExplorerSettings();
|
|
|
|
/// 네트워크 · DNS (사용자 요청 10/10)
|
|
final network = NetworkSettings();
|
|
|
|
/// Tor 釉뚮씪?곗? (?ъ슜???붿껌 10/10)
|
|
final tor = TorBrowserSettings();
|
|
|
|
/// 복사 · 이동 (파일 탐색기) · rsync · 실시간 동기화 (Rsync 화면)
|
|
final copy = CopySettings();
|
|
|
|
/// AI 그림 · 해상도 올리기 (123 · 121)
|
|
final aiImage = AiImageSettings();
|
|
|
|
/// 보기 화면 (그림 · 만화 · PDF · 문서)
|
|
final reader = ReaderSettings();
|
|
|
|
/// 다운로드 · YouTube (yt-dlp · aria2)
|
|
final download = DownloadSettings();
|
|
|
|
/// 웹 브라우저 (앱 안 · 외부)
|
|
final browser = BrowserSettings();
|
|
|
|
/// 실행 · 종료 · 업데이트
|
|
final run = RunSettings();
|
|
|
|
/// 암호 보관함
|
|
final vault = VaultSettings();
|
|
|
|
/// 탐색기 도구 - 시스템 폴더 (Shizuku) · 루트 · Wi-Fi 공유 · ZIP 묶기
|
|
final tools = ExplorerToolSettings();
|
|
|
|
/// 원격 서버 (WebDAV · SFTP · SMB · FTP)
|
|
final remote = RemoteSettings();
|
|
|
|
/// 화면 · 앱 모양 (배율 · 밝기 · 방향 · 언어 · 앱 아이콘 · 컴포넌트 · 화면 순서 · 밀기)
|
|
final display = DisplaySettings();
|
|
|
|
/// 설정 분야들 (설정 A) - 저마다 자기 키를 읽고 쓴다. toJson · fromJson 은 이 목록을 돌며 합치기만 한다
|
|
List<SettingsGroup> get groups => [explorer, network, tor, copy, aiImage, reader, download, browser, run, vault, tools, remote, display];
|
|
|
|
/// [plainSecrets]: 안전 저장소를 쓸 수 없을 때만 (예전 파일에 있던 비밀 값을 잃지 않게) 예전처럼 파일에 둔다
|
|
Map<String, Object?> toJson({bool plainSecrets = false}) {
|
|
final j = <String, Object?>{
|
|
// 이 판이 모르는 항목 (새 판이 쓴 것 등) 은 지우지 않고 그대로 (40)
|
|
...stripSecrets(extra),
|
|
'mkvOutputRoot': mkvOutputRoot,
|
|
'moveTargets': [for (final t in moveTargets) t.toJson()],
|
|
'folderBookmarks': [for (final b in folderBookmarks) b.toJson()],
|
|
'askAiOptions': askAiOptions,
|
|
// 비밀 값은 안전 저장소에 (54). 파일에는 저장해 두었는지만
|
|
'openSubtitlesSaved': {
|
|
'key': openSubtitlesKey.isNotEmpty,
|
|
'user': openSubtitlesUser.isNotEmpty,
|
|
'password': openSubtitlesPassword.isNotEmpty,
|
|
},
|
|
if (plainSecrets) ...{
|
|
'openSubtitlesKey': openSubtitlesKey,
|
|
'openSubtitlesUser': openSubtitlesUser,
|
|
'openSubtitlesPassword': openSubtitlesPassword,
|
|
},
|
|
'cookieScopeV2': true,
|
|
'maxParallelJobs': maxParallelJobs,
|
|
'mkvCompactList': mkvCompactList,
|
|
'openFileAction': openFileAction,
|
|
'openFileWindow': openFileWindow,
|
|
'mkvExisting': mkvExisting,
|
|
'robocopyLinksV1': true,
|
|
'imageExts': imageExts,
|
|
'cleanupPrechecked': cleanupPrechecked,
|
|
'migrationNotice': migrationNotice,
|
|
'allowInnerToOuter': allowInnerToOuter,
|
|
'rememberPasswords': rememberPasswords,
|
|
'recycleOnDelete': recycleOnDelete,
|
|
'tripleTapAction': tripleTapAction,
|
|
'preferredSubtitleLanguage': preferredSubtitleLanguage,
|
|
// 116: 예전 판의 표시 ('allFilesHintShown') 는 권한이 없어진 것을 몰라 다시 띄우지 못했다 → 새 이름으로 한 번 더
|
|
'allFilesHint2': allFilesHintShown,
|
|
'subtitleScale': subtitleScale,
|
|
'playlistMode': playlistMode.name,
|
|
'externalPlayers': externalPlayers,
|
|
'encode': {
|
|
'codec': encode.codec.name,
|
|
'resolution': encode.resolution.name,
|
|
'quality': encode.quality.name,
|
|
'frame': encode.frame.name,
|
|
'fit': encode.fit.name,
|
|
'rotate': encode.rotate.name,
|
|
'brightness': encode.brightness,
|
|
'contrast': encode.contrast,
|
|
'saturation': encode.saturation,
|
|
'temperature': encode.temperature,
|
|
},
|
|
'aiSource': aiSource,
|
|
'aiTargets': aiTargets,
|
|
'aiWhisper': aiWhisper,
|
|
};
|
|
for (final g in groups) {
|
|
g.write(j, plainSecrets: plainSecrets);
|
|
}
|
|
return j;
|
|
}
|
|
|
|
|
|
/// 색 보정 값 (-100 ~ 100)
|
|
static int _adj(Object? v) => ((v as num?)?.round() ?? 0).clamp(-100, 100);
|
|
|
|
factory AppSettings.fromJson(Map<String, dynamic> j) {
|
|
T pick<T extends Enum>(List<T> values, Object? name, T fallback) =>
|
|
values.firstWhere((v) => v.name == name, orElse: () => fallback);
|
|
final e = (j['encode'] as Map?) ?? const {};
|
|
final s = AppSettings()
|
|
..mkvOutputRoot = j['mkvOutputRoot'] as String?
|
|
..folderBookmarks = [for (final x in (j['folderBookmarks'] as List?) ?? const []) ?FolderBookmark.fromJson(x)]
|
|
..moveTargets = [
|
|
for (final x in (j['moveTargets'] as List?) ?? const [])
|
|
if (x is Map && x['dir'] is String) MoveTarget.fromJson(x),
|
|
// 예전 판의 이동 폴더 하나 → "이동" 버튼으로
|
|
if (j['moveTargets'] == null && j['moveTargetDir'] is String) MoveTarget('이동', j['moveTargetDir'] as String),
|
|
]
|
|
..askAiOptions = j['askAiOptions'] as bool? ?? true
|
|
..openSubtitlesKey = j['openSubtitlesKey'] as String? ?? ''
|
|
..openSubtitlesUser = j['openSubtitlesUser'] as String? ?? ''
|
|
..openSubtitlesPassword = j['openSubtitlesPassword'] as String? ?? ''
|
|
..maxParallelJobs = (j['maxParallelJobs'] as num?)?.toInt() ?? 5
|
|
..mkvCompactList = const ['auto', 'small', 'large'].contains(j['mkvCompactList']) ? j['mkvCompactList'] as String : 'auto'
|
|
..openFileAction = j['openFileAction'] == 'add' ? 'add' : 'play'
|
|
..openFileWindow = j['openFileWindow'] == 'new' ? 'new' : 'same'
|
|
..mkvExisting = const ['rename', 'overwrite', 'skip'].contains(j['mkvExisting']) ? j['mkvExisting'] as String : 'ask'
|
|
..imageExts = j['imageExts'] is List
|
|
? [for (final x in j['imageExts'] as List) '$x'.toLowerCase()]
|
|
: [...defaultImageExtensions]
|
|
..cleanupPrechecked = j['cleanupPrechecked'] is List
|
|
? [for (final x in j['cleanupPrechecked'] as List) '$x']
|
|
: ['work', 'download']
|
|
..migrationNotice = j['migrationNotice'] != false
|
|
..allowInnerToOuter = j['allowInnerToOuter'] != false
|
|
..rememberPasswords = j['rememberPasswords'] != false
|
|
..recycleOnDelete = j['recycleOnDelete'] != false
|
|
..tripleTapAction = j['tripleTapAction'] == 'none' ? 'none' : 'move'
|
|
..preferredSubtitleLanguage = j['preferredSubtitleLanguage'] is String ? j['preferredSubtitleLanguage'] as String : ''
|
|
..allFilesHintShown = j['allFilesHint2'] == true
|
|
..subtitleScale = ((j['subtitleScale'] as num?)?.toDouble() ?? 1.0).clamp(0.5, 2.5)
|
|
..playlistMode = pick(PlaylistMode.values, j['playlistMode'], PlaylistMode.series)
|
|
..externalPlayers = ((j['externalPlayers'] as Map?) ?? const {}).cast<String, String>()
|
|
..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),
|
|
frame: pick(FrameChoice.values, e['frame'], FrameChoice.original),
|
|
fit: pick(FitChoice.values, e['fit'], FitChoice.fill),
|
|
rotate: pick(RotateChoice.values, e['rotate'], RotateChoice.none),
|
|
brightness: _adj(e['brightness']),
|
|
contrast: _adj(e['contrast']),
|
|
saturation: _adj(e['saturation']),
|
|
temperature: _adj(e['temperature']),
|
|
)
|
|
..aiSource = j['aiSource'] as String? ?? 'und'
|
|
..aiTargets = (j['aiTargets'] as List?)?.cast<String>() ?? ['ko', 'en', 'ja']
|
|
..aiWhisper = j['aiWhisper'] as String? ?? 'whisper-base';
|
|
// 분야를 먼저 읽는다 - 바뀐 기본값 알림 ([_migrated]) 이 분야의 값을 본다
|
|
for (final g in s.groups) {
|
|
g.read(j);
|
|
}
|
|
s.migrated = _migrated(j, s);
|
|
s.extra = Map.of(j);
|
|
return s;
|
|
}
|
|
|
|
/// 업데이트하면서 예전 기본값을 새 기본값으로 옮긴 것 (예전 설정 파일에 그 값이 실제로 있었을 때만).
|
|
/// 앱이 켜질 때 한 번 알리고 (무엇이 바뀌었는지 · 환경 설정에서 되돌릴 수 있다는 것) 비운다. 저장하지 않는다.
|
|
List<String> migrated = [];
|
|
|
|
/// 읽은 설정 파일 그대로 (이 판이 모르는 항목을 저장할 때 지우지 않게)
|
|
Map<String, dynamic> extra = {};
|
|
|
|
/// 설정 파일에 평문으로 두면 안 되는 항목 (54)
|
|
static const secretKeys = ['openSubtitlesKey', 'openSubtitlesUser', 'openSubtitlesPassword'];
|
|
|
|
/// 설정 JSON 에서 비밀 값을 뺀 것 (.bak · 모르는 항목 보존용)
|
|
static Map<String, dynamic> stripSecrets(Map<String, dynamic> j) {
|
|
final r = Map<String, dynamic>.of(j);
|
|
for (final k in secretKeys) {
|
|
r.remove(k);
|
|
}
|
|
final servers = r['webdavServers'];
|
|
if (servers is List) {
|
|
r['webdavServers'] = [
|
|
for (final x in servers) x is Map ? (Map.of(x)..remove('password')) : x,
|
|
];
|
|
}
|
|
final sftp = r['sftpServers'];
|
|
if (sftp is List) {
|
|
r['sftpServers'] = [
|
|
for (final x in sftp) x is Map ? (Map.of(x)..remove('password')..remove('privateKey')..remove('keyPassphrase')) : x,
|
|
];
|
|
}
|
|
final smb = r['smbServers'];
|
|
if (smb is List) {
|
|
r['smbServers'] = [
|
|
for (final x in smb) x is Map ? (Map.of(x)..remove('password')) : x,
|
|
];
|
|
}
|
|
final ftp = r['ftpServers'];
|
|
if (ftp is List) {
|
|
r['ftpServers'] = [
|
|
for (final x in ftp) x is Map ? (Map.of(x)..remove('password')) : x,
|
|
];
|
|
}
|
|
final ai = r['aiServices'];
|
|
if (ai is List) {
|
|
r['aiServices'] = [
|
|
for (final x in ai) x is Map ? (Map.of(x)..remove('apiKey')) : x,
|
|
];
|
|
}
|
|
return r;
|
|
}
|
|
|
|
static List<String> _migrated(Map<String, dynamic> j, AppSettings s) => [
|
|
if (j['orientationV2'] != true && j['screenOrientation'] == 'landscape' && s.display.orientation == 'auto')
|
|
'orientation',
|
|
if (j['explorerV2'] != true && j['explorerLayout'] == 'dual' && s.explorer.layout == 'auto') 'explorerLayout',
|
|
if (j['explorerV2'] != true && j['explorerClick'] == 'select' && s.explorer.click == 'open') 'explorerClick',
|
|
if (j['explorerOrientV3'] != true && j['explorerOrientation'] == 'auto') 'explorerOrientation',
|
|
if (j['aiImageV1'] != true && j['components'] is List && !(j['components'] as List).contains('aiimage')) 'aiImage',
|
|
// 53 · 28: 앱 안 브라우저 쿠키를 쓰던 사람에게 넘기는 사이트가 넓어졌음을 한 번 알린다
|
|
if (j['cookieScopeV2'] != true && j.isNotEmpty && (j['ytCookiesBrowser'] ?? internalBrowserCookies) == internalBrowserCookies)
|
|
'cookieScope',
|
|
if (j['zipComicV2'] != true && j['zipComic'] != false && j.containsKey('imageExts') && !s.reader.zipComic) 'zipComic',
|
|
if (j['backgroundV2'] != true && j['runInBackground'] == false && s.run.inBackground && Platform.isAndroid)
|
|
'background',
|
|
// 193: 그 설정이 생기기 전 판 (설정 파일은 있는데 그 항목이 없음 - 예: 10.03) 에서 올라온 사람이 바로 겪는 동작 변화.
|
|
// 예전 판에는 키가 없어 위의 "예전 값 → 새 값" 조건에 걸리지 않았다 (P11: 탐색기에서 한 번 눌렀는데 바로 열려 놀람)
|
|
if (j.isNotEmpty && !j.containsKey('explorerClick') && s.explorer.click == 'open') 'explorerClickNew',
|
|
// E3 (10/22): 선택 규칙의 처음 값으로 옮긴 것 - 규칙 이전 판에서 올라온 사람에게 한 번
|
|
if (j.isNotEmpty && j['explorerRulesV1'] != true && j['explorerAlwaysMarks'] != false) 'explorerMarksRule',
|
|
if (j.isNotEmpty && j['explorerRulesV1'] != true && j['explorerRetapCollapse'] != true) 'explorerRetapRule',
|
|
if (j.isNotEmpty && j['explorerRulesV1'] != true && Platform.isAndroid && j['explorerClick'] != 'select')
|
|
'explorerClickRule',
|
|
if (j.isNotEmpty && j['explorerRulesV1'] != true && (j['explorerLayout'] ?? 'auto') == 'auto') 'explorerLayoutRule',
|
|
if (j.isNotEmpty && !j.containsKey('screenOrientation') && s.display.orientation == 'auto' && Platform.isAndroid) 'orientation',
|
|
if (j.isNotEmpty && !j.containsKey('swipeNav') && s.display.swipeNav) 'swipeNav',
|
|
if (j.isNotEmpty && !j.containsKey('runInBackground') && s.run.inBackground && Platform.isAndroid) 'background',
|
|
// DNS 강제 (사용자 요청 10/10, 처음 값 직접 지정 8.8.8.8 · 8.8.4.4) 가 없던 판에서 올라오면 이름을 묻는 서버가 바뀐다 - 193 과 같이 한 번 알림
|
|
if (j.isNotEmpty && !j.containsKey('dnsMode') && s.network.mode == 'custom') 'dnsForced',
|
|
// 193 · 229: 예전 판을 쓰던 사람 (설정 파일이 있는데 이 항목이 없음) 에게 복사 · 이동이 받는 쪽의 더 새 파일을 건너뛰게 바뀐 것을 한 번
|
|
if (j.isNotEmpty && !j.containsKey('copySkipNewer')) 'copySkipNewer',
|
|
// 193: robocopy 를 쓰던 사람 (Windows) 에게 링크 · junction 을 따라가지 않게 (/XJ) 바뀐 것을 한 번
|
|
if (j['robocopyLinksV1'] != true && Platform.isWindows && _usedRobocopy(j)) 'robocopyLinks',
|
|
];
|
|
|
|
/// 193: 예전 설정에서 robocopy 를 쓰고 있었는지 (복사 방법 · 기억한 복사 · 실시간 동기화 쌍)
|
|
static bool _usedRobocopy(Map<String, dynamic> j) {
|
|
bool robo(Object? x) => x is Map && x['method'] == 'robocopy';
|
|
return j['copyMethodFile'] == 'robocopy' ||
|
|
j['copyMethodFolder'] == 'robocopy' ||
|
|
((j['copyTasks'] as List?) ?? const []).any(robo) ||
|
|
((j['liveSyncPairs'] as List?) ?? const []).any(robo);
|
|
}
|
|
|
|
/// 다운로드 기본 위치: 프로그램 폴더 (쓸 수 없으면 사용자 다운로드 폴더)
|
|
String resolvedDownloadRoot() {
|
|
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);
|
|
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');
|
|
|
|
/// 앱 안 브라우저가 내보낸 YouTube · Google 쿠키 (cookies.txt). 없으면 빈 값
|
|
String get internalCookieFile {
|
|
if (webViewDataDir.isEmpty) return '';
|
|
final f = p.join(webViewDataDir, 'cookies_youtube.txt');
|
|
return File(f).existsSync() ? f : '';
|
|
}
|
|
|
|
/// 앱 안 브라우저의 쿠키가 있는 프로필 폴더 (로그인한 적이 없으면 빈 값)
|
|
String get internalCookieProfile {
|
|
if (webViewDataDir.isEmpty) return '';
|
|
final profile = p.join(webViewDataDir, 'EBWebView', 'Default');
|
|
final hasCookies = File(p.join(profile, 'Network', 'Cookies')).existsSync() ||
|
|
File(p.join(profile, 'Cookies')).existsSync();
|
|
return hasCookies ? profile : '';
|
|
}
|
|
String get aria2Dir => p.join(resolvedDownloadRoot(), 'jj_aria2');
|
|
}
|