MKV 취소 창은 창을 연 때의 동영상만 취소 · 여럿이 돌면 묻기 · 줄마다 ■ (239 · 240)
- 239 (버그, 기기 확인): 창을 보는 사이 다음 동영상이 시작하면 [지금 것만 취소] 가 그 다음 것을 취소했다 - FFmpeg 실행에 꼬리표 (MediaTool tag · cancelTagged), 이미 끝났으면 알리고 다시 묻기 - 240 (사용자 편의, 기기 확인): 여럿이 동시에 돌면 [그대로 두기] · [모두 취소 (n개)], 목록 줄의 ■ 로 하나만 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>fix_mkv
parent
976f4633aa
commit
54c4d6f97f
|
|
@ -323,6 +323,7 @@
|
|||
"{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2} (change at the top of the main screen)",
|
||||
"{0} · {1}배 · {2}": "{0} · {1}× · {2}",
|
||||
"{0} → {1} 번역 {2}%": "{0} → {1} translation {2}%",
|
||||
"{0} 는 이미 끝났습니다": "{0} has already finished",
|
||||
"{0} 라이선스": "{0} license",
|
||||
"{0} 로 번역 중 {1}%": "Translating to {0} {1}%",
|
||||
"{0} 로 표현할 수 없는 글자 {1}개가 \"?\" 로 바뀝니다.\n계속 저장할까요? (UTF-8 을 권장합니다)": "{1} characters that cannot be represented in {0} will become \"?\".\nSave anyway? (UTF-8 is recommended)",
|
||||
|
|
@ -857,6 +858,7 @@
|
|||
"모두 종료": "Quit all",
|
||||
"모두 지우기": "Clear all",
|
||||
"모두 초기화": "Reset all",
|
||||
"모두 취소 ({0}개)": "Cancel all ({0})",
|
||||
"모두 취소 (대기 {0}개 포함)": "Cancel all (including {0} waiting)",
|
||||
"모두 풀기": "Extract all",
|
||||
"모든 작업 취소": "Cancel all jobs",
|
||||
|
|
@ -1454,6 +1456,7 @@
|
|||
"이 기기의 안전 저장소를 쓸 수 없어 비밀번호를 저장하지 못했습니다": "Couldn't save the password because this device's secure storage is unavailable",
|
||||
"이 기기의 폴더": "Folder on this device",
|
||||
"이 단축키를 등록할 수 없습니다 (다른 프로그램이 사용 중일 수 있음)": "Cannot register this hotkey (another program may be using it)",
|
||||
"이 동영상만 취소": "Cancel only this video",
|
||||
"이 두 폴더는 지금 rsync 하는 중입니다. 끝난 뒤에 하세요.": "These two folders are being synced with rsync right now. Try again when it finishes.",
|
||||
"이 버전 건너뛰기": "Skip this version",
|
||||
"이 버전에는 Android 용 APK 가 없습니다.": "This version has no APK for Android.",
|
||||
|
|
@ -2010,6 +2013,7 @@
|
|||
"필요한 것 모두 받기": "Download everything needed",
|
||||
"필요한 공간 {0}": "Space needed {0}",
|
||||
"필요한 공간 {0} · 남은 공간 {1}": "Space needed {0} · free {1}",
|
||||
"하나씩 멈추려면 목록 줄의 ■ 를 누르세요.": "To stop one at a time, press ■ on its row in the list.",
|
||||
"하위 폴더가 없습니다": "No subfolders",
|
||||
"한 번 더": "Once more",
|
||||
"한 번: 바로 실행": "Single: open right away",
|
||||
|
|
|
|||
|
|
@ -323,6 +323,7 @@
|
|||
"{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2}(メイン画面の上部で変更)",
|
||||
"{0} · {1}배 · {2}": "{0}・{1} 倍・{2}",
|
||||
"{0} → {1} 번역 {2}%": "{0} → {1} 翻訳 {2}%",
|
||||
"{0} 는 이미 끝났습니다": "{0} はすでに終わりました",
|
||||
"{0} 라이선스": "{0} ライセンス",
|
||||
"{0} 로 번역 중 {1}%": "{0} に翻訳中 {1}%",
|
||||
"{0} 로 표현할 수 없는 글자 {1}개가 \"?\" 로 바뀝니다.\n계속 저장할까요? (UTF-8 을 권장합니다)": "{0} で表現できない文字 {1}個が「?」に変わります。\n保存を続けますか?(UTF-8 を推奨します)",
|
||||
|
|
@ -857,6 +858,7 @@
|
|||
"모두 종료": "すべて終了",
|
||||
"모두 지우기": "すべて削除",
|
||||
"모두 초기화": "すべてリセット",
|
||||
"모두 취소 ({0}개)": "すべてキャンセル ({0} 件)",
|
||||
"모두 취소 (대기 {0}개 포함)": "すべてキャンセル (待機 {0} 件を含む)",
|
||||
"모두 풀기": "すべて展開",
|
||||
"모든 작업 취소": "すべての作業をキャンセル",
|
||||
|
|
@ -1454,6 +1456,7 @@
|
|||
"이 기기의 안전 저장소를 쓸 수 없어 비밀번호를 저장하지 못했습니다": "この端末の安全な保存領域を使えないため、パスワードを保存できませんでした",
|
||||
"이 기기의 폴더": "この端末のフォルダー",
|
||||
"이 단축키를 등록할 수 없습니다 (다른 프로그램이 사용 중일 수 있음)": "このショートカットを登録できません(他のプログラムが使用中の可能性があります)",
|
||||
"이 동영상만 취소": "この動画だけキャンセル",
|
||||
"이 두 폴더는 지금 rsync 하는 중입니다. 끝난 뒤에 하세요.": "この 2 つのフォルダーは今 rsync 中です。終わってから行ってください。",
|
||||
"이 버전 건너뛰기": "このバージョンをスキップ",
|
||||
"이 버전에는 Android 용 APK 가 없습니다.": "このバージョンには Android 用 APK がありません。",
|
||||
|
|
@ -2010,6 +2013,7 @@
|
|||
"필요한 것 모두 받기": "必要なものをすべてダウンロード",
|
||||
"필요한 공간 {0}": "必要な容量 {0}",
|
||||
"필요한 공간 {0} · 남은 공간 {1}": "必要な容量 {0}・空き容量 {1}",
|
||||
"하나씩 멈추려면 목록 줄의 ■ 를 누르세요.": "1 つずつ止めるには、一覧の行の ■ を押してください。",
|
||||
"하위 폴더가 없습니다": "サブフォルダーがありません",
|
||||
"한 번 더": "もう一度",
|
||||
"한 번: 바로 실행": "1 回: すぐに開く",
|
||||
|
|
|
|||
|
|
@ -323,6 +323,7 @@
|
|||
"{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2}(在主界面顶部更改)",
|
||||
"{0} · {1}배 · {2}": "{0} · {1} 倍 · {2}",
|
||||
"{0} → {1} 번역 {2}%": "{0} → {1} 翻译 {2}%",
|
||||
"{0} 는 이미 끝났습니다": "{0} 已经完成",
|
||||
"{0} 라이선스": "{0} 许可证",
|
||||
"{0} 로 번역 중 {1}%": "正在翻译为 {0} {1}%",
|
||||
"{0} 로 표현할 수 없는 글자 {1}개가 \"?\" 로 바뀝니다.\n계속 저장할까요? (UTF-8 을 권장합니다)": "{1} 个无法用 {0} 表示的字符将变为“?”。\n仍要保存吗?(推荐使用 UTF-8)",
|
||||
|
|
@ -857,6 +858,7 @@
|
|||
"모두 종료": "全部退出",
|
||||
"모두 지우기": "全部清除",
|
||||
"모두 초기화": "全部重置",
|
||||
"모두 취소 ({0}개)": "全部取消({0} 个)",
|
||||
"모두 취소 (대기 {0}개 포함)": "全部取消 (包括等待中的 {0} 个)",
|
||||
"모두 풀기": "全部解压",
|
||||
"모든 작업 취소": "取消所有任务",
|
||||
|
|
@ -1454,6 +1456,7 @@
|
|||
"이 기기의 안전 저장소를 쓸 수 없어 비밀번호를 저장하지 못했습니다": "本设备的安全存储不可用,无法保存密码",
|
||||
"이 기기의 폴더": "本设备上的文件夹",
|
||||
"이 단축키를 등록할 수 없습니다 (다른 프로그램이 사용 중일 수 있음)": "无法注册此快捷键(可能已被其他程序使用)",
|
||||
"이 동영상만 취소": "仅取消此视频",
|
||||
"이 두 폴더는 지금 rsync 하는 중입니다. 끝난 뒤에 하세요.": "这两个文件夹正在进行 rsync。请等完成后再操作。",
|
||||
"이 버전 건너뛰기": "跳过此版本",
|
||||
"이 버전에는 Android 용 APK 가 없습니다.": "此版本没有 Android 版 APK。",
|
||||
|
|
@ -2010,6 +2013,7 @@
|
|||
"필요한 것 모두 받기": "下载全部所需文件",
|
||||
"필요한 공간 {0}": "所需空间 {0}",
|
||||
"필요한 공간 {0} · 남은 공간 {1}": "所需空间 {0} · 剩余空间 {1}",
|
||||
"하나씩 멈추려면 목록 줄의 ■ 를 누르세요.": "要逐个停止,请按列表行上的 ■。",
|
||||
"하위 폴더가 없습니다": "没有子文件夹",
|
||||
"한 번 더": "再输入一次",
|
||||
"한 번: 바로 실행": "单击: 立即打开",
|
||||
|
|
|
|||
|
|
@ -1356,18 +1356,28 @@ class AppController extends ChangeNotifier {
|
|||
/// 186: 지금 묶음에서 아직 시작하지 않은 동영상 수
|
||||
int get batchWaiting => buildingMkv ? (_batchQueue.length - _batchNext).clamp(0, _batchQueue.length) : 0;
|
||||
|
||||
/// 186: 지금 만드는 동영상 이름 (취소 창에 보일 것)
|
||||
List<String> get runningVideoNames => [for (final v in videos) if (v.status == JobStatus.running) v.fileName];
|
||||
/// 186: 지금 만드는 동영상 (취소 창에 보일 것)
|
||||
List<VideoItem> get runningVideos => [for (final v in videos) if (v.status == JobStatus.running) v];
|
||||
|
||||
/// 186 · 187: 취소 요청 차례 - 만들던 중에 바뀌었으면 그 동영상은 "취소됨"
|
||||
int _cancelGen = 0;
|
||||
|
||||
/// 186: 지금 만드는 동영상만 취소하고 묶음의 다음 것부터 이어서 (대기열의 다른 작업도 그대로)
|
||||
void cancelRunningVideos() {
|
||||
if (!buildingMkv) return cancelCurrent();
|
||||
_log(trf('지금 것만 취소: {0}', [runningVideoNames.join(', ')]));
|
||||
_cancelGen++;
|
||||
_tool.cancel();
|
||||
/// 239 · 240: 사용자가 취소하라고 고른 동영상 (만들던 것이 끝나면 "취소됨")
|
||||
final _cancelledVideos = <VideoItem>{};
|
||||
|
||||
/// 186 · 239: [targets] 동영상만 취소하고 묶음의 다음 것부터 이어서 (대기열의 다른 작업 · 동시에 도는 다른 동영상도 그대로).
|
||||
/// 취소 창을 연 때의 대상을 넘긴다 - 그사이 끝나고 다음 것이 시작했으면 그 다음 것을 취소하지 않는다.
|
||||
/// 돌려주는 것: 이미 끝나 (돌고 있지 않아) 취소하지 못한 것
|
||||
List<VideoItem> cancelVideos(List<VideoItem> targets) {
|
||||
final live = [for (final v in targets) if (v.status == JobStatus.running) v];
|
||||
if (live.isNotEmpty) {
|
||||
_log(trf('지금 것만 취소: {0}', [live.map((v) => v.fileName).join(', ')]));
|
||||
for (final v in live) {
|
||||
_cancelledVideos.add(v);
|
||||
_tool.cancelTagged(v);
|
||||
}
|
||||
}
|
||||
return [for (final v in targets) if (!live.contains(v)) v];
|
||||
}
|
||||
|
||||
Future<void> _build(VideoItem v) async {
|
||||
|
|
@ -1393,9 +1403,12 @@ class AppController extends ChangeNotifier {
|
|||
final c = await _utf8Copy(s);
|
||||
if (c != null) copies[s] = c;
|
||||
}
|
||||
// 239: FFmpeg 가 시작하기 전에 취소됐으면 (자막 준비 중) 시작하지 않는다
|
||||
if (_cancelledVideos.contains(v)) throw MediaToolException(tr('사용자가 취소했습니다.'));
|
||||
started = true;
|
||||
await _tool.runFfmpeg(
|
||||
buildMuxArgs(v, out, encode: encode, encoders: encoders, utf8Copies: copies),
|
||||
tag: v,
|
||||
duration: v.info?.duration,
|
||||
onProgress: (x) {
|
||||
v.progress = x;
|
||||
|
|
@ -1413,7 +1426,7 @@ class AppController extends ChangeNotifier {
|
|||
? '${e.message}'
|
||||
: '$e';
|
||||
// 187: 사용자가 취소한 것은 실패가 아니라 "취소됨"
|
||||
final cancelled = gen != _cancelGen || _buildCancelled;
|
||||
final cancelled = gen != _cancelGen || _buildCancelled || _cancelledVideos.contains(v);
|
||||
v
|
||||
..status = cancelled ? JobStatus.cancelled : JobStatus.failed
|
||||
..message = cancelled ? tr('취소됨') : msg;
|
||||
|
|
@ -1425,6 +1438,7 @@ class AppController extends ChangeNotifier {
|
|||
} catch (_) {}
|
||||
}
|
||||
} finally {
|
||||
_cancelledVideos.remove(v);
|
||||
for (final c in copies.values) {
|
||||
try {
|
||||
await services.storage.delete(c);
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ class FfmpegKitMediaTool implements MediaTool {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<void> runFfmpeg(List<String> args, {Duration? duration, ProgressCallback? onProgress}) async {
|
||||
Future<void> runFfmpeg(List<String> args, {Duration? duration, ProgressCallback? onProgress, Object? tag}) async {
|
||||
// 표준 출력으로 진행률을 쓰는 인수는 빼고 통계 콜백을 쓴다
|
||||
final a = <String>[];
|
||||
for (var i = 0; i < args.length; i++) {
|
||||
|
|
@ -78,6 +78,7 @@ class FfmpegKitMediaTool implements MediaTool {
|
|||
(s) async {
|
||||
final id = s.getSessionId();
|
||||
_running.remove(id);
|
||||
_tags.remove(id);
|
||||
final rc = await s.getReturnCode();
|
||||
if (_cancelled.remove(id) || ReturnCode.isCancel(rc)) {
|
||||
done.completeError(MediaToolException(tr('사용자가 취소했습니다.')));
|
||||
|
|
@ -104,7 +105,10 @@ class FfmpegKitMediaTool implements MediaTool {
|
|||
},
|
||||
);
|
||||
final id = session.getSessionId();
|
||||
if (id != null && !done.isCompleted) _running.add(id);
|
||||
if (id != null && !done.isCompleted) {
|
||||
_running.add(id);
|
||||
if (tag != null) _tags[id] = tag;
|
||||
}
|
||||
await done.future;
|
||||
onProgress?.call(1.0);
|
||||
}
|
||||
|
|
@ -116,4 +120,15 @@ class FfmpegKitMediaTool implements MediaTool {
|
|||
unawaited(FFmpegKit.cancel(id));
|
||||
}
|
||||
}
|
||||
|
||||
final _tags = <int, Object>{};
|
||||
|
||||
@override
|
||||
void cancelTagged(Object tag) {
|
||||
for (final id in _running.toList()) {
|
||||
if (_tags[id] != tag) continue;
|
||||
_cancelled.add(id);
|
||||
unawaited(FFmpegKit.cancel(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ class ProcessMediaTool implements MediaTool {
|
|||
List<String> args, {
|
||||
Duration? duration,
|
||||
ProgressCallback? onProgress,
|
||||
Object? tag,
|
||||
}) async {
|
||||
final Process proc;
|
||||
try {
|
||||
|
|
@ -116,6 +117,7 @@ class ProcessMediaTool implements MediaTool {
|
|||
throw MediaToolException(trf('ffmpeg 를 실행할 수 없습니다: {0}', [e.message]));
|
||||
}
|
||||
_running.add(proc);
|
||||
if (tag != null) _tags[proc] = tag;
|
||||
|
||||
final totalUs = duration?.inMicroseconds ?? 0;
|
||||
final stdoutDone = proc.stdout
|
||||
|
|
@ -142,6 +144,7 @@ class ProcessMediaTool implements MediaTool {
|
|||
final code = await proc.exitCode;
|
||||
await Future.wait([stdoutDone, stderrDone]);
|
||||
_running.remove(proc);
|
||||
_tags.remove(proc);
|
||||
|
||||
if (_killed.remove(proc)) throw MediaToolException(tr('사용자가 취소했습니다.'));
|
||||
if (code != 0) {
|
||||
|
|
@ -159,6 +162,17 @@ class ProcessMediaTool implements MediaTool {
|
|||
proc.kill();
|
||||
}
|
||||
}
|
||||
|
||||
final _tags = <Process, Object>{};
|
||||
|
||||
@override
|
||||
void cancelTagged(Object tag) {
|
||||
for (final proc in _running.toList()) {
|
||||
if (_tags[proc] != tag) continue;
|
||||
_killed.add(proc);
|
||||
proc.kill();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `ffmpeg -encoders` 출력 → 영상 인코더 이름
|
||||
|
|
|
|||
|
|
@ -24,12 +24,17 @@ abstract class MediaTool {
|
|||
Future<Set<String>> encoders();
|
||||
|
||||
/// FFmpeg 실행. [duration] 이 있으면 진행률을 계산해 [onProgress] 로 알린다.
|
||||
/// [tag] (239): 이 실행만 골라 취소할 때 ([cancelTagged]) 쓰는 표 (예: 만드는 동영상)
|
||||
Future<void> runFfmpeg(
|
||||
List<String> args, {
|
||||
Duration? duration,
|
||||
ProgressCallback? onProgress,
|
||||
Object? tag,
|
||||
});
|
||||
|
||||
/// 실행 중인 작업 취소
|
||||
void cancel();
|
||||
|
||||
/// 239: [tag] 를 붙여 실행한 것만 취소 (동시에 도는 다른 것은 그대로)
|
||||
void cancelTagged(Object tag);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -140,12 +140,40 @@ class _TopBar extends StatelessWidget {
|
|||
Future<void> _cancelJobs(BuildContext context) async {
|
||||
final inBatch = c.batchWaiting;
|
||||
final waiting = c.pendingJobs.length + inBatch;
|
||||
// 239: 창을 연 때 돌고 있던 것 - [지금 것만 취소] 는 이것만 (그사이 끝나고 다음 것이 시작해도 다음 것은 취소하지 않는다)
|
||||
final targets = c.buildingMkv ? c.runningVideos : const <VideoItem>[];
|
||||
if (waiting == 0) {
|
||||
// 240: 여럿이 동시에 돌고 있으면 묻는다 (하나씩은 목록 줄의 ■)
|
||||
if (targets.length > 1) {
|
||||
final all = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
scrollable: true,
|
||||
actionsOverflowDirection: VerticalDirection.down,
|
||||
title: Text(tr('작업 취소')),
|
||||
content: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(trf('지금: {0}', [targets.map((v) => v.fileName).join(', ')])),
|
||||
const SizedBox(height: 4),
|
||||
Text(tr('하나씩 멈추려면 목록 줄의 ■ 를 누르세요.'), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
]),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx), child: Text(tr('그대로 두기'))),
|
||||
FilledButton(
|
||||
style: FilledButton.styleFrom(backgroundColor: JjColors.danger),
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: Text(trf('모두 취소 ({0}개)', [targets.length])),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (all == true) c.cancel();
|
||||
return;
|
||||
}
|
||||
c.cancel();
|
||||
return;
|
||||
}
|
||||
if (inBatch > 0) {
|
||||
final now = c.runningVideoNames.join(', ');
|
||||
final now = targets.map((v) => v.fileName).join(', ');
|
||||
final pick = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
|
|
@ -169,7 +197,15 @@ class _TopBar extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
);
|
||||
if (pick == 'current') c.cancelRunningVideos();
|
||||
if (pick == 'current') {
|
||||
final gone = c.cancelVideos(targets);
|
||||
// 239: 창을 보는 사이 이미 끝났으면 알리고 지금 상태로 다시 묻는다
|
||||
if (gone.isNotEmpty && context.mounted) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text(trf('{0} 는 이미 끝났습니다', [gone.map((v) => v.fileName).join(', ')]))));
|
||||
if (c.busy) await _cancelJobs(context);
|
||||
}
|
||||
}
|
||||
if (pick == 'all') c.cancel();
|
||||
return;
|
||||
}
|
||||
|
|
@ -851,6 +887,14 @@ class _VideoTile extends StatelessWidget {
|
|||
onPressed: () => c.resetStatus(v),
|
||||
icon: const Icon(Icons.refresh, color: JjColors.textDim),
|
||||
),
|
||||
// 240: 만드는 중인 동영상 하나만 취소 (다른 것 · 남은 것은 그대로)
|
||||
if (v.status == JobStatus.running && c.buildingMkv)
|
||||
IconButton(
|
||||
tooltip: tr('이 동영상만 취소'),
|
||||
iconSize: 16,
|
||||
onPressed: () => c.cancelVideos([v]),
|
||||
icon: const Icon(Icons.stop, color: JjColors.danger),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: tr('목록에서 제거'),
|
||||
iconSize: 16,
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ void main() {
|
|||
}
|
||||
expect(v1.status, JobStatus.running);
|
||||
expect(c.batchWaiting, 2, reason: '한 묶음 안의 기다리는 동영상도 대기로 센다');
|
||||
c.cancelRunningVideos();
|
||||
expect(c.cancelVideos(c.runningVideos), isEmpty);
|
||||
await building;
|
||||
expect([v1.status, v2.status, v3.status], [JobStatus.cancelled, JobStatus.done, JobStatus.done],
|
||||
reason: '${v1.message} ${v2.message} ${v3.message}');
|
||||
|
|
@ -222,6 +222,44 @@ void main() {
|
|||
expect(c.logs.join('\n'), contains('성공 2 · 취소 1 · 실패 0 / 전체 3'));
|
||||
});
|
||||
|
||||
test('239: 취소 창을 연 뒤 그 동영상이 끝나고 다음 것이 시작했으면 [지금 것만 취소] 는 다음 것을 취소하지 않는다', () async {
|
||||
final tool = ProcessMediaTool('ffmpeg', 'ffprobe');
|
||||
if (await tool.version() == null) return markTestSkipped('FFmpeg 없음');
|
||||
final dir = Directory.systemTemp.createTempSync('jj_batch239_');
|
||||
addTearDown(() => dir.deleteSync(recursive: true));
|
||||
// v1 은 짧게 (창을 보는 사이 끝남), v2 는 길게
|
||||
final files = [for (final n in ['v1', 'v2', 'v3']) p.join(dir.path, '$n.mp4')];
|
||||
for (final (i, f) in files.indexed) {
|
||||
await tool.runFfmpeg(['-y', '-f', 'lavfi', '-i', 'testsrc=size=1280x720:rate=30', '-t', i == 1 ? '60' : '1',
|
||||
'-c:v', 'libx264', '-preset', 'ultrafast', '-pix_fmt', 'yuv420p', f]);
|
||||
}
|
||||
final c = AppController(PlatformServices(mediaTool: tool, storage: DesktopStorageService()));
|
||||
await c.init();
|
||||
c.settings.maxParallelJobs = 1;
|
||||
c.encode = const EncodeSettings(codec: VideoCodecChoice.h264, quality: QualityChoice.high);
|
||||
await c.addVideos(files);
|
||||
final [v1, v2, v3] = c.videos;
|
||||
final building = c.buildVideos([v1, v2, v3]);
|
||||
for (var i = 0; i < 400 && v1.status != JobStatus.running; i++) {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||||
}
|
||||
final shown = c.runningVideos; // 창을 연 때: v1
|
||||
expect(shown, [v1]);
|
||||
// 창을 보는 사이 v1 이 끝나고 v2 가 시작
|
||||
for (var i = 0; i < 800 && !(v2.status == JobStatus.running && v2.progress > 0.01); i++) {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 25));
|
||||
}
|
||||
expect([v1.status, v2.status], [JobStatus.done, JobStatus.running]);
|
||||
expect(c.cancelVideos(shown), [v1], reason: '이미 끝남 - 알리고 다시 묻는다');
|
||||
await Future<void>.delayed(const Duration(milliseconds: 500));
|
||||
expect(v2.status, JobStatus.running, reason: '창이 보여 준 것이 아닌 v2 는 취소하지 않음');
|
||||
// 다시 묻는 창에서 v2 를 취소 → v3 은 이어서
|
||||
expect(c.cancelVideos(c.runningVideos), isEmpty);
|
||||
await building;
|
||||
expect([v1.status, v2.status, v3.status], [JobStatus.done, JobStatus.cancelled, JobStatus.done],
|
||||
reason: '${v1.message} ${v2.message} ${v3.message}');
|
||||
});
|
||||
|
||||
testWidgets('MKV 화면: 체크 → "자막 만들기 (2)" · "MKV 만들기 (2)" 글, 전체 선택', (tester) async {
|
||||
tester.view.physicalSize = const Size(1600, 900); // 위쪽 버튼 글이 다 보이는 너비
|
||||
tester.view.devicePixelRatio = 1;
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ class _Tool implements MediaTool {
|
|||
@override
|
||||
Future<MediaInfo> probe(String path) async => const MediaInfo(duration: Duration(seconds: 1));
|
||||
@override
|
||||
Future<void> runFfmpeg(List<String> args, {Duration? duration, ProgressCallback? onProgress}) async {
|
||||
Future<void> runFfmpeg(List<String> args, {Duration? duration, ProgressCallback? onProgress, Object? tag}) async {
|
||||
now++;
|
||||
if (now > peak) peak = now;
|
||||
await Future<void>.delayed(const Duration(milliseconds: 30));
|
||||
|
|
@ -73,6 +73,8 @@ class _Tool implements MediaTool {
|
|||
|
||||
@override
|
||||
void cancel() {}
|
||||
@override
|
||||
void cancelTagged(Object tag) {}
|
||||
}
|
||||
|
||||
void main() {
|
||||
|
|
|
|||
|
|
@ -33,9 +33,11 @@ class _Tool implements MediaTool {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<void> runFfmpeg(List<String> args, {Duration? duration, ProgressCallback? onProgress}) async {}
|
||||
Future<void> runFfmpeg(List<String> args, {Duration? duration, ProgressCallback? onProgress, Object? tag}) async {}
|
||||
@override
|
||||
void cancel() {}
|
||||
@override
|
||||
void cancelTagged(Object tag) {}
|
||||
}
|
||||
|
||||
void main() {
|
||||
|
|
|
|||
|
|
@ -31,9 +31,11 @@ class _FakeTool implements MediaTool {
|
|||
);
|
||||
@override
|
||||
Future<void> runFfmpeg(List<String> args,
|
||||
{Duration? duration, ProgressCallback? onProgress}) async {}
|
||||
{Duration? duration, ProgressCallback? onProgress, Object? tag}) async {}
|
||||
@override
|
||||
void cancel() {}
|
||||
@override
|
||||
void cancelTagged(Object tag) {}
|
||||
}
|
||||
|
||||
class _FakeStorage implements StorageService {
|
||||
|
|
|
|||
Loading…
Reference in New Issue