diff --git a/lib/ui/home_page.dart b/lib/ui/home_page.dart index 9244980..f92f9f8 100644 --- a/lib/ui/home_page.dart +++ b/lib/ui/home_page.dart @@ -1075,7 +1075,53 @@ class _VideoDetail extends StatelessWidget { Widget _list(BuildContext context, MediaInfo? info, StreamInfo? video, int audioCount, bool locked, {Widget? moveButtons, double bottom = 88}) { - final side = isCompact(context) ? 12.0 : 20.0; + final compact = isCompact(context); + final side = compact ? 12.0 : 20.0; + // 198: 마지막 AI 자막의 원어를 자동 감지했으면 - 알림을 놓쳐도 여기서 다른 언어로 다시 만들 수 있게 + final detected = v.aiDetected == null + ? null + : Wrap(crossAxisAlignment: WrapCrossAlignment.center, spacing: 8, children: [ + Text(trf('감지한 언어: {0} (자동 감지)', [languageOf(v.aiDetected).name]), + style: const TextStyle(fontSize: 12, color: JjColors.textDim)), + TextButton( + onPressed: c.busy + ? null + : () async { + final lang = await pickSourceLanguage(context, languageOf(v.aiDetected)); + if (lang != null) await c.generateAiSubtitles([v], c.aiOptions.copyWith(source: lang)); + }, + child: Text(tr('다른 언어로 다시')), + ), + ]); + // 자막 머리 줄: 좁으면 버튼이 다음 줄로 (오른쪽 정렬) + final subtitleActions = Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text(tr('자막'), style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), + ), + const SizedBox(width: 16), + Expanded( + child: Wrap(alignment: WrapAlignment.end, spacing: 8, runSpacing: 8, children: [ + if (c.aiAvailable) + FilledButton.icon( + onPressed: locked || c.ffmpegVersion == null ? null : () => showAiDialog(context, c, v), + icon: const Icon(Icons.auto_awesome, size: 18), + label: Text(tr('AI 자막 만들기')), + ), + if (c.subtitleProvider != null) + OutlinedButton.icon( + onPressed: locked ? null : () => showSubtitleSearch(context, c, v), + icon: const Icon(Icons.travel_explore, size: 18), + label: Text(tr('인터넷 자막 찾기')), + ), + OutlinedButton.icon( + onPressed: locked ? null : () => c.pickSubtitlesFor(v), + icon: const Icon(Icons.subtitles_outlined, size: 18), + label: Text(tr('자막 파일 추가')), + ), + ]), + ), + ]); return ListView( padding: EdgeInsets.fromLTRB(side, side, side, bottom), children: [ @@ -1111,26 +1157,16 @@ class _VideoDetail extends StatelessWidget { ], _Chip(trf('음성 {0}개', [audioCount])), ]), - if (moveButtons != null) ...[ - const SizedBox(height: 10), - moveButtons, - ], - // 198: 마지막 AI 자막의 원어를 자동 감지했으면 - 알림을 놓쳐도 여기서 다른 언어로 다시 만들 수 있게 - if (v.aiDetected != null) ...[ - const SizedBox(height: 8), - Wrap(crossAxisAlignment: WrapCrossAlignment.center, spacing: 8, children: [ - Text(trf('감지한 언어: {0} (자동 감지)', [languageOf(v.aiDetected).name]), - style: const TextStyle(fontSize: 12, color: JjColors.textDim)), - TextButton( - onPressed: c.busy - ? null - : () async { - final lang = await pickSourceLanguage(context, languageOf(v.aiDetected)); - if (lang != null) await c.generateAiSubtitles([v], c.aiOptions.copyWith(source: lang)); - }, - child: Text(tr('다른 언어로 다시')), - ), - ]), + // 261: 좁은 화면 (폰 세로) 은 세부 정보 칸이 낮다 (노트 10+ 에서 약 210) - "감지한 언어" 줄 · 이동 버튼이 + // [AI 자막 만들기] 를 칸 밖으로 밀었다. 자막 버튼을 칩 바로 아래에 두고, 감지한 언어 · 이동 버튼은 그 다음 + if (compact) ...[ + const SizedBox(height: 12), + subtitleActions, + if (detected != null) ...[const SizedBox(height: 4), detected], + if (moveButtons != null) ...[const SizedBox(height: 10), moveButtons], + ] else ...[ + if (moveButtons != null) ...[const SizedBox(height: 10), moveButtons], + if (detected != null) ...[const SizedBox(height: 8), detected], ], if (v.message != null) ...[ const SizedBox(height: 12), @@ -1144,36 +1180,10 @@ class _VideoDetail extends StatelessWidget { style: const TextStyle(color: JjColors.danger, fontSize: 12)), ), ], - const SizedBox(height: 24), - // 좁으면 버튼이 다음 줄로 (오른쪽 정렬) - Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.only(top: 8), - child: Text(tr('자막'), style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), - ), - const SizedBox(width: 16), - Expanded( - child: Wrap(alignment: WrapAlignment.end, spacing: 8, runSpacing: 8, children: [ - if (c.aiAvailable) - FilledButton.icon( - onPressed: locked || c.ffmpegVersion == null ? null : () => showAiDialog(context, c, v), - icon: const Icon(Icons.auto_awesome, size: 18), - label: Text(tr('AI 자막 만들기')), - ), - if (c.subtitleProvider != null) - OutlinedButton.icon( - onPressed: locked ? null : () => showSubtitleSearch(context, c, v), - icon: const Icon(Icons.travel_explore, size: 18), - label: Text(tr('인터넷 자막 찾기')), - ), - OutlinedButton.icon( - onPressed: locked ? null : () => c.pickSubtitlesFor(v), - icon: const Icon(Icons.subtitles_outlined, size: 18), - label: Text(tr('자막 파일 추가')), - ), - ]), - ), - ]), + if (!compact) ...[ + const SizedBox(height: 24), + subtitleActions, + ], const SizedBox(height: 8), if (v.subtitles.isEmpty) Padding( diff --git a/test/mkv_portrait_detail_test.dart b/test/mkv_portrait_detail_test.dart new file mode 100644 index 0000000..32bbeaa --- /dev/null +++ b/test/mkv_portrait_detail_test.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:jj_mkvmaker/app/app_controller.dart'; +import 'package:jj_mkvmaker/app/settings.dart'; +import 'package:jj_mkvmaker/core/models.dart'; +import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart'; +import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart'; +import 'package:jj_mkvmaker/services/platform_services.dart'; +import 'package:jj_mkvmaker/ui/home_page.dart'; + +/// 261: 폰 세로 (노트 10+ 412×869) 의 MKV 세부 정보 - "감지한 언어" 줄이 생겨도 [AI 자막 만들기] 가 밀려 가려지지 않는다 +void main() { + for (final detected in [false, true]) { + testWidgets('261: 폰 세로 · 작업 기록 켬 · 감지한 언어 ${detected ? '있음' : '없음'} - [AI 자막 만들기] 가 스크롤 없이 보인다', (tester) async { + tester.view.physicalSize = const Size(412, 869); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + final c = AppController(PlatformServices( + mediaTool: ProcessMediaTool('x', 'y'), + storage: DesktopStorageService(), + createRecognizer: () => throw UnimplementedError(), + createTranslator: () => throw UnimplementedError(), + )); + c.settings.showLog = true; + c.settings.moveTargets = [MoveTarget('보관', r'D:\보관')]; + for (var i = 1; i <= 4; i++) { + c.videos.add(VideoItem('D:/v/아주 긴 이름의 동영상 파일 $i.mp4')); + } + c.selected = c.videos.first..aiDetected = detected ? 'en' : null; + await tester.pumpWidget(MaterialApp(home: HomePage(c: c))); + await tester.pump(); + expect(tester.takeException(), isNull); + final ai = find.text('AI 자막 만들기'); + // 화면 밖이면 ListView 가 아예 만들지 않는다 + expect(ai, findsOneWidget, reason: '세부 정보 칸 아래로 밀림'); + final view = tester.getRect(find.ancestor(of: ai, matching: find.byType(Scrollable)).first); + final button = tester.getRect(ai); + expect(button.bottom, lessThanOrEqualTo(view.bottom), reason: '버튼 $button · 세부 정보 칸 $view'); + }); + } +}