JJ_MKVMaker/lib/app/subtitle_editor_controller....

155 lines
3.9 KiB
Dart

import 'package:flutter/foundation.dart';
import '../core/srt.dart';
/// 자막 편집 화면의 상태 (플랫폼 무관)
class SubtitleEditorController extends ChangeNotifier {
final List<Cue> cues;
/// 시간 입력 형식이 틀린 큐 (저장 막음)
final Set<Cue> badInput = {};
String saveCharset;
bool dirty = false;
SubtitleEditorController(this.cues, {required this.saveCharset});
List<int> get invalidOrder => invalidCues(cues);
bool get canSave => badInput.isEmpty && invalidOrder.isEmpty;
void setText(Cue c, String text) {
if (c.text == text) return;
c.text = text;
_changed();
}
/// 시간 입력. 형식이 틀리면 false.
bool setTime(Cue c, String input, {required bool isStart}) {
final d = parseSrtTime(input);
if (d == null) {
badInput.add(c);
notifyListeners();
return false;
}
badInput.remove(c);
if (isStart) {
c.start = d;
} else {
c.end = d;
}
_changed();
return true;
}
/// [i] 번째 뒤에 새 줄 추가 (없으면 맨 앞). 앞 큐 끝 + 0.1초 부터 2초 길이.
void insertAfter(int i) {
final start = i >= 0 && i < cues.length
? cues[i].end + const Duration(milliseconds: 100)
: Duration.zero;
cues.insert(i + 1, Cue(start, start + const Duration(seconds: 2), ''));
_changed();
}
void removeAt(int i) {
final c = cues.removeAt(i);
badInput.remove(c);
if (selected == c) selected = null;
_changed();
}
/// 전체 싱크 이동 (밀리초, 음수는 앞당김)
void shiftAll(int ms) {
if (ms == 0) return;
shiftCues(cues, Duration(milliseconds: ms));
_changed();
}
/// 시작 시간 순으로 정렬
void sortByTime() {
cues.sort((a, b) => a.start.compareTo(b.start));
_changed();
}
// ───────── 싱크 편집 (3단계) ─────────
/// 최소 자막 길이
static const minLength = Duration(milliseconds: 100);
/// 선택한 줄
Cue? selected;
void select(Cue? c) {
if (selected == c) return;
selected = c;
notifyListeners();
}
/// [pos] 시점에 표시되는 줄들
List<Cue> activeAt(Duration pos) =>
[for (final c in cues) if (c.start <= pos && pos < c.end) c];
/// 시작 시간 지정 (끝은 유지, 최소 길이 보장)
void setStart(Cue c, Duration d) {
d = _nonNeg(d);
if (d > c.end - minLength) d = _nonNeg(c.end - minLength);
c.start = d;
badInput.remove(c);
_changed();
}
/// 끝 시간 지정 (시작은 유지, 최소 길이 보장)
void setEnd(Cue c, Duration d) {
if (d < c.start + minLength) d = c.start + minLength;
c.end = d;
badInput.remove(c);
_changed();
}
/// 길이를 유지한 채 시작 시간을 옮김
void moveTo(Cue c, Duration start) {
final len = c.end - c.start;
c.start = _nonNeg(start);
c.end = c.start + len;
_changed();
}
/// [c] 와 그 뒤(시작 시간 기준)의 모든 줄을 함께 이동해 [c] 가 [start] 에서 시작하도록 맞춘다.
/// 앞부분 싱크는 맞고 중간부터 어긋난 자막을 고칠 때 사용.
void alignFrom(Cue c, Duration start) {
final delta = _nonNeg(start) - c.start;
if (delta == Duration.zero) return;
final from = c.start;
shiftCues(cues.where((e) => e.start >= from), delta);
_changed();
}
/// [pos] 에서 시작하는 새 줄 추가 후 선택
Cue insertAt(Duration pos) {
final c = Cue(_nonNeg(pos), _nonNeg(pos) + const Duration(seconds: 2), '');
var i = cues.indexWhere((e) => e.start > c.start);
if (i < 0) i = cues.length;
cues.insert(i, c);
selected = c;
_changed();
return c;
}
static Duration _nonNeg(Duration d) => d.isNegative ? Duration.zero : d;
void setCharset(String cs) {
saveCharset = cs;
notifyListeners();
}
void markSaved() {
dirty = false;
notifyListeners();
}
void _changed() {
dirty = true;
notifyListeners();
}
}