WIP 261 폰 세로 MKV 세부 정보: 자막 버튼을 칩 바로 아래로 - 감지한 언어 줄 · 이동 버튼이 [AI 자막 만들기] 를 밀어내지 않게 (검토 대기)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
backup_묶음6_d46e089
RyuJongJin 2026-10-12 08:32:27 +09:00
parent 5646c976a8
commit 2d2bd40824
2 changed files with 102 additions and 51 deletions

View File

@ -1075,7 +1075,53 @@ class _VideoDetail extends StatelessWidget {
Widget _list(BuildContext context, MediaInfo? info, StreamInfo? video, int audioCount, bool locked, Widget _list(BuildContext context, MediaInfo? info, StreamInfo? video, int audioCount, bool locked,
{Widget? moveButtons, double bottom = 88}) { {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( return ListView(
padding: EdgeInsets.fromLTRB(side, side, side, bottom), padding: EdgeInsets.fromLTRB(side, side, side, bottom),
children: [ children: [
@ -1111,26 +1157,16 @@ class _VideoDetail extends StatelessWidget {
], ],
_Chip(trf('음성 {0}개', [audioCount])), _Chip(trf('음성 {0}개', [audioCount])),
]), ]),
if (moveButtons != null) ...[ // 261: 좁은 화면 (폰 세로) 은 세부 정보 칸이 낮다 (노트 10+ 에서 약 210) - "감지한 언어" 줄 · 이동 버튼이
const SizedBox(height: 10), // [AI 자막 만들기] 를 칸 밖으로 밀었다. 자막 버튼을 칩 바로 아래에 두고, 감지한 언어 · 이동 버튼은 그 다음
moveButtons, if (compact) ...[
], const SizedBox(height: 12),
// 198: 마지막 AI 자막의 원어를 자동 감지했으면 - 알림을 놓쳐도 여기서 다른 언어로 다시 만들 수 있게 subtitleActions,
if (v.aiDetected != null) ...[ if (detected != null) ...[const SizedBox(height: 4), detected],
const SizedBox(height: 8), if (moveButtons != null) ...[const SizedBox(height: 10), moveButtons],
Wrap(crossAxisAlignment: WrapCrossAlignment.center, spacing: 8, children: [ ] else ...[
Text(trf('감지한 언어: {0} (자동 감지)', [languageOf(v.aiDetected).name]), if (moveButtons != null) ...[const SizedBox(height: 10), moveButtons],
style: const TextStyle(fontSize: 12, color: JjColors.textDim)), if (detected != null) ...[const SizedBox(height: 8), detected],
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('다른 언어로 다시')),
),
]),
], ],
if (v.message != null) ...[ if (v.message != null) ...[
const SizedBox(height: 12), const SizedBox(height: 12),
@ -1144,36 +1180,10 @@ class _VideoDetail extends StatelessWidget {
style: const TextStyle(color: JjColors.danger, fontSize: 12)), style: const TextStyle(color: JjColors.danger, fontSize: 12)),
), ),
], ],
const SizedBox(height: 24), if (!compact) ...[
// 좁으면 버튼이 다음 줄로 (오른쪽 정렬) const SizedBox(height: 24),
Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ subtitleActions,
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('자막 파일 추가')),
),
]),
),
]),
const SizedBox(height: 8), const SizedBox(height: 8),
if (v.subtitles.isEmpty) if (v.subtitles.isEmpty)
Padding( Padding(

View File

@ -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');
});
}
}