app_controller 509 → 498줄: 자막 파일 고르기 · 다른 이름 저장 · 편집할 수 있는지를 SubtitleFiles 로
- SubtitleFiles.pickFor (있는 경로 건너뜀 · 이름으로 언어, 더한 수) · saveAs (SRT 바이트 · 표현 못 한 글자 수) · canEdit (static) - app_controller 는 부르기만 (알림 · 기록은 그대로) - 약속 시험: 실제 파일 · 실제 SRT 바이트, 고르기 · 저장 창 대신 정한 것을 돌려주는 저장소 (사용자가 고른 것) 확인: analyze 0 · 단위 1475 ×2 · 통합 6/6 ×2 · base10 214장 0px Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>modular
parent
093cf3252e
commit
051c259d89
|
|
@ -7,13 +7,10 @@ import 'package:path/path.dart' as p;
|
|||
|
||||
import '../core/encode_options.dart';
|
||||
import '../core/languages.dart';
|
||||
import '../core/mkv_command_builder.dart' show bitmapSubtitleCodecs;
|
||||
import '../core/models.dart';
|
||||
import '../core/srt.dart';
|
||||
import '../core/subtitle_detector.dart';
|
||||
import '../core/subtitle_search.dart';
|
||||
import '../services/subtitle_provider.dart';
|
||||
import '../core/text_codec.dart';
|
||||
import '../services/media_tool.dart';
|
||||
import '../services/platform_services.dart';
|
||||
import '../services/system_usage.dart';
|
||||
|
|
@ -261,12 +258,7 @@ class AppController extends ChangeNotifier {
|
|||
// ───────── 자막 ─────────
|
||||
|
||||
Future<void> pickSubtitlesFor(VideoItem v) async {
|
||||
final paths = await services.storage.pickSubtitles(initialDirectory: v.directory);
|
||||
for (final path in paths) {
|
||||
if (v.subtitles.any((s) => s.path != null && p.equals(s.path!, path))) continue;
|
||||
final d = findSiblingSubtitles(v.path, [path]);
|
||||
v.subtitles.add(await _files.external(path, d.isEmpty ? undetermined : d.first.language));
|
||||
}
|
||||
await _files.pickFor(v);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
|
|
@ -293,9 +285,7 @@ class AppController extends ChangeNotifier {
|
|||
// ───────── 자막 편집 (2단계) ─────────
|
||||
|
||||
/// 편집 가능 여부 (이미지 자막은 불가)
|
||||
bool canEdit(SubtitleEntry s) =>
|
||||
!(s.kind == SubtitleKind.embedded && bitmapSubtitleCodecs.contains(s.codec)) &&
|
||||
(s.kind == SubtitleKind.external || ffmpegVersion != null);
|
||||
bool canEdit(SubtitleEntry s) => SubtitleFiles.canEdit(s, hasFfmpeg: ffmpegVersion != null);
|
||||
|
||||
/// 자막을 큐 목록으로 불러온다 ([SubtitleFiles.loadCues])
|
||||
Future<List<Cue>> loadCues(VideoItem v, SubtitleEntry s) => _files.loadCues(v, s);
|
||||
|
|
@ -311,10 +301,9 @@ class AppController extends ChangeNotifier {
|
|||
|
||||
/// 다른 이름으로 저장 (목록은 바꾸지 않음)
|
||||
Future<(String?, int)> saveCuesAs(VideoItem v, List<Cue> cues, String charset, String fileName) async {
|
||||
final enc = encodeText(formatSrt(cues), charset);
|
||||
final path = await services.storage.saveAs(fileName: fileName, bytes: enc.bytes, initialDirectory: v.directory);
|
||||
final (path, lost) = await _files.saveAs(v, cues, charset, fileName);
|
||||
if (path != null) _log(trf('다른 이름으로 저장: {0} ({1})', [path, charset]));
|
||||
return (path, enc.lostChars);
|
||||
return (path, lost);
|
||||
}
|
||||
|
||||
// ───────── AI 자막 (4단계) · 자막 번역 ([AiSubtitleRunner]) ─────────
|
||||
|
|
|
|||
|
|
@ -22,6 +22,30 @@ class SubtitleFiles {
|
|||
final MediaTool tool;
|
||||
SubtitleFiles(this.storage, this.tool);
|
||||
|
||||
/// 자막 파일을 골라 [v] 에 더한다 (이미 있는 경로는 건너뜀, 언어는 파일 이름으로). 더한 수
|
||||
Future<int> pickFor(VideoItem v) async {
|
||||
var n = 0;
|
||||
for (final path in await storage.pickSubtitles(initialDirectory: v.directory)) {
|
||||
if (v.subtitles.any((s) => s.path != null && p.equals(s.path!, path))) continue;
|
||||
final d = findSiblingSubtitles(v.path, [path]);
|
||||
v.subtitles.add(await external(path, d.isEmpty ? undetermined : d.first.language));
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/// 편집할 수 있는 자막인지 (이미지 자막은 아님 · 내장 자막은 FFmpeg 가 있어야 꺼냄)
|
||||
static bool canEdit(SubtitleEntry s, {required bool hasFfmpeg}) =>
|
||||
!(s.kind == SubtitleKind.embedded && bitmapSubtitleCodecs.contains(s.codec)) &&
|
||||
(s.kind == SubtitleKind.external || hasFfmpeg);
|
||||
|
||||
/// [cues] 를 SRT 로 다른 이름 저장 (목록은 바꾸지 않음). (저장한 경로 - 취소면 null, 표현할 수 없어 '?' 로 바뀐 글자 수)
|
||||
Future<(String?, int)> saveAs(VideoItem v, List<Cue> cues, String charset, String fileName) async {
|
||||
final enc = encodeText(formatSrt(cues), charset);
|
||||
final path = await storage.saveAs(fileName: fileName, bytes: enc.bytes, initialDirectory: v.directory);
|
||||
return (path, enc.lostChars);
|
||||
}
|
||||
|
||||
/// 외부 자막 항목 (문자셋은 앞 64KB 로 알아낸다, 못 읽으면 UTF-8)
|
||||
Future<SubtitleEntry> external(String path, Language lang) async {
|
||||
String charset = 'UTF-8';
|
||||
|
|
|
|||
|
|
@ -1,15 +1,21 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jj_mkvmaker/app/controller/app_log.dart';
|
||||
import 'package:jj_mkvmaker/app/controller/encode_choice.dart';
|
||||
import 'package:jj_mkvmaker/app/controller/job_queue.dart';
|
||||
import 'package:jj_mkvmaker/app/controller/subtitle_files.dart';
|
||||
import 'package:jj_mkvmaker/app/controller/video_files.dart';
|
||||
import 'package:jj_mkvmaker/app/controller/video_list.dart';
|
||||
import 'package:jj_mkvmaker/app/controller/video_list_share.dart';
|
||||
import 'package:jj_mkvmaker/core/encode_options.dart';
|
||||
import 'package:jj_mkvmaker/core/languages.dart';
|
||||
import 'package:jj_mkvmaker/core/models.dart';
|
||||
import 'package:jj_mkvmaker/core/srt.dart';
|
||||
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
|
||||
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
/// app_controller 를 나눈 모듈의 약속 (실제 파일 · 실제 Future 로)
|
||||
|
|
@ -133,4 +139,49 @@ void main() {
|
|||
list.clear();
|
||||
expect([list.items, list.selected], [isEmpty, null]);
|
||||
});
|
||||
|
||||
test('약속 SubtitleFiles: 고른 자막 더하기 (있는 것 건너뜀 · 이름으로 언어) · 다른 이름 저장 · 편집할 수 있는지', () async {
|
||||
final video = p.join(tmp.path, 'movie.mkv');
|
||||
File(video).writeAsStringSync('v');
|
||||
final ko = File(p.join(tmp.path, 'movie.ko.srt'))..writeAsStringSync('1\n00:00:01,000 --> 00:00:02,000\n안녕\n');
|
||||
final storage = _ChosenStorage([ko.path, ko.path]);
|
||||
final files = SubtitleFiles(storage, ProcessMediaTool('x', 'y'));
|
||||
final v = VideoItem(video);
|
||||
expect(await files.pickFor(v), 1, reason: '같은 경로는 한 번만');
|
||||
expect([v.subtitles.single.path, v.subtitles.single.language.code], [ko.path, 'ko']);
|
||||
expect(await files.pickFor(v), 0, reason: '이미 있는 경로는 건너뜀');
|
||||
// 다른 이름 저장: 실제 SRT 바이트 · 표현할 수 없는 글자 수
|
||||
storage.saveTo = p.join(tmp.path, 'out.srt');
|
||||
final (path, lost) = await files.saveAs(v, [Cue(const Duration(seconds: 1), const Duration(seconds: 2), '안녕')], 'UTF-8', 'out.srt');
|
||||
expect([path, lost], [storage.saveTo, 0]);
|
||||
expect(File(path!).readAsStringSync(), contains('00:00:01,000 --> 00:00:02,000'));
|
||||
storage.saveTo = null;
|
||||
expect((await files.saveAs(v, const [], 'UTF-8', 'x.srt')).$1, isNull, reason: '취소');
|
||||
// 편집: 외부 자막은 늘 · 내장 글 자막은 FFmpeg 가 있을 때만 · 이미지 자막은 아님
|
||||
final emb = SubtitleEntry.embedded(streamIndex: 2, codec: 'subrip', language: undetermined);
|
||||
final pgs = SubtitleEntry.embedded(streamIndex: 3, codec: 'hdmv_pgs_subtitle', language: undetermined);
|
||||
expect([
|
||||
SubtitleFiles.canEdit(v.subtitles.single, hasFfmpeg: false),
|
||||
SubtitleFiles.canEdit(emb, hasFfmpeg: true),
|
||||
SubtitleFiles.canEdit(emb, hasFfmpeg: false),
|
||||
SubtitleFiles.canEdit(pgs, hasFfmpeg: true),
|
||||
], [true, true, false, false]);
|
||||
});
|
||||
}
|
||||
|
||||
/// 고르기 · 저장 창 대신 정한 것을 돌려주는 저장소 (사용자가 고른 것) - 나머지는 실제 데스크톱 저장소
|
||||
class _ChosenStorage extends DesktopStorageService {
|
||||
_ChosenStorage(this.picked);
|
||||
final List<String> picked;
|
||||
String? saveTo;
|
||||
|
||||
@override
|
||||
Future<List<String>> pickSubtitles({String? initialDirectory}) async => picked;
|
||||
|
||||
@override
|
||||
Future<String?> saveAs({required String fileName, required Uint8List bytes, String? initialDirectory}) async {
|
||||
if (saveTo == null) return null;
|
||||
File(saveTo!).writeAsBytesSync(bytes);
|
||||
return saveTo;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue