diff --git a/integration_test/windows_ui2_shot_test.dart b/integration_test/windows_ui2_shot_test.dart index 341c70f..c081c23 100644 --- a/integration_test/windows_ui2_shot_test.dart +++ b/integration_test/windows_ui2_shot_test.dart @@ -25,13 +25,14 @@ import 'package:path/path.dart' as p; import 'support/temp_dir.dart'; /// 관리자 지시 (10/21, 28 찾음): ① 화면 · 색 보정 창 - 고른 비율 칩 ("세로 9:16") 은 ✓ 만 (그림 위에 체크가 겹치지 않게) -/// ② 자막 편집 화면 - 제목은 "자막 편집", 파일 이름은 그 아래 한 줄 (가운데를 줄여 끝 .srt 가 보이게). ko · ja × 412 · 960 +/// ② 자막 편집 화면 - 제목은 "자막 편집", 파일 이름은 그 아래 한 줄 (가운데를 줄여 끝 .srt 가 보이게). ko · ja × 360 · 412 · 960 +/// F11 (7247aeb, after_1020 에서 옮김): 360 에서도 [✕] 는 화면 안 · [모두 초기화] 는 도움말 이름 그대로 · 안내 글은 숨기지 않음 void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); final out = Platform.environment['JJ_SHOT_DIR'] ?? ''; 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 { t.view.physicalSize = Size(w.toDouble(), h.toDouble()); t.view.devicePixelRatio = 1; @@ -70,13 +71,15 @@ void main() { Future shot(String name) async { await settle(3); + // 넘쳐도 무엇이 넘쳤는지 볼 수 있게 먼저 찍는다 + if (out.isNotEmpty) { + final b = shotKey.currentContext!.findRenderObject()! as RenderRepaintBoundary; + final img = await t.runAsync(() => b.toImage()); + final bytes = await t.runAsync(() => img!.toByteData(format: ui.ImageByteFormat.png)); + Directory(out).createSync(recursive: true); + File(p.join(out, 'ui2_${name}_${lang}_$w.png')).writeAsBytesSync(bytes!.buffer.asUint8List()); + } expect(t.takeException(), isNull, reason: '$name 넘침'); - if (out.isEmpty) return; - final b = shotKey.currentContext!.findRenderObject()! as RenderRepaintBoundary; - final img = await t.runAsync(() => b.toImage()); - final bytes = await t.runAsync(() => img!.toByteData(format: ui.ImageByteFormat.png)); - Directory(out).createSync(recursive: true); - File(p.join(out, 'ui2_${name}_${lang}_$w.png')).writeAsBytesSync(bytes!.buffer.asUint8List()); } // ① 화면 · 색 보정 - "세로 9:16" 을 고른다 @@ -92,6 +95,11 @@ void main() { expect(find.descendant(of: chip, matching: find.byIcon(Icons.check)), findsOneWidget, reason: '✓ 만'); expect(find.descendant(of: chip, matching: find.byIcon(Icons.crop_portrait)), findsNothing); 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); await settle(); @@ -106,6 +114,12 @@ void main() { final name = find.textContaining('.srt'); expect(name, findsOneWidget, reason: '파일 이름은 아래 한 줄 - 끝 (.srt) 이 보임'); await shot('2_editor'); + // F9 (관리자 10/21, 7247aeb 에서 옮김): 줄의 단추는 줄이지 않는다 - 기본 누를 자리 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).first); + expect(r.width >= 48 + density - 0.5 && r.height >= 48 + density - 0.5, isTrue, reason: '$tip 누를 자리 $r (밀도 조정 $density)'); + } expect(t.widget(name).data!.endsWith('.srt'), isTrue, reason: '${t.widget(name).data}'); }); } diff --git a/lib/ui/subtitle_editor_panes.dart b/lib/ui/subtitle_editor_panes.dart index 8ac4d02..0c35305 100644 --- a/lib/ui/subtitle_editor_panes.dart +++ b/lib/ui/subtitle_editor_panes.dart @@ -254,12 +254,12 @@ class _HeaderRow extends StatelessWidget { } /// 관리자 지시 (10/21, 15 찾음 F9): 자막 편집 표는 # · 시작 · 끝 · 단추 칸 폭이 정해져 있어 (48 + 130 + 130 + 96) 폰 세로 (412) 에서 -/// 오른쪽으로 넘쳤다 - 내용 칸까지 한 줄에 들지 않으면 두 단 (위: # · 시작 · 끝 · 단추, 아래: 내용). 시간 칸은 줄이지 않는다 (편집이 어려움) +/// 오른쪽으로 넘쳤다 - 내용 칸까지 한 줄에 들지 않으면 두 단 (위: # · 시작 · 끝, 아래: 내용 · 단추). 시간 칸은 줄이지 않는다 (편집이 어려움). +/// 단추는 두 단이 되는 폰 (손가락) 에서 누를 자리 48 을 지킨다 (관리자 10/21, 7247aeb 는 after_1020 에만 있어 다시 - 360 에서 위 단이 넘쳤다) 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 _narrowButton = 40.0; final _hdr = TextStyle(fontSize: 12, color: JjColors.textDim); @@ -362,22 +362,16 @@ class _CueRowState extends State<_CueRow> { onTap: () => e.select(widget.cue), onChanged: (t) => e.setText(widget.cue, t), ); - // 좁으면 단추 누를 자리를 40 으로 (기본 48) - 위 단이 폭에 들게 - final small = narrow ? const BoxConstraints(minWidth: _narrowButton, minHeight: _narrowButton) : null; final buttons = [ IconButton( tooltip: tr('아래에 줄 추가'), iconSize: 18, - constraints: small, - padding: narrow ? EdgeInsets.zero : null, icon: Icon(Icons.add, color: JjColors.textDim), onPressed: () => e.insertAfter(widget.index), ), IconButton( tooltip: tr('줄 삭제'), iconSize: 18, - constraints: small, - padding: narrow ? EdgeInsets.zero : null, icon: Icon(Icons.delete_outline, color: JjColors.textDim), onPressed: () => e.removeAt(widget.index), ), @@ -391,10 +385,13 @@ class _CueRowState extends State<_CueRow> { SizedBox(width: 96, child: Row(children: buttons)), ]); } - // F9: 좁을 때 두 단 - 위 # · 시작 · 끝 · 단추, 아래 내용 (# 칸만큼 들여) + // F9: 좁을 때 두 단 - 위 # · 시작 · 끝, 아래 내용 (# 칸만큼 들여) · 단추 (오른쪽 끝, 누를 자리 48) return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row(crossAxisAlignment: CrossAxisAlignment.start, children: [number, start, end, const Spacer(), ...buttons]), - Padding(padding: const EdgeInsets.only(left: _narrowNo), child: text), + Row(crossAxisAlignment: CrossAxisAlignment.start, children: [number, start, end]), + Padding( + padding: const EdgeInsets.only(left: _narrowNo), + child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [Expanded(child: text), ...buttons]), + ), ]); }), );