이동: 만든 결과물 (jj_mkv 의 MKV · 자막) 도 이동 폴더의 jj_mkv 로 함께

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main v2026.10.04_002
RyuJongJin 2026-10-04 14:30:01 +09:00
parent b466bff15f
commit 480209b45d
5 changed files with 50 additions and 2 deletions

View File

@ -3,6 +3,7 @@
"\n한국어 번역은 대기열에서 이어서 진행합니다.": "\nKorean translation will continue in the queue.",
" ☆ 를 누르면 여기에 즐겨찾기가 추가됩니다 · 오른쪽 클릭: 폴더 추가": " Press ☆ to add bookmarks here · Right-click: add folder",
" ({0}개는 이미 있음)": " ({0} already exist)",
" (결과물 {0}개 함께)": " ({0} results moved too)",
" (대기열)": " (queued)",
" (동영상 {0}개)": " ({0} videos)",
" (사용 불가)": " (unavailable)",
@ -188,6 +189,7 @@
"결과 폴더": "Output folder",
"결과 폴더 열기 (동영상을 고르세요)": "Open output folder (choose a video)",
"결과 폴더 열기: {0}": "Open output folder: {0}",
"결과물 이동 실패: {0} ({1})": "Failed to move result: {0} ({1})",
"계속 편집": "Keep editing",
"고른 크기 이하에서 가장 좋은 화질 (영상에 없는 크기면 가까운 것)": "Best quality at or below the chosen size (nearest if the video lacks that size)",
"고지 문서": "Notices",

View File

@ -3,6 +3,7 @@
"\n한국어 번역은 대기열에서 이어서 진행합니다.": "\n韓国語への翻訳はキューで続けて行います。",
" ☆ 를 누르면 여기에 즐겨찾기가 추가됩니다 · 오른쪽 클릭: 폴더 추가": " ☆ を押すとここにブックマークが追加されます · 右クリック: フォルダー追加",
" ({0}개는 이미 있음)": "({0}個は既にあります)",
" (결과물 {0}개 함께)": "(成果物 {0}個も一緒に)",
" (대기열)": "(キュー)",
" (동영상 {0}개)": "(動画 {0}個)",
" (사용 불가)": "(使用不可)",
@ -188,6 +189,7 @@
"결과 폴더": "出力フォルダー",
"결과 폴더 열기 (동영상을 고르세요)": "出力フォルダーを開く(動画を選んでください)",
"결과 폴더 열기: {0}": "出力フォルダーを開く: {0}",
"결과물 이동 실패: {0} ({1})": "成果物の移動失敗: {0}({1})",
"계속 편집": "編集を続ける",
"고른 크기 이하에서 가장 좋은 화질 (영상에 없는 크기면 가까운 것)": "選んだサイズ以下で最も良い画質(動画にないサイズなら近いもの)",
"고지 문서": "告知文書",

View File

@ -3,6 +3,7 @@
"\n한국어 번역은 대기열에서 이어서 진행합니다.": "\n韩语翻译将在队列中继续进行。",
" ☆ 를 누르면 여기에 즐겨찾기가 추가됩니다 · 오른쪽 클릭: 폴더 추가": " 点击 ☆ 即可在此添加书签 · 右键: 添加文件夹",
" ({0}개는 이미 있음)": "({0} 个已存在)",
" (결과물 {0}개 함께)": "(成品 {0} 个一并移动)",
" (대기열)": "(队列)",
" (동영상 {0}개)": "({0} 个视频)",
" (사용 불가)": "(不可用)",
@ -188,6 +189,7 @@
"결과 폴더": "输出文件夹",
"결과 폴더 열기 (동영상을 고르세요)": "打开输出文件夹(请选择视频)",
"결과 폴더 열기: {0}": "打开输出文件夹: {0}",
"결과물 이동 실패: {0} ({1})": "成品移动失败: {0}({1})",
"계속 편집": "继续编辑",
"고른 크기 이하에서 가장 좋은 화질 (영상에 없는 크기면 가까운 것)": "不超过所选尺寸的最佳画质(视频没有该尺寸时取最接近的)",
"고지 문서": "声明文件",

View File

@ -402,7 +402,8 @@ class AppController extends ChangeNotifier {
return n;
}
/// 동영상을 이동 폴더 (환경 설정) 로 옮긴다. 옆에 있는 같은 이름의 자막 파일 (a.srt · a.ko.smi …) 도 함께.
/// 동영상을 이동 폴더 (환경 설정) 로 옮긴다. 옆에 있는 같은 이름의 자막 파일 (a.srt · a.ko.smi …) 과
/// 만든 결과물 (jj_mkv 의 a.mkv · a_AI.srt · a_ko.srt …) 도 함께 (결과물은 이동 폴더의 jj_mkv 로).
/// 옮긴 동영상은 목록에서 뺀다. 작업 중인 것은 옮기지 않는다. 반환: (옮긴 수, 못 옮긴 이유들)
Future<(int, List<String>)> moveVideos(List<VideoItem> targets) async {
final dest = settings.moveTargetDir;
@ -440,7 +441,15 @@ class AppController extends ChangeNotifier {
_log(trf('자막 이동 실패: {0} ({1})', [p.basename(s), e]));
}
}
_log(trf('이동: {0} → {1}{2}', [v.fileName, to, side.isEmpty ? '' : trf(' (자막 {0}개 함께)', [side.length])]));
final results = await _moveResults(v.path, to);
_log(trf('이동: {0} → {1}{2}', [
v.fileName,
to,
[
if (side.isNotEmpty) trf(' (자막 {0}개 함께)', [side.length]),
if (results > 0) trf(' (결과물 {0}개 함께)', [results]),
].join(),
]));
videos.remove(v);
checked.remove(v);
moved++;
@ -454,6 +463,26 @@ class AppController extends ChangeNotifier {
return (moved, errors);
}
/// 만든 결과물 (jj_mkv 의 이름.mkv · 이름_AI.srt · 이름_ko.srt …) 을 옮긴 동영상의 jj_mkv 로. 옮긴 개수.
Future<int> _moveResults(String oldVideo, String newVideo) async {
final from = outputDirFor(oldVideo), to = outputDirFor(newVideo);
if (p.equals(from, to) || !Directory(from).existsSync()) return 0;
final stem = p.basenameWithoutExtension(oldVideo);
final mine = RegExp('^${RegExp.escape(stem)}(\\.mkv|_[A-Za-z-]+\\.(srt|ass|ssa|smi|vtt|sub))\$', caseSensitive: false);
var n = 0;
for (final f in Directory(from).listSync().whereType<File>()) {
if (!mine.hasMatch(p.basename(f.path))) continue;
try {
await Directory(to).create(recursive: true);
await moveFileInto(f.path, to);
n++;
} catch (e) {
_log(trf('결과물 이동 실패: {0} ({1})', [p.basename(f.path), e]));
}
}
return n;
}
/// 파일을 폴더 안으로 옮긴다 (같은 이름이 있으면 "이름 (2).mp4"). 다른 드라이브 · SD 카드면 복사 후 지운다. 옮긴 경로를 돌려준다.
static Future<String> moveFileInto(String from, String dir) async {
final stem = p.basenameWithoutExtension(from), ext = p.extension(from);

View File

@ -34,6 +34,12 @@ void main() {
final b = VideoItem(make('b.mkv').path);
final busy = VideoItem(make('c.mp4').path)..status = JobStatus.running;
make(p.join('done', 'b.mkv')); // 이미 있는 이름
// 만든 결과물 (jj_mkv): a 의 것만 함께 옮긴다
make(p.join('jj_mkv', 'a.mkv'));
make(p.join('jj_mkv', 'a_AI.srt'));
make(p.join('jj_mkv', 'a_zh-Hans.srt'));
make(p.join('jj_mkv', 'ab.mkv'));
make(p.join('jj_mkv', 'a_1_ko.srt'));
c.videos.addAll([a, b, busy]);
c.selected = a;
@ -45,6 +51,13 @@ void main() {
expect(File(p.join(dir.path, 'other.srt')).existsSync(), isTrue, reason: '이름이 다른 자막은 그대로');
expect(File(p.join(dest, 'b (2).mkv')).readAsStringSync(), 'b.mkv');
expect(File(p.join(dir.path, 'a.mp4')).existsSync(), isFalse);
for (final f in ['a.mkv', 'a_AI.srt', 'a_zh-Hans.srt']) {
expect(File(p.join(dest, 'jj_mkv', f)).existsSync(), isTrue, reason: f);
expect(File(p.join(dir.path, 'jj_mkv', f)).existsSync(), isFalse, reason: f);
}
for (final f in ['ab.mkv', 'a_1_ko.srt']) {
expect(File(p.join(dir.path, 'jj_mkv', f)).existsSync(), isTrue, reason: '다른 동영상의 결과물 $f 는 그대로');
}
expect(c.videos, [busy]);
expect(c.selected, busy);
});