187 lines
7.3 KiB
Dart
187 lines
7.3 KiB
Dart
/// 약속: 자막 한 줄 (언어 · 문자셋 고르기 · 편집 · 번역 · 빼기) - 바꾸기는 AppController 가.
|
|
library;
|
|
|
|
import 'dart:math' as math;
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../../app/app_controller.dart';
|
|
import '../../core/charset_detector.dart';
|
|
import '../../core/languages.dart';
|
|
import '../../core/models.dart';
|
|
import '../subtitle_editor_page.dart';
|
|
import '../theme.dart';
|
|
import '../translate_dialog.dart';
|
|
import '../../l10n/tr.dart';
|
|
|
|
class SubtitleRow extends StatelessWidget {
|
|
final AppController c;
|
|
final VideoItem v;
|
|
final SubtitleEntry s;
|
|
final bool locked;
|
|
const SubtitleRow(
|
|
{super.key, required this.c, required this.v, required this.s, required this.locked});
|
|
|
|
/// 관리자 지시 (10/21, 15 찾음 F6): 한 줄에 들어가는 폭 - 내장 표 · 이름 (최소) · 언어 고르기 · 문자셋 · 단추.
|
|
/// 고르기는 가장 긴 항목 글의 폭을 쓰므로 (DropdownButton) 그 글을 지금 글꼴 · 글자 배율로 잰다
|
|
static double _oneLineWidth(BuildContext context, {required bool embedded, required int buttons}) {
|
|
final scaler = MediaQuery.textScalerOf(context);
|
|
final base = DefaultTextStyle.of(context).style;
|
|
double textW(String t, double size) =>
|
|
(TextPainter(text: TextSpan(text: t, style: base.copyWith(fontSize: size)), textDirection: TextDirection.ltr, textScaler: scaler, maxLines: 1)
|
|
..layout())
|
|
.width;
|
|
double widest(Iterable<String> xs, double size) => xs.fold(0.0, (m, t) => math.max(m, textW(t, size)));
|
|
final lang = widest([tr('언어: 미지정'), for (final l in languages) '${l.name} (${l.code})'], 12) + 24;
|
|
final charset = embedded ? 0.0 : widest(supportedCharsets.values, 12) + 24 + 8;
|
|
final badge = textW(embedded ? tr('내장') : tr('외부'), 11) + 12;
|
|
const nameMin = 60.0;
|
|
return badge + 10 + nameMin + 8 + lang + charset + 4 + buttons * 48;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final embedded = s.kind == SubtitleKind.embedded;
|
|
final off = !s.enabled;
|
|
final badge = Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: (embedded ? JjColors.textDim : JjColors.accent)
|
|
.withValues(alpha: 0.18),
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: Text(embedded ? tr('내장') : tr('외부'),
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
color: embedded ? JjColors.textDim : JjColors.accent)),
|
|
);
|
|
final name = Text(
|
|
s.displayName,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: off ? JjColors.textDim : JjColors.text,
|
|
decoration: off ? TextDecoration.lineThrough : null,
|
|
),
|
|
);
|
|
final pickers = <Widget>[
|
|
LanguageDropdown(
|
|
value: s.language,
|
|
onChanged: locked || off ? null : (l) => c.setLanguage(s, l),
|
|
),
|
|
if (!embedded)
|
|
CharsetDropdown(
|
|
value: s.charset ?? 'UTF-8',
|
|
onChanged: locked ? null : (cs) => c.setCharset(s, cs),
|
|
),
|
|
];
|
|
final buttons = <Widget>[
|
|
IconButton(
|
|
tooltip: c.canEdit(s) ? tr('내용 편집 · 문자셋 변환') : tr('이미지 자막은 편집할 수 없습니다'),
|
|
iconSize: 18,
|
|
onPressed: locked || off || !c.canEdit(s)
|
|
? null
|
|
: () => openSubtitleEditor(context, c, v, s),
|
|
icon: Icon(Icons.edit_note, color: JjColors.accent),
|
|
),
|
|
if (c.services.createTranslator != null)
|
|
IconButton(
|
|
tooltip: c.canEdit(s) ? trf('다른 언어로 번역 (AI · 이 {0}에서)', [Platform.isAndroid ? tr('기기') : 'PC']) : tr('이미지 자막은 번역할 수 없습니다'),
|
|
iconSize: 18,
|
|
onPressed: locked || off || !c.canEdit(s) ? null : () => showTranslateDialog(context, c, v, s),
|
|
icon: Icon(Icons.translate, color: JjColors.accent),
|
|
),
|
|
IconButton(
|
|
tooltip: embedded ? (off ? tr('되살리기') : tr('삭제 (출력에서 제외)')) : tr('목록에서 제거'),
|
|
iconSize: 18,
|
|
onPressed: locked ? null : () => c.removeSubtitle(v, s),
|
|
icon: Icon(
|
|
embedded && off ? Icons.undo : Icons.delete_outline,
|
|
color: JjColors.textDim,
|
|
),
|
|
),
|
|
];
|
|
return Container(
|
|
margin: const EdgeInsets.only(bottom: 6),
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: JjColors.panel,
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: JjColors.border),
|
|
),
|
|
child: LayoutBuilder(builder: (context, box) {
|
|
final fits = box.maxWidth >= _oneLineWidth(context, embedded: embedded, buttons: buttons.length);
|
|
if (fits) {
|
|
// 넓을 때: 지금처럼 한 줄
|
|
return Row(children: [
|
|
badge,
|
|
const SizedBox(width: 10),
|
|
Expanded(child: name),
|
|
const SizedBox(width: 8),
|
|
for (final (i, x) in pickers.indexed) ...[if (i > 0) const SizedBox(width: 8), x],
|
|
const SizedBox(width: 4),
|
|
...buttons,
|
|
]);
|
|
}
|
|
// 좁아 넘칠 때만 두 줄: 위 = 내장 표 · 트랙 이름, 아래 = 언어 고르기 · 단추 (언어 이름은 줄이지 않는다 - 무엇을 골랐는지 보이게)
|
|
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
|
Row(children: [badge, const SizedBox(width: 10), Expanded(child: name)]),
|
|
const SizedBox(height: 2),
|
|
Wrap(
|
|
crossAxisAlignment: WrapCrossAlignment.center,
|
|
spacing: 8,
|
|
children: [...pickers, Row(mainAxisSize: MainAxisSize.min, children: buttons)],
|
|
),
|
|
]);
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
class LanguageDropdown extends StatelessWidget {
|
|
final Language value;
|
|
final ValueChanged<Language>? onChanged;
|
|
const LanguageDropdown({super.key, required this.value, required this.onChanged});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final items = [undetermined, ...languages];
|
|
return DropdownButton<Language>(
|
|
value: items.contains(value) ? value : undetermined,
|
|
isDense: true,
|
|
underline: const SizedBox(),
|
|
style: TextStyle(fontSize: 12, color: JjColors.text),
|
|
items: [
|
|
for (final l in items)
|
|
DropdownMenuItem(
|
|
value: l,
|
|
child: Text(l == undetermined ? tr('언어: 미지정') : '${l.name} (${l.code})'),
|
|
),
|
|
],
|
|
onChanged: onChanged == null ? null : (l) => onChanged!(l!),
|
|
);
|
|
}
|
|
}
|
|
|
|
class CharsetDropdown extends StatelessWidget {
|
|
final String value;
|
|
final ValueChanged<String>? onChanged;
|
|
const CharsetDropdown({super.key, required this.value, required this.onChanged});
|
|
|
|
@override
|
|
Widget build(BuildContext context) => DropdownButton<String>(
|
|
value: supportedCharsets.containsKey(value) ? value : 'UTF-8',
|
|
isDense: true,
|
|
underline: const SizedBox(),
|
|
style: TextStyle(fontSize: 12, color: JjColors.text),
|
|
items: [
|
|
for (final e in supportedCharsets.entries)
|
|
DropdownMenuItem(value: e.key, child: Text(e.value)),
|
|
],
|
|
onChanged: onChanged == null ? null : (v) => onChanged!(v!),
|
|
);
|
|
}
|
|
|
|
// ───────── 아래: 작업 기록 ─────────
|