자막 편집 표 좁을 때 두 단 (F9) · 화면 · 색 보정 제목 줄 360 (F11)
- F9 (관리자 지시 10/21, 15 찾음): 412 에서 표가 오른쪽으로 넘침 → 좁으면 위 단 # · 시작 · 끝, 아래 단 내용 + 단추 (기본 누를 자리 그대로, 오른쪽 끝). 시간 칸은 줄이지 않음 - F11 (관리자 승인 10/21): 360 에서 [✕] 가 화면 밖 → 좁으면 제목은 줄이고 (…), [모두 초기화] 는 아이콘 단추 (도움말 이름 그대로, 누르면 같은 동작), [✕] 는 늘 보임. 안내 글은 숨기지 않고 제목 아래 줄로 (내 판단). 넓으면 그대로 - 시험: windows_ui2_shot (ko · ja × 360 · 412 · 960) - 넘침 없음 · ✕ 화면 안 · 단추 누를 자리 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>after_1020
parent
62ba78dfeb
commit
7247aeb55e
|
|
@ -25,13 +25,13 @@ import 'package:path/path.dart' as p;
|
||||||
import 'support/temp_dir.dart';
|
import 'support/temp_dir.dart';
|
||||||
|
|
||||||
/// 관리자 지시 (10/21, 28 찾음): ① 화면 · 색 보정 창 - 고른 비율 칩 ("세로 9:16") 은 ✓ 만 (그림 위에 체크가 겹치지 않게)
|
/// 관리자 지시 (10/21, 28 찾음): ① 화면 · 색 보정 창 - 고른 비율 칩 ("세로 9:16") 은 ✓ 만 (그림 위에 체크가 겹치지 않게)
|
||||||
/// ② 자막 편집 화면 - 제목은 "자막 편집", 파일 이름은 그 아래 한 줄 (가운데를 줄여 끝 .srt 가 보이게). ko · ja × 412 · 960
|
/// ② 자막 편집 화면 - 제목은 "자막 편집", 파일 이름은 그 아래 한 줄 (가운데를 줄여 끝 .srt 가 보이게). ko · ja × 360 · 412 · 960
|
||||||
void main() {
|
void main() {
|
||||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
final out = Platform.environment['JJ_SHOT_DIR'] ?? '';
|
final out = Platform.environment['JJ_SHOT_DIR'] ?? '';
|
||||||
|
|
||||||
for (final lang in ['ko', 'ja']) {
|
for (final lang in ['ko', 'ja']) {
|
||||||
for (final (w, h) in [(412, 900), (960, 600)]) {
|
for (final (w, h) in [(360, 780), (412, 900), (960, 600)]) {
|
||||||
testWidgets('화면 · 색 보정 칩 · 자막 편집 제목 $lang ${w}x$h', (t) async {
|
testWidgets('화면 · 색 보정 칩 · 자막 편집 제목 $lang ${w}x$h', (t) async {
|
||||||
t.view.physicalSize = Size(w.toDouble(), h.toDouble());
|
t.view.physicalSize = Size(w.toDouble(), h.toDouble());
|
||||||
t.view.devicePixelRatio = 1;
|
t.view.devicePixelRatio = 1;
|
||||||
|
|
@ -70,14 +70,16 @@ void main() {
|
||||||
|
|
||||||
Future<void> shot(String name) async {
|
Future<void> shot(String name) async {
|
||||||
await settle(3);
|
await settle(3);
|
||||||
expect(t.takeException(), isNull, reason: '$name 넘침');
|
// 넘쳐도 무엇이 넘쳤는지 볼 수 있게 먼저 찍는다
|
||||||
if (out.isEmpty) return;
|
if (out.isNotEmpty) {
|
||||||
final b = shotKey.currentContext!.findRenderObject()! as RenderRepaintBoundary;
|
final b = shotKey.currentContext!.findRenderObject()! as RenderRepaintBoundary;
|
||||||
final img = await t.runAsync(() => b.toImage());
|
final img = await t.runAsync(() => b.toImage());
|
||||||
final bytes = await t.runAsync(() => img!.toByteData(format: ui.ImageByteFormat.png));
|
final bytes = await t.runAsync(() => img!.toByteData(format: ui.ImageByteFormat.png));
|
||||||
Directory(out).createSync(recursive: true);
|
Directory(out).createSync(recursive: true);
|
||||||
File(p.join(out, 'ui2_${name}_${lang}_$w.png')).writeAsBytesSync(bytes!.buffer.asUint8List());
|
File(p.join(out, 'ui2_${name}_${lang}_$w.png')).writeAsBytesSync(bytes!.buffer.asUint8List());
|
||||||
}
|
}
|
||||||
|
expect(t.takeException(), isNull, reason: '$name 넘침');
|
||||||
|
}
|
||||||
|
|
||||||
// ① 화면 · 색 보정 - "세로 9:16" 을 고른다
|
// ① 화면 · 색 보정 - "세로 9:16" 을 고른다
|
||||||
unawaited(showVideoAdjust(navKey.currentContext!, c));
|
unawaited(showVideoAdjust(navKey.currentContext!, c));
|
||||||
|
|
@ -92,6 +94,11 @@ void main() {
|
||||||
expect(find.descendant(of: chip, matching: find.byIcon(Icons.check)), findsOneWidget, reason: '✓ 만');
|
expect(find.descendant(of: chip, matching: find.byIcon(Icons.check)), findsOneWidget, reason: '✓ 만');
|
||||||
expect(find.descendant(of: chip, matching: find.byIcon(Icons.crop_portrait)), findsNothing);
|
expect(find.descendant(of: chip, matching: find.byIcon(Icons.crop_portrait)), findsNothing);
|
||||||
await shot('1_adjust');
|
await shot('1_adjust');
|
||||||
|
// F11 (관리자 10/21): 좁아도 [✕] 는 화면 안, [모두 초기화] 는 (좁으면 아이콘이라도) 도움말 이름 그대로
|
||||||
|
final close = t.getRect(find.byTooltip(tr('닫기')));
|
||||||
|
expect(close.right <= w + 0.5, isTrue, reason: '✕ 가 화면 안 ($close)');
|
||||||
|
expect(find.byTooltip(tr('모두 초기화')).evaluate().isNotEmpty || find.text(tr('모두 초기화')).evaluate().isNotEmpty, isTrue);
|
||||||
|
expect(find.textContaining(tr('MKV 만들기에 적용됩니다 (영상을 다시 인코딩)')), findsOneWidget, reason: '안내 글은 숨기지 않는다');
|
||||||
navKey.currentState!.popUntil((r) => r.isFirst);
|
navKey.currentState!.popUntil((r) => r.isFirst);
|
||||||
await settle();
|
await settle();
|
||||||
|
|
||||||
|
|
@ -106,6 +113,12 @@ void main() {
|
||||||
final name = find.textContaining('.srt');
|
final name = find.textContaining('.srt');
|
||||||
expect(name, findsOneWidget, reason: '파일 이름은 아래 한 줄 - 끝 (.srt) 이 보임');
|
expect(name, findsOneWidget, reason: '파일 이름은 아래 한 줄 - 끝 (.srt) 이 보임');
|
||||||
await shot('2_editor');
|
await shot('2_editor');
|
||||||
|
// F9 (관리자 10/21): 줄의 단추는 줄이지 않는다 - 기본 누를 자리 48 (+ 기기의 밀도 조정: Windows 는 compact 라 40, Android 는 48)
|
||||||
|
final density = Theme.of(t.element(find.byType(SubtitleEditorPage))).visualDensity.baseSizeAdjustment.dx;
|
||||||
|
for (final tip in [tr('아래에 줄 추가'), tr('줄 삭제')]) {
|
||||||
|
final r = t.getRect(find.byTooltip(tip));
|
||||||
|
expect(r.width >= 48 + density - 0.5 && r.height >= 48 + density - 0.5, isTrue, reason: '$tip 누를 자리 $r (밀도 조정 $density)');
|
||||||
|
}
|
||||||
expect(t.widget<Text>(name).data!.endsWith('.srt'), isTrue, reason: '${t.widget<Text>(name).data}');
|
expect(t.widget<Text>(name).data!.endsWith('.srt'), isTrue, reason: '${t.widget<Text>(name).data}');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -615,12 +615,12 @@ class _HeaderRow extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 관리자 지시 (10/21, 15 찾음 F9): 자막 편집 표는 # · 시작 · 끝 · 단추 칸 폭이 정해져 있어 (48 + 130 + 130 + 96) 폰 세로 (412) 에서
|
/// 관리자 지시 (10/21, 15 찾음 F9): 자막 편집 표는 # · 시작 · 끝 · 단추 칸 폭이 정해져 있어 (48 + 130 + 130 + 96) 폰 세로 (412) 에서
|
||||||
/// 오른쪽으로 넘쳤다 - 내용 칸까지 한 줄에 들지 않으면 두 단 (위: # · 시작 · 끝 · 단추, 아래: 내용). 시간 칸은 줄이지 않는다 (편집이 어려움)
|
/// 오른쪽으로 넘쳤다 - 내용 칸까지 한 줄에 들지 않으면 두 단 (위: # · 시작 · 끝, 아래: 내용 · 단추). 시간 칸은 줄이지 않는다 (편집이 어려움).
|
||||||
|
/// 단추는 두 단이 되는 폰 (손가락) 에서 누를 자리 48 을 지킨다 (관리자 10/21)
|
||||||
bool _cueNarrow(double width) => width < 48 + 130 + 130 + 96 + 200;
|
bool _cueNarrow(double width) => width < 48 + 130 + 130 + 96 + 200;
|
||||||
|
|
||||||
/// 두 단일 때 # 칸 · 단추 하나의 폭 (위 단이 412 에 들게: 28 + 130 + 130 + 2 × 40 = 368)
|
/// 두 단일 때 # 칸 폭 (위 단 28 + 130 + 130 = 288 - 360 폭에도 듬)
|
||||||
const _narrowNo = 28.0;
|
const _narrowNo = 28.0;
|
||||||
const _narrowButton = 40.0;
|
|
||||||
|
|
||||||
const _hdr = TextStyle(fontSize: 12, color: JjColors.textDim);
|
const _hdr = TextStyle(fontSize: 12, color: JjColors.textDim);
|
||||||
|
|
||||||
|
|
@ -723,22 +723,16 @@ class _CueRowState extends State<_CueRow> {
|
||||||
onTap: () => e.select(widget.cue),
|
onTap: () => e.select(widget.cue),
|
||||||
onChanged: (t) => e.setText(widget.cue, t),
|
onChanged: (t) => e.setText(widget.cue, t),
|
||||||
);
|
);
|
||||||
// 좁으면 단추 누를 자리를 40 으로 (기본 48) - 위 단이 폭에 들게
|
|
||||||
final small = narrow ? const BoxConstraints(minWidth: _narrowButton, minHeight: _narrowButton) : null;
|
|
||||||
final buttons = [
|
final buttons = [
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: tr('아래에 줄 추가'),
|
tooltip: tr('아래에 줄 추가'),
|
||||||
iconSize: 18,
|
iconSize: 18,
|
||||||
constraints: small,
|
|
||||||
padding: narrow ? EdgeInsets.zero : null,
|
|
||||||
icon: const Icon(Icons.add, color: JjColors.textDim),
|
icon: const Icon(Icons.add, color: JjColors.textDim),
|
||||||
onPressed: () => e.insertAfter(widget.index),
|
onPressed: () => e.insertAfter(widget.index),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: tr('줄 삭제'),
|
tooltip: tr('줄 삭제'),
|
||||||
iconSize: 18,
|
iconSize: 18,
|
||||||
constraints: small,
|
|
||||||
padding: narrow ? EdgeInsets.zero : null,
|
|
||||||
icon: const Icon(Icons.delete_outline, color: JjColors.textDim),
|
icon: const Icon(Icons.delete_outline, color: JjColors.textDim),
|
||||||
onPressed: () => e.removeAt(widget.index),
|
onPressed: () => e.removeAt(widget.index),
|
||||||
),
|
),
|
||||||
|
|
@ -752,10 +746,13 @@ class _CueRowState extends State<_CueRow> {
|
||||||
SizedBox(width: 96, child: Row(children: buttons)),
|
SizedBox(width: 96, child: Row(children: buttons)),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
// F9: 좁을 때 두 단 - 위 # · 시작 · 끝 · 단추, 아래 내용 (# 칸만큼 들여)
|
// F9: 좁을 때 두 단 - 위 # · 시작 · 끝, 아래 내용 (# 칸만큼 들여) · 단추 (오른쪽 끝, 누를 자리 48)
|
||||||
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
Row(crossAxisAlignment: CrossAxisAlignment.start, children: [number, start, end, const Spacer(), ...buttons]),
|
Row(crossAxisAlignment: CrossAxisAlignment.start, children: [number, start, end]),
|
||||||
Padding(padding: const EdgeInsets.only(left: _narrowNo), child: text),
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: _narrowNo),
|
||||||
|
child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [Expanded(child: text), ...buttons]),
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -122,22 +122,39 @@ class _VideoAdjustState extends State<_VideoAdjust> {
|
||||||
Container(
|
Container(
|
||||||
color: JjColors.panelHigh,
|
color: JjColors.panelHigh,
|
||||||
padding: const EdgeInsets.fromLTRB(16, 8, 8, 8),
|
padding: const EdgeInsets.fromLTRB(16, 8, 8, 8),
|
||||||
child: Row(children: [
|
// 관리자 지시 (10/21, 15 찾음 F11): 360 에서 제목 줄이 넘쳐 [✕] 가 화면 밖으로 밀렸다 - 좁으면 제목은 줄이고 (…)
|
||||||
|
// [모두 초기화] 는 아이콘 단추 (도움말 · 읽어 주기 이름 그대로) 로, [✕] 는 늘 보이게. 넓으면 그대로
|
||||||
|
child: LayoutBuilder(builder: (context, box) {
|
||||||
|
final narrow = box.maxWidth < 560;
|
||||||
|
final reset = s.adjusts ? () => _set(s.resetAdjust()) : null;
|
||||||
|
return Row(children: [
|
||||||
const Icon(Icons.tune, color: JjColors.accent, size: 20),
|
const Icon(Icons.tune, color: JjColors.accent, size: 20),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
|
if (narrow)
|
||||||
|
// 안내 글은 숨기지 않고 제목 아래 줄로
|
||||||
|
Expanded(
|
||||||
|
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Text(tr('화면 · 색 보정'),
|
||||||
|
maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
||||||
|
Text(tr('MKV 만들기에 적용됩니다 (영상을 다시 인코딩)'),
|
||||||
|
maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
else ...[
|
||||||
Text(tr('화면 · 색 보정'), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
Text(tr('화면 · 색 보정'), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(tr('MKV 만들기에 적용됩니다 (영상을 다시 인코딩)'),
|
child: Text(tr('MKV 만들기에 적용됩니다 (영상을 다시 인코딩)'),
|
||||||
overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||||
),
|
),
|
||||||
TextButton.icon(
|
],
|
||||||
onPressed: s.adjusts ? () => _set(s.resetAdjust()) : null,
|
if (narrow)
|
||||||
icon: const Icon(Icons.restart_alt, size: 18),
|
IconButton(tooltip: tr('모두 초기화'), icon: const Icon(Icons.restart_alt), onPressed: reset)
|
||||||
label: Text(tr('모두 초기화')),
|
else
|
||||||
),
|
TextButton.icon(onPressed: reset, icon: const Icon(Icons.restart_alt, size: 18), label: Text(tr('모두 초기화'))),
|
||||||
IconButton(tooltip: tr('닫기'), icon: const Icon(Icons.close), onPressed: () => Navigator.pop(context)),
|
IconButton(tooltip: tr('닫기'), icon: const Icon(Icons.close), onPressed: () => Navigator.pop(context)),
|
||||||
]),
|
]);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: wide
|
child: wide
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue