설정 A: MKV · 자막 분야 (MkvSettings) - 저장 위치 · 이동 버튼 · 인코딩 · AI 자막 · OpenSubtitles 키를 분야로
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>modular
parent
c5aa8c657b
commit
7fe2f0562f
|
|
@ -72,7 +72,7 @@ void main() {
|
|||
late Future<void> building;
|
||||
await t.runAsync(() async {
|
||||
await c.init();
|
||||
c.settings.maxParallelJobs = 1;
|
||||
c.settings.mkv.maxParallelJobs = 1;
|
||||
await c.addVideos(files);
|
||||
building = c.buildVideos([...c.videos]);
|
||||
for (var i = 0; i < 200 && tool.runs.isEmpty; i++) {
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ void main() {
|
|||
createTranslator: () => throw UnimplementedError(),
|
||||
));
|
||||
c.settings.display.showLog = true;
|
||||
c.settings.moveTargets = [MoveTarget('보관', p.join(tmp.path, '보관'))];
|
||||
c.settings.mkv.moveTargets = [MoveTarget('보관', p.join(tmp.path, '보관'))];
|
||||
for (var i = 1; i <= 4; i++) {
|
||||
c.videos.add(VideoItem(p.join(tmp.path, '아주 긴 이름의 동영상 파일 $i.mp4')));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ class AppController extends ChangeNotifier {
|
|||
SmbRegistry.configure(settings.remote.smbServers);
|
||||
FtpRegistry.configure(settings.remote.ftpServers);
|
||||
outputRootOverride =
|
||||
(settings.mkvOutputRoot?.isNotEmpty ?? false) ? settings.mkvOutputRoot : null;
|
||||
(settings.mkv.outputRoot?.isNotEmpty ?? false) ? settings.mkv.outputRoot : null;
|
||||
}
|
||||
|
||||
void _saveSettings() => settingsStore?.save(settings);
|
||||
|
|
@ -199,14 +199,14 @@ class AppController extends ChangeNotifier {
|
|||
try {
|
||||
settings.webViewDataDir = p.join((await getApplicationSupportDirectory()).path, 'webview');
|
||||
} catch (_) {}
|
||||
encode = settings.encode;
|
||||
encode = settings.mkv.encode;
|
||||
aiOptions = AiOptions(
|
||||
source: languageOf(settings.aiSource),
|
||||
source: languageOf(settings.mkv.aiSource),
|
||||
targets: {
|
||||
for (final c in settings.aiTargets)
|
||||
for (final c in settings.mkv.aiTargets)
|
||||
if (languageOf(c) != undetermined) languageOf(c),
|
||||
}.ifEmpty({for (final c in defaultTargetLanguages) languageOf(c)}),
|
||||
whisper: whisperModels.firstWhere((m) => m.id == settings.aiWhisper,
|
||||
whisper: whisperModels.firstWhere((m) => m.id == settings.mkv.aiWhisper,
|
||||
orElse: () => whisperModels.first),
|
||||
);
|
||||
_applySettings();
|
||||
|
|
@ -284,7 +284,7 @@ class AppController extends ChangeNotifier {
|
|||
}
|
||||
|
||||
void _encodeChanged() {
|
||||
settings.encode = encode;
|
||||
settings.mkv.encode = encode;
|
||||
_saveSettings();
|
||||
notifyListeners();
|
||||
}
|
||||
|
|
@ -888,9 +888,9 @@ class AppController extends ChangeNotifier {
|
|||
if (!aiAvailable || ffmpegVersion == null || targets.isEmpty) return;
|
||||
aiOptions = opts;
|
||||
settings
|
||||
..aiSource = opts.source.code
|
||||
..aiTargets = [for (final t in opts.targets) t.code]
|
||||
..aiWhisper = opts.whisper.id;
|
||||
..mkv.aiSource = opts.source.code
|
||||
..mkv.aiTargets = [for (final t in opts.targets) t.code]
|
||||
..mkv.aiWhisper = opts.whisper.id;
|
||||
_saveSettings();
|
||||
if (busy) {
|
||||
for (final v in targets) {
|
||||
|
|
@ -1391,7 +1391,7 @@ class AppController extends ChangeNotifier {
|
|||
|
||||
/// P0: 결과 MKV 가 이미 있을 때 실제로 쓸 동작 ("묻기" 인데 묻지 못한 곳은 예전 것을 지키는 '다른 이름')
|
||||
String _existingMode(String? asked) {
|
||||
final m = asked ?? settings.mkvExisting;
|
||||
final m = asked ?? settings.mkv.existing;
|
||||
return const ['overwrite', 'skip', 'rename'].contains(m) ? m : 'rename';
|
||||
}
|
||||
|
||||
|
|
@ -1408,7 +1408,7 @@ class AppController extends ChangeNotifier {
|
|||
final queue = only?.where(videos.contains).toList() ??
|
||||
videos.where((v) => v.status != JobStatus.done).toList();
|
||||
if (queue.isEmpty) return;
|
||||
final limit = settings.maxParallelJobs <= 0 ? queue.length : settings.maxParallelJobs;
|
||||
final limit = settings.mkv.maxParallelJobs <= 0 ? queue.length : settings.mkv.maxParallelJobs;
|
||||
if (queue.length > 1) _log(trf('MKV 만들기: {0}개, 동시에 {1}개씩', [queue.length, limit.clamp(1, queue.length)]));
|
||||
_batchQueue = queue;
|
||||
_batchNext = 0;
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ 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/mkv_settings.dart';
|
||||
import 'settings/display_settings.dart';
|
||||
import 'settings/remote_settings.dart';
|
||||
import 'settings/explorer_tool_settings.dart';
|
||||
|
|
@ -25,6 +25,7 @@ import 'settings/explorer_settings.dart';
|
|||
export 'settings/records.dart';
|
||||
export 'settings/settings_group.dart';
|
||||
export 'settings/settings_store.dart';
|
||||
export 'settings/mkv_settings.dart';
|
||||
export 'settings/display_settings.dart';
|
||||
export 'settings/remote_settings.dart';
|
||||
export 'settings/explorer_tool_settings.dart';
|
||||
|
|
@ -41,12 +42,7 @@ 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 = [];
|
||||
|
|
@ -59,15 +55,6 @@ class AppSettings {
|
|||
|
||||
|
||||
|
||||
/// AI 자막: 매번 설정 창을 띄울지 (끄면 마지막 설정으로 바로 시작)
|
||||
bool askAiOptions = true;
|
||||
|
||||
|
||||
|
||||
/// OpenSubtitles (인터넷 자막 검색) - 키는 필수, 아이디·비밀번호는 선택 (받기 횟수 늘어남)
|
||||
String openSubtitlesKey = '';
|
||||
String openSubtitlesUser = '';
|
||||
String openSubtitlesPassword = '';
|
||||
|
||||
|
||||
|
||||
|
|
@ -75,12 +62,11 @@ class AppSettings {
|
|||
|
||||
|
||||
|
||||
/// 동시에 만드는 MKV 수 (0 = 무제한)
|
||||
int maxParallelJobs = 5;
|
||||
|
||||
|
||||
/// 폰 세로 (좁은 화면) MKV 목록 크기: 'auto' 목록은 내용만큼 · 자세히 칸에 최소 높이 (처음 값) · 'small' · 'large' (관리자 판단 10/17)
|
||||
String mkvCompactList = 'auto';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -150,9 +136,6 @@ class AppSettings {
|
|||
// ── 복사 · 이동 (파일 탐색기) · rsync · lsync (Rsync 화면) ──
|
||||
|
||||
|
||||
/// P0: MKV 를 만들 때 결과 MKV 가 이미 있으면 - 'ask' 묻기 (처음 값) · 'overwrite' 덮어쓰기 · 'skip' 건너뛰기 · 'rename' 다른 이름.
|
||||
/// 덮어쓰기여도 다 만든 뒤에만 바꿔 넣는다 (도중에 취소 · 실패하면 예전 것 그대로)
|
||||
String mkvExisting = 'ask';
|
||||
|
||||
|
||||
|
||||
|
|
@ -197,8 +180,6 @@ class AppSettings {
|
|||
/// 65 (Windows): 탐색기에서 지우면 휴지통으로 (끄면 늘 영구 삭제로 묻는다)
|
||||
bool recycleOnDelete = true;
|
||||
|
||||
/// 47: MKV 목록에서 세 번 누르면 'move' (확인 뒤 이동 폴더로) · 'none' (아무것도 안 함)
|
||||
String tripleTapAction = 'move';
|
||||
|
||||
/// 61: 기본 자막 언어 (MKV 의 기본 자막 트랙 · 플레이어가 먼저 켜는 자막). '' = 화면 언어 따르기, 그 밖은 자막 언어 코드 (ko · ja …)
|
||||
String preferredSubtitleLanguage = '';
|
||||
|
|
@ -257,13 +238,7 @@ class AppSettings {
|
|||
/// 확장자별 재생 프로그램 (없으면 내장 플레이어): 'system' 또는 실행 파일 경로
|
||||
Map<String, String> externalPlayers = {};
|
||||
|
||||
/// 마지막 인코딩 설정
|
||||
EncodeSettings encode = const EncodeSettings();
|
||||
|
||||
/// 마지막 AI 자막 설정
|
||||
String aiSource = 'und';
|
||||
List<String> aiTargets = ['ko', 'en', 'ja'];
|
||||
String aiWhisper = 'whisper-base';
|
||||
|
||||
AppSettings();
|
||||
|
||||
|
|
@ -306,35 +281,21 @@ class AppSettings {
|
|||
/// 화면 · 앱 모양 (배율 · 밝기 · 방향 · 언어 · 앱 아이콘 · 컴포넌트 · 화면 순서 · 밀기)
|
||||
final display = DisplaySettings();
|
||||
|
||||
/// MKV · 자막 만들기 (저장 위치 · 이동 버튼 · 인코딩 · AI 자막 · OpenSubtitles)
|
||||
final mkv = MkvSettings();
|
||||
|
||||
/// 설정 분야들 (설정 A) - 저마다 자기 키를 읽고 쓴다. toJson · fromJson 은 이 목록을 돌며 합치기만 한다
|
||||
List<SettingsGroup> get groups => [explorer, network, tor, copy, aiImage, reader, download, browser, run, vault, tools, remote, display];
|
||||
List<SettingsGroup> get groups => [explorer, network, tor, copy, aiImage, reader, download, browser, run, vault, tools, remote, display, mkv];
|
||||
|
||||
/// [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,
|
||||
|
|
@ -342,28 +303,12 @@ class AppSettings {
|
|||
'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);
|
||||
|
|
@ -371,32 +316,13 @@ class AppSettings {
|
|||
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]
|
||||
|
|
@ -407,27 +333,11 @@ class AppSettings {
|
|||
..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';
|
||||
..externalPlayers = ((j['externalPlayers'] as Map?) ?? const {}).cast<String, String>();
|
||||
// 분야를 먼저 읽는다 - 바뀐 기본값 알림 ([_migrated]) 이 분야의 값을 본다
|
||||
for (final g in s.groups) {
|
||||
g.read(j);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
import '../../core/encode_options.dart';
|
||||
import 'records.dart';
|
||||
import 'settings_group.dart';
|
||||
|
||||
/// 약속 (설정 A): 환경 설정 > MKV · 자막 만들기 (저장 위치 · 이동 버튼 · 인코딩 · AI 자막 · OpenSubtitles) - 자기 키 (이름 · 처음 값 · 읽기 · 쓰기) 를 스스로 갖는다.
|
||||
/// 설정 하나를 더할 때는 이 파일만 고친다. 저장 파일의 키 이름은 예전 그대로 (평평한 키)
|
||||
class MkvSettings implements SettingsGroup {
|
||||
/// MKV·자막 저장 위치. null 이면 동영상이 있는 폴더 아래 jj_mkv
|
||||
String? outputRoot;
|
||||
|
||||
/// MKV 세부 정보 오른쪽 아래의 이동 버튼들 (표시 이름 · 폴더). 세 번 누르기는 첫 번째 버튼의 폴더로.
|
||||
/// 비어 있으면 [이동] 을 처음 누를 때 폴더를 골라 하나 만든다.
|
||||
List<MoveTarget> moveTargets = [];
|
||||
|
||||
/// AI 자막: 매번 설정 창을 띄울지 (끄면 마지막 설정으로 바로 시작)
|
||||
bool askAiOptions = true;
|
||||
|
||||
/// OpenSubtitles (인터넷 자막 검색) - 키는 필수, 아이디·비밀번호는 선택 (받기 횟수 늘어남)
|
||||
String openSubtitlesKey = '';
|
||||
String openSubtitlesUser = '';
|
||||
String openSubtitlesPassword = '';
|
||||
|
||||
/// 동시에 만드는 MKV 수 (0 = 무제한)
|
||||
int maxParallelJobs = 5;
|
||||
|
||||
/// 폰 세로 (좁은 화면) MKV 목록 크기: 'auto' 목록은 내용만큼 · 자세히 칸에 최소 높이 (처음 값) · 'small' · 'large' (관리자 판단 10/17)
|
||||
String compactList = 'auto';
|
||||
|
||||
/// P0: MKV 를 만들 때 결과 MKV 가 이미 있으면 - 'ask' 묻기 (처음 값) · 'overwrite' 덮어쓰기 · 'skip' 건너뛰기 · 'rename' 다른 이름.
|
||||
/// 덮어쓰기여도 다 만든 뒤에만 바꿔 넣는다 (도중에 취소 · 실패하면 예전 것 그대로)
|
||||
String existing = 'ask';
|
||||
|
||||
/// 47: MKV 목록에서 세 번 누르면 'move' (확인 뒤 이동 폴더로) · 'none' (아무것도 안 함)
|
||||
String tripleTapAction = 'move';
|
||||
|
||||
/// 마지막 인코딩 설정
|
||||
EncodeSettings encode = const EncodeSettings();
|
||||
|
||||
/// 마지막 AI 자막 설정
|
||||
String aiSource = 'und';
|
||||
List<String> aiTargets = ['ko', 'en', 'ja'];
|
||||
String aiWhisper = 'whisper-base';
|
||||
|
||||
/// 색 보정 값 (-100 ~ 100)
|
||||
static int _adj(Object? v) => ((v as num?)?.round() ?? 0).clamp(-100, 100);
|
||||
|
||||
@override
|
||||
void write(Map<String, Object?> j, {required bool plainSecrets}) {
|
||||
j['mkvOutputRoot'] = outputRoot;
|
||||
j['moveTargets'] = [for (final t in moveTargets) t.toJson()];
|
||||
j['askAiOptions'] = askAiOptions;
|
||||
// 비밀 값은 안전 저장소에 (54). 파일에는 저장해 두었는지만
|
||||
j['openSubtitlesSaved'] = {
|
||||
'key': openSubtitlesKey.isNotEmpty,
|
||||
'user': openSubtitlesUser.isNotEmpty,
|
||||
'password': openSubtitlesPassword.isNotEmpty,
|
||||
};
|
||||
if (plainSecrets) {
|
||||
j['openSubtitlesKey'] = openSubtitlesKey;
|
||||
j['openSubtitlesUser'] = openSubtitlesUser;
|
||||
j['openSubtitlesPassword'] = openSubtitlesPassword;
|
||||
}
|
||||
j['maxParallelJobs'] = maxParallelJobs;
|
||||
j['mkvCompactList'] = compactList;
|
||||
j['mkvExisting'] = existing;
|
||||
j['tripleTapAction'] = tripleTapAction;
|
||||
j['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,
|
||||
};
|
||||
j['aiSource'] = aiSource;
|
||||
j['aiTargets'] = aiTargets;
|
||||
j['aiWhisper'] = aiWhisper;
|
||||
}
|
||||
|
||||
@override
|
||||
void read(Map<String, dynamic> j) {
|
||||
final e = (j['encode'] as Map?) ?? const {};
|
||||
outputRoot = j['mkvOutputRoot'] as String?;
|
||||
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;
|
||||
compactList = const ['auto', 'small', 'large'].contains(j['mkvCompactList']) ? j['mkvCompactList'] as String : 'auto';
|
||||
existing = const ['rename', 'overwrite', 'skip'].contains(j['mkvExisting']) ? j['mkvExisting'] as String : 'ask';
|
||||
tripleTapAction = j['tripleTapAction'] == 'none' ? 'none' : 'move';
|
||||
encode = EncodeSettings(
|
||||
codec: pickEnum(VideoCodecChoice.values, e['codec'], VideoCodecChoice.copy),
|
||||
resolution: pickEnum(ResolutionChoice.values, e['resolution'], ResolutionChoice.original),
|
||||
quality: pickEnum(QualityChoice.values, e['quality'], QualityChoice.normal),
|
||||
frame: pickEnum(FrameChoice.values, e['frame'], FrameChoice.original),
|
||||
fit: pickEnum(FitChoice.values, e['fit'], FitChoice.fill),
|
||||
rotate: pickEnum(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';
|
||||
}
|
||||
}
|
||||
|
|
@ -54,9 +54,9 @@ class SettingsStore {
|
|||
static const _davServers = 'dav.servers';
|
||||
|
||||
static String _os(AppSettings s, String k) => switch (k) {
|
||||
'openSubtitlesKey' => s.openSubtitlesKey,
|
||||
'openSubtitlesUser' => s.openSubtitlesUser,
|
||||
_ => s.openSubtitlesPassword,
|
||||
'openSubtitlesKey' => s.mkv.openSubtitlesKey,
|
||||
'openSubtitlesUser' => s.mkv.openSubtitlesUser,
|
||||
_ => s.mkv.openSubtitlesPassword,
|
||||
};
|
||||
|
||||
/// 지금 설정의 비밀 값 (빈 값은 넣지 않음 = 저장소에서 지움)
|
||||
|
|
@ -109,9 +109,9 @@ class SettingsStore {
|
|||
os[e.key] = v.isNotEmpty ? v : stored[e.value] ?? '';
|
||||
}
|
||||
s
|
||||
..openSubtitlesKey = os['openSubtitlesKey']!
|
||||
..openSubtitlesUser = os['openSubtitlesUser']!
|
||||
..openSubtitlesPassword = os['openSubtitlesPassword']!;
|
||||
..mkv.openSubtitlesKey = os['openSubtitlesKey']!
|
||||
..mkv.openSubtitlesUser = os['openSubtitlesUser']!
|
||||
..mkv.openSubtitlesPassword = os['openSubtitlesPassword']!;
|
||||
s.remote.webdavServers = [
|
||||
for (final x in s.remote.webdavServers)
|
||||
x.password.isNotEmpty ? x : x.copyWith(password: stored[_davKey(x.id)] ?? ''),
|
||||
|
|
|
|||
|
|
@ -114,9 +114,9 @@ class PlatformServices {
|
|||
usage: Platform.isWindows ? WindowsUsage() : null,
|
||||
createSubtitleProviders: (s) => [
|
||||
OpenSubtitlesProvider(
|
||||
apiKey: () => s().openSubtitlesKey,
|
||||
username: () => s().openSubtitlesUser,
|
||||
password: () => s().openSubtitlesPassword,
|
||||
apiKey: () => s().mkv.openSubtitlesKey,
|
||||
username: () => s().mkv.openSubtitlesUser,
|
||||
password: () => s().mkv.openSubtitlesPassword,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
@ -137,9 +137,9 @@ class PlatformServices {
|
|||
shell: AndroidShell(),
|
||||
createSubtitleProviders: (s) => [
|
||||
OpenSubtitlesProvider(
|
||||
apiKey: () => s().openSubtitlesKey,
|
||||
username: () => s().openSubtitlesUser,
|
||||
password: () => s().openSubtitlesPassword,
|
||||
apiKey: () => s().mkv.openSubtitlesKey,
|
||||
username: () => s().mkv.openSubtitlesUser,
|
||||
password: () => s().mkv.openSubtitlesPassword,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ Future<void> showAiDialog(BuildContext context, AppController c, VideoItem curre
|
|||
);
|
||||
// 환경 설정에서 "매번 묻기" 를 끄면 사용자가 고른 대로 묻지 않는다 (마지막 설정으로).
|
||||
// 138: 다만 [MKV 만들기] 와 헷갈려 잘못 누른 것을 되돌릴 수 있게 3초 알림 ([취소]) 뒤에 시작한다.
|
||||
if (!c.settings.askAiOptions) {
|
||||
if (!c.settings.mkv.askAiOptions) {
|
||||
final messenger = ScaffoldMessenger.maybeOf(context);
|
||||
if (messenger == null) return run(list, c.aiOptions);
|
||||
var cancelled = false;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import '../l10n/tr.dart';
|
|||
/// 돌려주는 것: 'overwrite' · 'skip' · 'rename', 이미 있는 것이 없으면 설정 값 그대로, [취소] 면 null (만들지 않음).
|
||||
/// 예전에는 묻지 않고 시작해, 도중에 취소하면 예전에 완성한 MKV 까지 사라졌다
|
||||
Future<String?> askExistingOutputs(BuildContext context, AppController c, List<VideoItem> targets) async {
|
||||
final mode = c.settings.mkvExisting;
|
||||
final mode = c.settings.mkv.existing;
|
||||
if (mode != 'ask') return mode;
|
||||
final have = await c.withExistingOutput(targets);
|
||||
if (have.isEmpty || !context.mounted) return 'ask';
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ class _BookmarksDialogState extends State<_BookmarksDialog> {
|
|||
// 바로 가기: 다운로드 폴더 · MKV 결과 폴더 (넣지 않아도)
|
||||
final shortcuts = [
|
||||
(tr('다운로드'), s.resolvedDownloadRoot()),
|
||||
if (s.mkvOutputRoot?.isNotEmpty ?? false) (tr('MKV 결과 폴더'), s.mkvOutputRoot!),
|
||||
if (s.mkv.outputRoot?.isNotEmpty ?? false) (tr('MKV 결과 폴더'), s.mkv.outputRoot!),
|
||||
].where((x) => !mine.any((b) => !b.isPair && samePath(b.path, x.$2))).toList();
|
||||
final already = mine.any((b) => !b.isPair && samePath(b.path, widget.current));
|
||||
final pairAlready = _rsync && mine.any((b) => b.isPair && samePath(b.path, widget.current) && samePath(b.right!, widget.right!));
|
||||
|
|
|
|||
|
|
@ -861,7 +861,7 @@ class _VideoTile extends StatelessWidget {
|
|||
if (!context.mounted) return;
|
||||
if (n == 2) {
|
||||
playFiles(context, c, [v.path]);
|
||||
} else if (n >= 3 && c.settings.tripleTapAction == 'move') {
|
||||
} else if (n >= 3 && c.settings.mkv.tripleTapAction == 'move') {
|
||||
// 47: 잘못 세 번 눌러도 파일이 옮겨지지 않게 묻는다
|
||||
moveToTarget(context, c, [v], confirm: true);
|
||||
}
|
||||
|
|
@ -1003,7 +1003,7 @@ class _CompactBody extends StatelessWidget {
|
|||
Widget build(BuildContext context) => LayoutBuilder(builder: (context, box) {
|
||||
final h = box.maxHeight;
|
||||
final picked = c.selected != null;
|
||||
final cap = switch (c.settings.mkvCompactList) {
|
||||
final cap = switch (c.settings.mkv.compactList) {
|
||||
'small' => h * 0.25,
|
||||
'large' => h * 0.55,
|
||||
// 자동: 자세히 칸 최소 - 고른 동영상이 있으면 62% (세부 정보 · 자막 버튼이 칸 밖으로 밀리지 않게, 261 · 266),
|
||||
|
|
@ -1011,7 +1011,7 @@ class _CompactBody extends StatelessWidget {
|
|||
_ => h - (picked ? h * 0.62 : math.min(120.0, h * 0.4)),
|
||||
}.clamp(math.min(120.0, h), h).toDouble();
|
||||
// 줄이 많아 내용이 칸보다 확실히 길면 칸 높이 그대로 (shrinkWrap 은 줄을 모두 그려 느리다)
|
||||
final shrink = c.settings.mkvCompactList == 'auto' && c.videos.length * _rowMin < cap;
|
||||
final shrink = c.settings.mkv.compactList == 'auto' && c.videos.length * _rowMin < cap;
|
||||
return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
|
||||
if (shrink)
|
||||
ConstrainedBox(constraints: BoxConstraints(maxHeight: cap), child: _VideoList(c: c, shrink: true))
|
||||
|
|
@ -1088,7 +1088,7 @@ class _VideoDetail extends StatelessWidget {
|
|||
final moving = c.checked.isEmpty ? [v] : c.batchTargets;
|
||||
final canMove = moving.any((x) => x.status != JobStatus.running);
|
||||
final count = c.checked.isEmpty ? '' : ' (${c.checked.length})';
|
||||
final buttons = c.settings.moveTargets;
|
||||
final buttons = c.settings.mkv.moveTargets;
|
||||
Widget button(MoveTarget? t) => Tooltip(
|
||||
message: t == null
|
||||
? tr('이동: 옮길 폴더를 골라 이동 버튼을 만듭니다 (환경 설정 > MKV 만들기 > 이동 버튼에서 여러 개 만들 수 있음)')
|
||||
|
|
@ -1538,13 +1538,13 @@ Future<void> moveToTarget(BuildContext context, AppController c, List<VideoItem>
|
|||
{MoveTarget? target, bool confirm = false}) async {
|
||||
if (videos.isEmpty) return;
|
||||
final messenger = ScaffoldMessenger.maybeOf(context);
|
||||
var t = target ?? c.settings.moveTargets.firstOrNull;
|
||||
var t = target ?? c.settings.mkv.moveTargets.firstOrNull;
|
||||
if (t == null) {
|
||||
final d = await pickFolder(context, tr('이동 폴더 (환경 설정에서 바꿀 수 있음)'));
|
||||
if (d == null) return;
|
||||
t = MoveTarget(p.basename(d).isEmpty ? tr('이동') : p.basename(d), d);
|
||||
final add = t;
|
||||
await c.updateSettings((x) => x.moveTargets = [...x.moveTargets, add]);
|
||||
await c.updateSettings((x) => x.mkv.moveTargets = [...x.mkv.moveTargets, add]);
|
||||
} else if (confirm && context.mounted) {
|
||||
final ok = await confirmAction(
|
||||
context,
|
||||
|
|
|
|||
|
|
@ -146,8 +146,8 @@ Future<void> clearStoredSecrets(AppController c) => c.updateSettings((s) => s
|
|||
..remote.ftpServers = [for (final x in s.remote.ftpServers) x.copyWith(password: '')]
|
||||
// AI 서비스 API 키도 (문구 "비밀번호 · API 키" 와 같게 - 예전에는 남았다, 관리자 결정 10/17)
|
||||
..aiImage.services = [for (final x in s.aiImage.services) x.copyWith(apiKey: '')]
|
||||
..openSubtitlesKey = ''
|
||||
..openSubtitlesPassword = '').then((_) async {
|
||||
..mkv.openSubtitlesKey = ''
|
||||
..mkv.openSubtitlesPassword = '').then((_) async {
|
||||
// 관리자 판단 (10/21): 다른 판 (새 판) 이 안전 저장소에 넣은 이 판이 모르는 비밀번호 · 키도 (마스터 · 최상은 남김)
|
||||
await c.settingsStore?.clearForeignSecrets();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -50,12 +50,12 @@ class _MkvSettingsSectionState extends State<MkvSettingsSection> {
|
|||
title: Text(tr('동영상을 세 번 누르면')),
|
||||
subtitle: Text(tr('한 번: 보기 · 두 번: 재생 · 세 번: 이동 폴더로 옮기기 (옮기기 전에 묻습니다)')),
|
||||
trailing: DropdownButton<String>(
|
||||
value: s.tripleTapAction,
|
||||
value: s.mkv.tripleTapAction,
|
||||
items: [
|
||||
DropdownMenuItem(value: 'move', child: Text(tr('이동 폴더로 (확인 뒤, 기본)'))),
|
||||
DropdownMenuItem(value: 'none', child: Text(tr('아무것도 안 함'))),
|
||||
],
|
||||
onChanged: (v) => c.updateSettings((x) => x.tripleTapAction = v!),
|
||||
onChanged: (v) => c.updateSettings((x) => x.mkv.tripleTapAction = v!),
|
||||
),
|
||||
),
|
||||
// P0: 결과 MKV 가 이미 있을 때 (처음 값: 묻기)
|
||||
|
|
@ -63,14 +63,14 @@ class _MkvSettingsSectionState extends State<MkvSettingsSection> {
|
|||
title: Text(tr('만들 MKV 가 이미 있으면')),
|
||||
subtitle: Text(tr('덮어쓰기여도 다 만든 뒤에만 바꿉니다 (도중에 취소 · 실패하면 예전 MKV 그대로)')),
|
||||
trailing: DropdownButton<String>(
|
||||
value: const ['ask', 'overwrite', 'skip', 'rename'].contains(s.mkvExisting) ? s.mkvExisting : 'ask',
|
||||
value: const ['ask', 'overwrite', 'skip', 'rename'].contains(s.mkv.existing) ? s.mkv.existing : 'ask',
|
||||
items: [
|
||||
DropdownMenuItem(value: 'ask', child: Text(tr('묻기'))),
|
||||
DropdownMenuItem(value: 'overwrite', child: Text(tr('덮어쓰기'))),
|
||||
DropdownMenuItem(value: 'skip', child: Text(tr('건너뛰기'))),
|
||||
DropdownMenuItem(value: 'rename', child: Text(tr('다른 이름 (이름 (2).mkv)'))),
|
||||
],
|
||||
onChanged: (v) => c.updateSettings((x) => x.mkvExisting = v ?? 'ask'),
|
||||
onChanged: (v) => c.updateSettings((x) => x.mkv.existing = v ?? 'ask'),
|
||||
),
|
||||
),
|
||||
// 15 지적 · 관리자 판단 (10/17): 폰 세로에서 목록 · 자세히 칸 나누기
|
||||
|
|
@ -78,21 +78,21 @@ class _MkvSettingsSectionState extends State<MkvSettingsSection> {
|
|||
title: Text(tr('폰 세로 목록 크기')),
|
||||
subtitle: Text(tr('좁은 화면에서 동영상 목록이 차지하는 높이 (자동: 목록은 줄 수만큼, 자세히 칸 자리는 남김)')),
|
||||
trailing: DropdownButton<String>(
|
||||
value: s.mkvCompactList,
|
||||
value: s.mkv.compactList,
|
||||
items: [
|
||||
DropdownMenuItem(value: 'auto', child: Text(tr('자동 (기본)'))),
|
||||
DropdownMenuItem(value: 'small', child: Text(tr('작게'))),
|
||||
DropdownMenuItem(value: 'large', child: Text(tr('크게'))),
|
||||
],
|
||||
onChanged: (v) => c.updateSettings((x) => x.mkvCompactList = v ?? 'auto'),
|
||||
onChanged: (v) => c.updateSettings((x) => x.mkv.compactList = v ?? 'auto'),
|
||||
),
|
||||
),
|
||||
SettingTile(
|
||||
title: Text(tr('동시 MKV 변환 수')),
|
||||
subtitle: Text(tr('여러 동영상을 MKV 로 만들 때 동시에 처리할 개수 (재인코딩은 CPU 를 많이 씁니다)')),
|
||||
trailing: CountSelector(
|
||||
value: s.maxParallelJobs,
|
||||
onChanged: (n) => c.updateSettings((x) => x.maxParallelJobs = n),
|
||||
value: s.mkv.maxParallelJobs,
|
||||
onChanged: (n) => c.updateSettings((x) => x.mkv.maxParallelJobs = n),
|
||||
),
|
||||
),
|
||||
SettingTile(
|
||||
|
|
@ -101,16 +101,16 @@ class _MkvSettingsSectionState extends State<MkvSettingsSection> {
|
|||
),
|
||||
settingsFolderTile(
|
||||
title: tr('MKV · 자막 (jj_mkv)'),
|
||||
value: s.mkvOutputRoot,
|
||||
value: s.mkv.outputRoot,
|
||||
defaultText: tr('동영상이 있는 폴더 아래 jj_mkv (기본)'),
|
||||
onPick: () async {
|
||||
final d = await pickFolder(context, tr('MKV · 자막 저장 위치'), s.mkvOutputRoot);
|
||||
if (d != null) await c.updateSettings((x) => x.mkvOutputRoot = d);
|
||||
final d = await pickFolder(context, tr('MKV · 자막 저장 위치'), s.mkv.outputRoot);
|
||||
if (d != null) await c.updateSettings((x) => x.mkv.outputRoot = d);
|
||||
},
|
||||
onReset: () => c.updateSettings((x) => x.mkvOutputRoot = null),
|
||||
onReset: () => c.updateSettings((x) => x.mkv.outputRoot = null),
|
||||
),
|
||||
settingsSubTitle(tr('이동 버튼 (세부 정보 오른쪽 아래, 위로 하나씩)')),
|
||||
for (var i = 0; i < s.moveTargets.length; i++) MoveTargetTile(c: c, index: i, target: s.moveTargets[i]),
|
||||
for (var i = 0; i < s.mkv.moveTargets.length; i++) MoveTargetTile(c: c, index: i, target: s.mkv.moveTargets[i]),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 4, 16, 8),
|
||||
child: Row(children: [
|
||||
|
|
@ -119,7 +119,7 @@ class _MkvSettingsSectionState extends State<MkvSettingsSection> {
|
|||
final d = await pickFolder(context, tr('이동 버튼의 폴더'));
|
||||
if (d == null) return;
|
||||
final name = p.basename(d).isEmpty ? tr('이동') : p.basename(d);
|
||||
await c.updateSettings((x) => x.moveTargets = [...x.moveTargets, MoveTarget(name, d)]);
|
||||
await c.updateSettings((x) => x.mkv.moveTargets = [...x.mkv.moveTargets, MoveTarget(name, d)]);
|
||||
},
|
||||
icon: const Icon(Icons.add, size: 18),
|
||||
label: Text(tr('이동 버튼 추가')),
|
||||
|
|
@ -127,7 +127,7 @@ class _MkvSettingsSectionState extends State<MkvSettingsSection> {
|
|||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
s.moveTargets.isEmpty
|
||||
s.mkv.moveTargets.isEmpty
|
||||
? tr('없음: 세부 정보의 [이동] 을 처음 누를 때 폴더를 골라 하나 만듭니다')
|
||||
: tr('표시 이름은 바로 고칠 수 있습니다. 동영상 줄을 세 번 누르면 첫 번째 버튼의 폴더로 옮깁니다'),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim),
|
||||
|
|
@ -161,8 +161,8 @@ class _SubtitleSettingsSectionState extends State<SubtitleSettingsSection> {
|
|||
final s = c.settings;
|
||||
return Column(crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [
|
||||
SwitchListTile(
|
||||
value: s.askAiOptions,
|
||||
onChanged: (v) => c.updateSettings((x) => x.askAiOptions = v),
|
||||
value: s.mkv.askAiOptions,
|
||||
onChanged: (v) => c.updateSettings((x) => x.mkv.askAiOptions = v),
|
||||
title: Text(tr('시작할 때마다 설정 창 보기')),
|
||||
subtitle: Text(trf('끄면 마지막 설정으로 바로 시작: 원어 {0}, ' '언어 {1}, {2}', [c.aiOptions.source.name, c.aiOptions.targets.map((t) => t.code).join('/'), c.aiOptions.whisper.label])),
|
||||
),
|
||||
|
|
@ -170,13 +170,13 @@ class _SubtitleSettingsSectionState extends State<SubtitleSettingsSection> {
|
|||
settingsSubTitle(tr('받은 AI 자막 모델')),
|
||||
AiSubtitleModels(c: c),
|
||||
settingsSubTitle(tr('인터넷 자막 (OpenSubtitles.com)')),
|
||||
settingsTextTile(tr('API 키 (필수, 무료)'), s.openSubtitlesKey,
|
||||
settingsTextTile(tr('API 키 (필수, 무료)'), s.mkv.openSubtitlesKey,
|
||||
tr('가입 → 프로필 → API consumers → New consumer'),
|
||||
(v) => c.updateSettings((x) => x.openSubtitlesKey = v.trim())),
|
||||
settingsTextTile(tr('아이디 (선택)'), s.openSubtitlesUser, tr('로그인하면 하루 받기 횟수가 늘어납니다'),
|
||||
(v) => c.updateSettings((x) => x.openSubtitlesUser = v.trim())),
|
||||
settingsTextTile(tr('비밀번호 (선택)'), s.openSubtitlesPassword, trf('이 {0}의 안전 저장소에 저장됩니다 (설정 파일에는 쓰지 않음)', [desk ? 'PC' : tr('기기')]),
|
||||
(v) => c.updateSettings((x) => x.openSubtitlesPassword = v), obscure: true),
|
||||
(v) => c.updateSettings((x) => x.mkv.openSubtitlesKey = v.trim())),
|
||||
settingsTextTile(tr('아이디 (선택)'), s.mkv.openSubtitlesUser, tr('로그인하면 하루 받기 횟수가 늘어납니다'),
|
||||
(v) => c.updateSettings((x) => x.mkv.openSubtitlesUser = v.trim())),
|
||||
settingsTextTile(tr('비밀번호 (선택)'), s.mkv.openSubtitlesPassword, trf('이 {0}의 안전 저장소에 저장됩니다 (설정 파일에는 쓰지 않음)', [desk ? 'PC' : tr('기기')]),
|
||||
(v) => c.updateSettings((x) => x.mkv.openSubtitlesPassword = v), obscure: true),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -273,7 +273,7 @@ class MoveTargetTile extends StatelessWidget {
|
|||
final i = index, t = target;
|
||||
final s = c.settings;
|
||||
void update(List<MoveTarget> Function(List<MoveTarget>) f) =>
|
||||
c.updateSettings((x) => x.moveTargets = f([...x.moveTargets]));
|
||||
c.updateSettings((x) => x.mkv.moveTargets = f([...x.mkv.moveTargets]));
|
||||
return SettingTile(
|
||||
key: ValueKey('move_${i}_${t.dir}'),
|
||||
leading: Icon(Icons.drive_file_move_outline, color: JjColors.accent),
|
||||
|
|
@ -302,7 +302,7 @@ class MoveTargetTile extends StatelessWidget {
|
|||
IconButton(
|
||||
tooltip: tr('이 이동 버튼 지우기'),
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
onPressed: s.moveTargets.length > i ? () => update((l) => l..removeAt(i)) : null,
|
||||
onPressed: s.mkv.moveTargets.length > i ? () => update((l) => l..removeAt(i)) : null,
|
||||
),
|
||||
]),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class _SearchDialogState extends State<_SearchDialog> {
|
|||
final _episode = TextEditingController();
|
||||
final _key = TextEditingController();
|
||||
late Set<Language> _langs = {
|
||||
for (final code in widget.c.settings.aiTargets)
|
||||
for (final code in widget.c.settings.mkv.aiTargets)
|
||||
if (languageOf(code) != undetermined) languageOf(code),
|
||||
};
|
||||
String? _hash;
|
||||
|
|
@ -169,7 +169,7 @@ class _SearchDialogState extends State<_SearchDialog> {
|
|||
FilledButton(
|
||||
onPressed: () async {
|
||||
if (_key.text.trim().isEmpty) return;
|
||||
await c.updateSettings((s) => s.openSubtitlesKey = _key.text.trim());
|
||||
await c.updateSettings((s) => s.mkv.openSubtitlesKey = _key.text.trim());
|
||||
setState(() {});
|
||||
_search();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ void main() {
|
|||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings.askAiOptions = false;
|
||||
c.settings.mkv.askAiOptions = false;
|
||||
final v1 = VideoItem(r'C:\v\a.mp4')..info = const MediaInfo(duration: Duration(minutes: 30));
|
||||
final v2 = VideoItem(r'C:\v\b.mp4')..info = const MediaInfo(duration: Duration(minutes: 30));
|
||||
late BuildContext ctx;
|
||||
|
|
@ -61,7 +61,7 @@ void main() {
|
|||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
expect(c.settings.askAiOptions, isTrue);
|
||||
expect(c.settings.mkv.askAiOptions, isTrue);
|
||||
final v1 = VideoItem(r'C:\v\a.mp4')..info = const MediaInfo(duration: Duration(hours: 1));
|
||||
late BuildContext ctx;
|
||||
await tester.pumpWidget(MaterialApp(home: Scaffold(body: Builder(builder: (context) {
|
||||
|
|
|
|||
|
|
@ -263,7 +263,7 @@ void main() {
|
|||
}
|
||||
final c = AppController(PlatformServices(mediaTool: tool, storage: DesktopStorageService()));
|
||||
await c.init();
|
||||
c.settings.maxParallelJobs = 1;
|
||||
c.settings.mkv.maxParallelJobs = 1;
|
||||
c.encode = const EncodeSettings(codec: VideoCodecChoice.h264, quality: QualityChoice.high);
|
||||
await c.addVideos(files);
|
||||
final [v1, v2, v3] = c.videos;
|
||||
|
|
@ -304,7 +304,7 @@ void main() {
|
|||
}
|
||||
c = AppController(PlatformServices(mediaTool: tool, storage: DesktopStorageService()));
|
||||
await c.init();
|
||||
c.settings.maxParallelJobs = 1;
|
||||
c.settings.mkv.maxParallelJobs = 1;
|
||||
c.encode = const EncodeSettings(codec: VideoCodecChoice.h264, quality: QualityChoice.high);
|
||||
await c.addVideos(files);
|
||||
return true;
|
||||
|
|
@ -386,7 +386,7 @@ void main() {
|
|||
}
|
||||
final c = AppController(PlatformServices(mediaTool: tool, storage: DesktopStorageService()));
|
||||
await c.init();
|
||||
c.settings.maxParallelJobs = 1;
|
||||
c.settings.mkv.maxParallelJobs = 1;
|
||||
c.encode = const EncodeSettings(codec: VideoCodecChoice.h264, quality: QualityChoice.high);
|
||||
await c.addVideos(files);
|
||||
final [v1, v2, v3] = c.videos;
|
||||
|
|
@ -442,7 +442,7 @@ void main() {
|
|||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings.moveTargets = [for (var i = 0; i < targets; i++) MoveTarget('보관$i', 'D:\\m$i')];
|
||||
c.settings.mkv.moveTargets = [for (var i = 0; i < targets; i++) MoveTarget('보관$i', 'D:\\m$i')];
|
||||
c.videos.add(VideoItem(r'D:\v\a.mp4')
|
||||
..info = const MediaInfo(duration: Duration(minutes: 3), streams: [
|
||||
StreamInfo(index: 0, type: 'video', codec: 'h264', width: 1920, height: 1080),
|
||||
|
|
@ -750,8 +750,8 @@ void main() {
|
|||
final c = AppController(PlatformServices(mediaTool: tool, storage: DesktopStorageService()));
|
||||
await c.init();
|
||||
c.settings
|
||||
..maxParallelJobs = 1
|
||||
..mkvExisting = 'overwrite';
|
||||
..mkv.maxParallelJobs = 1
|
||||
..mkv.existing = 'overwrite';
|
||||
await c.addVideos(files);
|
||||
final [v1, v2, v3] = c.videos;
|
||||
File(outputMkvPath(v1.path))
|
||||
|
|
@ -800,7 +800,7 @@ void main() {
|
|||
late Future<void> building;
|
||||
await tester.runAsync(() async {
|
||||
await c.init();
|
||||
c.settings.maxParallelJobs = 1;
|
||||
c.settings.mkv.maxParallelJobs = 1;
|
||||
await c.addVideos(files);
|
||||
building = c.buildVideos([...c.videos]);
|
||||
for (var i = 0; i < 200 && tool.runs.isEmpty; i++) {
|
||||
|
|
@ -850,7 +850,7 @@ void main() {
|
|||
late Future<void> building;
|
||||
await tester.runAsync(() async {
|
||||
await c.init();
|
||||
c.settings.maxParallelJobs = 5;
|
||||
c.settings.mkv.maxParallelJobs = 5;
|
||||
await c.addVideos(files);
|
||||
building = c.buildVideos([...c.videos]);
|
||||
for (var i = 0; i < 200 && tool.runs.length < 2; i++) {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ void main() {
|
|||
s.rememberPasswords, s.recycleOnDelete, s.reader.systemBars,
|
||||
], everyElement(isTrue));
|
||||
expect(s.cleanupPrechecked, ['work', 'download']);
|
||||
expect(s.tripleTapAction, 'move');
|
||||
expect(s.mkv.tripleTapAction, 'move');
|
||||
expect(s.remote.davExternalOpen, 'ask');
|
||||
// 이미 있던 설정의 기본값도 결정대로
|
||||
expect([s.browser.pauseOnLeave, s.run.autoCheckUpdates, s.browser.adSkip, s.browser.adHide, s.run.inBackground, s.display.swipeNav],
|
||||
|
|
@ -49,12 +49,12 @@ void main() {
|
|||
..rememberPasswords = false
|
||||
..recycleOnDelete = false
|
||||
..cleanupPrechecked = ['logs']
|
||||
..tripleTapAction = 'none'
|
||||
..mkv.tripleTapAction = 'none'
|
||||
..remote.davExternalOpen = 'url')
|
||||
.toJson());
|
||||
expect([b.run.startMenuShortcut, b.copy.syncStopToast, b.display.swipeWrap, b.migrationNotice, b.allowInnerToOuter, b.rememberPasswords,
|
||||
b.recycleOnDelete], everyElement(isFalse));
|
||||
expect([b.cleanupPrechecked, b.tripleTapAction, b.remote.davExternalOpen], [['logs'], 'none', 'url']);
|
||||
expect([b.cleanupPrechecked, b.mkv.tripleTapAction, b.remote.davExternalOpen], [['logs'], 'none', 'url']);
|
||||
});
|
||||
|
||||
test('안쪽 → 바깥 허용을 끄면 늘 막는다', () {
|
||||
|
|
|
|||
|
|
@ -139,20 +139,20 @@ https://example.com/page.html
|
|||
final store = SettingsStore(p.join(dir.path, 'settings.json'));
|
||||
expect((await store.load()).download.clipboardWatch, isTrue); // 파일 없으면 기본값
|
||||
final s = AppSettings()
|
||||
..mkvOutputRoot = r'D:\out'
|
||||
..mkv.outputRoot = r'D:\out'
|
||||
..download.root = r'D:\dl'
|
||||
..download.clipboardWatch = false
|
||||
..run.showHotkey = 'Alt+F9'
|
||||
..askAiOptions = false
|
||||
..encode = const EncodeSettings(codec: VideoCodecChoice.vp9, resolution: ResolutionChoice.p720)
|
||||
..aiTargets = ['ko', 'fr'];
|
||||
..mkv.askAiOptions = false
|
||||
..mkv.encode = const EncodeSettings(codec: VideoCodecChoice.vp9, resolution: ResolutionChoice.p720)
|
||||
..mkv.aiTargets = ['ko', 'fr'];
|
||||
await store.save(s);
|
||||
final l = await store.load();
|
||||
expect([l.mkvOutputRoot, l.download.root, l.download.clipboardWatch, l.run.showHotkey, l.askAiOptions],
|
||||
expect([l.mkv.outputRoot, l.download.root, l.download.clipboardWatch, l.run.showHotkey, l.mkv.askAiOptions],
|
||||
[r'D:\out', r'D:\dl', false, 'Alt+F9', false]);
|
||||
expect(l.encode.codec, VideoCodecChoice.vp9);
|
||||
expect(l.encode.resolution, ResolutionChoice.p720);
|
||||
expect(l.aiTargets, ['ko', 'fr']);
|
||||
expect(l.mkv.encode.codec, VideoCodecChoice.vp9);
|
||||
expect(l.mkv.encode.resolution, ResolutionChoice.p720);
|
||||
expect(l.mkv.aiTargets, ['ko', 'fr']);
|
||||
expect(l.ytDlpDir, p.join(r'D:\dl', 'jj_yt-dlp'));
|
||||
expect(l.aria2Dir, p.join(r'D:\dl', 'jj_aria2'));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -191,9 +191,9 @@ void main() {
|
|||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..remote.webdavServers = [const DavServer(id: 'n', name: 'NAS', url: 'https://nas', user: 'u', password: 'pw')]
|
||||
..openSubtitlesKey = 'k'
|
||||
..openSubtitlesUser = 'ou'
|
||||
..openSubtitlesPassword = 'op';
|
||||
..mkv.openSubtitlesKey = 'k'
|
||||
..mkv.openSubtitlesUser = 'ou'
|
||||
..mkv.openSubtitlesPassword = 'op';
|
||||
return c;
|
||||
}
|
||||
|
||||
|
|
@ -256,7 +256,7 @@ void main() {
|
|||
expect(find.text('초기화했습니다'), findsOneWidget);
|
||||
expect(c.settings.remote.webdavServers.single.password, '');
|
||||
expect(c.settings.remote.webdavServers.single.user, 'u');
|
||||
expect((c.settings.openSubtitlesKey, c.settings.openSubtitlesPassword, c.settings.openSubtitlesUser), ('', '', 'ou'));
|
||||
expect((c.settings.mkv.openSubtitlesKey, c.settings.mkv.openSubtitlesPassword, c.settings.mkv.openSubtitlesUser), ('', '', 'ou'));
|
||||
expect(lock.hasMaster, isFalse);
|
||||
});
|
||||
|
||||
|
|
@ -283,8 +283,8 @@ void main() {
|
|||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()),
|
||||
settingsStore: store);
|
||||
await c.updateSettings((s) => s
|
||||
..openSubtitlesKey = 'k'
|
||||
..openSubtitlesPassword = 'op'
|
||||
..mkv.openSubtitlesKey = 'k'
|
||||
..mkv.openSubtitlesPassword = 'op'
|
||||
..remote.webdavServers = [const DavServer(id: 'd1', name: 'n', url: 'https://x', user: 'u', password: 'dp')]
|
||||
..remote.sftpServers = [const SftpServer(id: 's1', name: 'n', host: 'h', user: 'u', password: 'sp', privateKey: 'PRIVATE KEY', keyPassphrase: 'kp')]
|
||||
..aiImage.services = [const AiService(id: 'a1', name: 'n', url: 'https://ai', kind: 'openai', apiKey: 'ak')]);
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ void main() {
|
|||
));
|
||||
c.settings
|
||||
..display.showLog = true
|
||||
..mkvCompactList = size;
|
||||
..mkv.compactList = size;
|
||||
for (var i = 0; i < n; i++) {
|
||||
c.videos.add(VideoItem('D:/v/v$i.mp4'));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ void main() {
|
|||
createTranslator: () => throw UnimplementedError(),
|
||||
));
|
||||
c.settings.display.showLog = true;
|
||||
c.settings.moveTargets = [MoveTarget('보관', r'D:\보관')];
|
||||
c.settings.mkv.moveTargets = [MoveTarget('보관', r'D:\보관')];
|
||||
for (var i = 1; i <= 4; i++) {
|
||||
c.videos.add(VideoItem('D:/v/아주 긴 이름의 동영상 파일 $i.mp4'));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -142,10 +142,10 @@ void main() {
|
|||
|
||||
test('이동 버튼 설정 저장 (표시 이름 · 폴더 · 순서), 예전 이동 폴더 하나는 "이동" 버튼으로', () {
|
||||
final s = AppSettings()
|
||||
..moveTargets = const [MoveTarget('완료', r'D:\done'), MoveTarget('보관', r'E:\archive')];
|
||||
expect(AppSettings.fromJson(s.toJson()).moveTargets, s.moveTargets);
|
||||
expect(AppSettings().moveTargets, isEmpty);
|
||||
expect(AppSettings.fromJson({'moveTargetDir': r'D:\old'}).moveTargets, const [MoveTarget('이동', r'D:\old')]);
|
||||
..mkv.moveTargets = const [MoveTarget('완료', r'D:\done'), MoveTarget('보관', r'E:\archive')];
|
||||
expect(AppSettings.fromJson(s.toJson()).mkv.moveTargets, s.mkv.moveTargets);
|
||||
expect(AppSettings().mkv.moveTargets, isEmpty);
|
||||
expect(AppSettings.fromJson({'moveTargetDir': r'D:\old'}).mkv.moveTargets, const [MoveTarget('이동', r'D:\old')]);
|
||||
});
|
||||
|
||||
testWidgets('MKV 목록: 세 번 누르면 첫 번째 이동 버튼으로 · 세부 정보의 이동 버튼 (표시 이름) 은 그 폴더로', (tester) async {
|
||||
|
|
@ -155,7 +155,7 @@ void main() {
|
|||
final c = _controller();
|
||||
final dest = p.join(dir.path, 'done');
|
||||
final archive = p.join(dir.path, 'archive');
|
||||
c.settings.moveTargets = [MoveTarget('완료함', dest), MoveTarget('보관함', archive)];
|
||||
c.settings.mkv.moveTargets = [MoveTarget('완료함', dest), MoveTarget('보관함', archive)];
|
||||
final a = VideoItem(make('a.mp4').path);
|
||||
final b = VideoItem(make('b.mp4').path);
|
||||
final d = VideoItem(make('d.mp4').path);
|
||||
|
|
@ -200,7 +200,7 @@ void main() {
|
|||
}
|
||||
expect(File(p.join(dest, 'b.mp4')).existsSync(), isTrue);
|
||||
// 102: 세 번 누르기를 "아무것도 안 함" 으로 하면 묻지도 옮기지도 않는다
|
||||
c.settings.tripleTapAction = 'none';
|
||||
c.settings.mkv.tripleTapAction = 'none';
|
||||
final dTile = find.text('d.mp4').first;
|
||||
for (var i = 0; i < 3; i++) {
|
||||
await tester.tap(dTile);
|
||||
|
|
@ -210,7 +210,7 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
expect(find.text('파일 옮기기'), findsNothing);
|
||||
expect(File(p.join(dest, 'd.mp4')).existsSync(), isFalse);
|
||||
c.settings.tripleTapAction = 'move';
|
||||
c.settings.mkv.tripleTapAction = 'move';
|
||||
expect(c.videos.map((v) => v.fileName), ['a.mp4', 'd.mp4']);
|
||||
|
||||
// 세부 정보 오른쪽 아래: 표시 이름 버튼 두 개 (첫 번째가 아래, 두 번째가 그 위)
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ void main() {
|
|||
final tool = _Tool();
|
||||
final c = AppController(PlatformServices(mediaTool: tool, storage: DesktopStorageService()));
|
||||
await c.init();
|
||||
c.settings.maxParallelJobs = limit;
|
||||
c.settings.mkv.maxParallelJobs = limit;
|
||||
final dir = Directory.systemTemp.createTempSync('jj_par_');
|
||||
for (var i = 0; i < 5; i++) {
|
||||
final f = File(p.join(dir.path, 'v$i.mp4'))..writeAsStringSync(''); // 없는 파일은 만들지 않으므로
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ void main() {
|
|||
'futureFeature': {'x': 1},
|
||||
}));
|
||||
final s = await store().load();
|
||||
expect(s.openSubtitlesKey, 'KEY-1');
|
||||
expect(s.openSubtitlesPassword, 'os-pw');
|
||||
expect(s.mkv.openSubtitlesKey, 'KEY-1');
|
||||
expect(s.mkv.openSubtitlesPassword, 'os-pw');
|
||||
expect(s.remote.webdavServers.single.password, 'dav-pw');
|
||||
expect(secrets.values.values, containsAll(['KEY-1', 'me', 'os-pw', 'dav-pw']));
|
||||
for (final f in [file, '$file.bak']) {
|
||||
|
|
@ -51,14 +51,14 @@ void main() {
|
|||
// 다시 켜도 그대로
|
||||
final again = await store().load();
|
||||
expect(again.remote.webdavServers.single.password, 'dav-pw');
|
||||
expect(again.openSubtitlesKey, 'KEY-1');
|
||||
expect(again.mkv.openSubtitlesKey, 'KEY-1');
|
||||
});
|
||||
|
||||
test('예전 판이 서버 목록 · 비밀 값을 지운 설정 파일을 써도 되살아난다', () async {
|
||||
final st = store();
|
||||
await st.load();
|
||||
await st.save(AppSettings()
|
||||
..openSubtitlesKey = 'KEY-2'
|
||||
..mkv.openSubtitlesKey = 'KEY-2'
|
||||
..remote.webdavServers = [const DavServer(id: 'd1', name: 'nas', url: 'https://nas', user: 'u', password: 'pw')]);
|
||||
// 예전 판: 모르는 항목을 지우고 저장
|
||||
File(file).writeAsStringSync(jsonEncode({'homeUrl': 'https://old.example'}));
|
||||
|
|
@ -66,7 +66,7 @@ void main() {
|
|||
expect(s.browser.homeUrl, 'https://old.example');
|
||||
expect(s.remote.webdavServers.single.url, 'https://nas');
|
||||
expect(s.remote.webdavServers.single.password, 'pw');
|
||||
expect(s.openSubtitlesKey, 'KEY-2');
|
||||
expect(s.mkv.openSubtitlesKey, 'KEY-2');
|
||||
// 사용자가 일부러 서버를 모두 지웠으면 (목록 항목은 있음) 되살리지 않는다
|
||||
final st2 = store();
|
||||
final s2 = await st2.load();
|
||||
|
|
@ -81,11 +81,11 @@ void main() {
|
|||
const DavServer(id: 'a', name: 'a', url: 'https://a', password: 'pa'),
|
||||
const DavServer(id: 'b', name: 'b', url: 'https://b', password: 'pb'),
|
||||
];
|
||||
s.openSubtitlesPassword = 'x';
|
||||
s.mkv.openSubtitlesPassword = 'x';
|
||||
await st.save(s);
|
||||
expect(secrets.values.values, containsAll(['pa', 'pb', 'x']));
|
||||
s.remote.webdavServers = [s.remote.webdavServers.first];
|
||||
s.openSubtitlesPassword = '';
|
||||
s.mkv.openSubtitlesPassword = '';
|
||||
await st.save(s);
|
||||
expect(secrets.values.values, isNot(contains('pb')));
|
||||
expect(secrets.values.values, isNot(contains('x')));
|
||||
|
|
@ -103,7 +103,7 @@ void main() {
|
|||
}));
|
||||
final st = store();
|
||||
final s = await st.load();
|
||||
expect(s.openSubtitlesKey, 'KEY-3');
|
||||
expect(s.mkv.openSubtitlesKey, 'KEY-3');
|
||||
await st.save(s..browser.homeUrl = 'https://x.example');
|
||||
expect(text(file), contains('KEY-3'));
|
||||
expect(text(file), contains('plain'));
|
||||
|
|
|
|||
|
|
@ -71,9 +71,9 @@ void main() {
|
|||
final secrets = MemorySecretStore()..values['sec.master'] = 'hash';
|
||||
final store = SettingsStore(p.join(tmp.path, 'settings.json'), secrets);
|
||||
final s = await store.load()
|
||||
..openSubtitlesKey = 'k'
|
||||
..openSubtitlesUser = 'u'
|
||||
..openSubtitlesPassword = 'op'
|
||||
..mkv.openSubtitlesKey = 'k'
|
||||
..mkv.openSubtitlesUser = 'u'
|
||||
..mkv.openSubtitlesPassword = 'op'
|
||||
..remote.webdavServers = [const DavServer(id: 'd1', name: 'n', url: 'https://x', user: 'u', password: 'dp')]
|
||||
..remote.sftpServers = [const SftpServer(id: 's1', name: 'n', host: 'h', user: 'u', password: 'sp', privateKey: 'KEY', keyPassphrase: 'kp')]
|
||||
..remote.smbServers = [const SmbServer(id: 'm1', name: 'n', host: 'h', share: 'v', password: 'mp')]
|
||||
|
|
|
|||
|
|
@ -181,13 +181,13 @@ void main() {
|
|||
addTearDown(() => dir.deleteSync(recursive: true));
|
||||
final video = File(p.join(dir.path, 'Movie.2010.mkv'))..writeAsBytesSync(List.filled(200000, 7));
|
||||
|
||||
final settings = AppSettings()..openSubtitlesKey = 'KEY';
|
||||
final settings = AppSettings()..mkv.openSubtitlesKey = 'KEY';
|
||||
final c = AppController(PlatformServices(
|
||||
mediaTool: tool,
|
||||
storage: DesktopStorageService(),
|
||||
createSubtitleProviders: (s) => [
|
||||
OpenSubtitlesProvider(
|
||||
apiKey: () => s().openSubtitlesKey, username: () => '', password: () => '', baseUrl: api.base),
|
||||
apiKey: () => s().mkv.openSubtitlesKey, username: () => '', password: () => '', baseUrl: api.base),
|
||||
],
|
||||
));
|
||||
c.settings = settings;
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ void main() {
|
|||
|
||||
// 체크한 것이 없으면 세부 정보에 보이는 동영상을 바로 만든다 (묻지 않음).
|
||||
// P0: 이 시험의 가짜 저장소는 모든 파일이 "있다" 고 하므로, 결과 MKV 가 있을 때 묻지 않게 덮어쓰기로
|
||||
c.settings.mkvExisting = 'overwrite';
|
||||
c.settings.mkv.existing = 'overwrite';
|
||||
await tester.tap(find.text('MKV 만들기'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('전체 만들기'), findsNothing);
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ void main() {
|
|||
|
||||
test('설정 저장: 화면 비율 · 맞추기 · 회전 · 색 보정', () {
|
||||
final s = AppSettings()
|
||||
..encode = const EncodeSettings(
|
||||
..mkv.encode = const EncodeSettings(
|
||||
codec: VideoCodecChoice.h264,
|
||||
frame: FrameChoice.square,
|
||||
fit: FitChoice.blur,
|
||||
|
|
@ -40,10 +40,10 @@ void main() {
|
|||
contrast: -5,
|
||||
saturation: 30,
|
||||
temperature: 100);
|
||||
final e = AppSettings.fromJson(s.toJson()).encode;
|
||||
final e = AppSettings.fromJson(s.toJson()).mkv.encode;
|
||||
expect([e.frame, e.fit, e.rotate], [FrameChoice.square, FitChoice.blur, RotateChoice.ccw90]);
|
||||
expect([e.brightness, e.contrast, e.saturation, e.temperature], [10, -5, 30, 100]);
|
||||
expect(AppSettings.fromJson({'encode': {'saturation': 999}}).encode.saturation, 100);
|
||||
expect(AppSettings.fromJson({'encode': {'saturation': 999}}).mkv.encode.saturation, 100);
|
||||
});
|
||||
|
||||
// 실제 FFmpeg 로 만들어 보고 출력 크기 확인
|
||||
|
|
|
|||
Loading…
Reference in New Issue