From 480209b45da8cb4593ebf4997205e5a6e5001b2e Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Sun, 4 Oct 2026 14:30:01 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9D=B4=EB=8F=99:=20=EB=A7=8C=EB=93=A0=20?= =?UTF-8?q?=EA=B2=B0=EA=B3=BC=EB=AC=BC=20(jj=5Fmkv=20=EC=9D=98=20MKV=20?= =?UTF-8?q?=C2=B7=20=EC=9E=90=EB=A7=89)=20=EB=8F=84=20=EC=9D=B4=EB=8F=99?= =?UTF-8?q?=20=ED=8F=B4=EB=8D=94=EC=9D=98=20jj=5Fmkv=20=EB=A1=9C=20?= =?UTF-8?q?=ED=95=A8=EA=BB=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- assets/l10n/en.json | 2 ++ assets/l10n/ja.json | 2 ++ assets/l10n/zh-Hans.json | 2 ++ lib/app/app_controller.dart | 33 +++++++++++++++++++++++++++++++-- test/move_test.dart | 13 +++++++++++++ 5 files changed, 50 insertions(+), 2 deletions(-) diff --git a/assets/l10n/en.json b/assets/l10n/en.json index d74d39e..3130ed9 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -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", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index a89350a..4b8f1ee 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -3,6 +3,7 @@ "\n한국어 번역은 대기열에서 이어서 진행합니다.": "\n韓国語への翻訳はキューで続けて行います。", " ☆ 를 누르면 여기에 즐겨찾기가 추가됩니다 · 오른쪽 클릭: 폴더 추가": " ☆ を押すとここにブックマークが追加されます · 右クリック: フォルダー追加", " ({0}개는 이미 있음)": "({0}個は既にあります)", + " (결과물 {0}개 함께)": "(成果物 {0}個も一緒に)", " (대기열)": "(キュー)", " (동영상 {0}개)": "(動画 {0}個)", " (사용 불가)": "(使用不可)", @@ -188,6 +189,7 @@ "결과 폴더": "出力フォルダー", "결과 폴더 열기 (동영상을 고르세요)": "出力フォルダーを開く(動画を選んでください)", "결과 폴더 열기: {0}": "出力フォルダーを開く: {0}", + "결과물 이동 실패: {0} ({1})": "成果物の移動失敗: {0}({1})", "계속 편집": "編集を続ける", "고른 크기 이하에서 가장 좋은 화질 (영상에 없는 크기면 가까운 것)": "選んだサイズ以下で最も良い画質(動画にないサイズなら近いもの)", "고지 문서": "告知文書", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 0af3d1e..40af1f6 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -3,6 +3,7 @@ "\n한국어 번역은 대기열에서 이어서 진행합니다.": "\n韩语翻译将在队列中继续进行。", " ☆ 를 누르면 여기에 즐겨찾기가 추가됩니다 · 오른쪽 클릭: 폴더 추가": " 点击 ☆ 即可在此添加书签 · 右键: 添加文件夹", " ({0}개는 이미 있음)": "({0} 个已存在)", + " (결과물 {0}개 함께)": "(成品 {0} 个一并移动)", " (대기열)": "(队列)", " (동영상 {0}개)": "({0} 个视频)", " (사용 불가)": "(不可用)", @@ -188,6 +189,7 @@ "결과 폴더": "输出文件夹", "결과 폴더 열기 (동영상을 고르세요)": "打开输出文件夹(请选择视频)", "결과 폴더 열기: {0}": "打开输出文件夹: {0}", + "결과물 이동 실패: {0} ({1})": "成品移动失败: {0}({1})", "계속 편집": "继续编辑", "고른 크기 이하에서 가장 좋은 화질 (영상에 없는 크기면 가까운 것)": "不超过所选尺寸的最佳画质(视频没有该尺寸时取最接近的)", "고지 문서": "声明文件", diff --git a/lib/app/app_controller.dart b/lib/app/app_controller.dart index 5cd398a..3e3247b 100644 --- a/lib/app/app_controller.dart +++ b/lib/app/app_controller.dart @@ -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)> moveVideos(List 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 _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()) { + 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 moveFileInto(String from, String dir) async { final stem = p.basenameWithoutExtension(from), ext = p.extension(from); diff --git a/test/move_test.dart b/test/move_test.dart index 62b1cb4..4d8da6d 100644 --- a/test/move_test.dart +++ b/test/move_test.dart @@ -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); });