diff --git a/lib/ui/home_page.dart b/lib/ui/home_page.dart index ec9fb64..f19296d 100644 --- a/lib/ui/home_page.dart +++ b/lib/ui/home_page.dart @@ -416,7 +416,8 @@ class _TopBar extends StatelessWidget { return AppTopBar( // 242: 작업 중 (진행 표시 · 취소) 이 생기거나 없어지면 다시 잰다 - key: ValueKey(('mkvTopBar', c.busy)), + // (여러 개를 만드는지에 따라 진행 표시 폭이 달라 그때도 다시 잰다) + key: ValueKey(('mkvTopBar', c.busy, c.batchTotal > 1)), // 242: 오른쪽 묶음 때문에 왼쪽 (동영상 추가 · AI) 이 아이콘 하나보다 좁아지면 두 줄 minMiddleWidth: 56, // 모든 화면 공통: [JJ 홈] [MKV 화면] [뒤로] [다운로드 목록] diff --git a/lib/ui/work_panel.dart b/lib/ui/work_panel.dart index 52af2f0..83946e9 100644 --- a/lib/ui/work_panel.dart +++ b/lib/ui/work_panel.dart @@ -419,10 +419,24 @@ class JobIndicator extends StatelessWidget { value: p == null || p == 0 ? null : p, ), ), - // 좁아서 아이콘만일 때도 진행률은 보이게 - if (iconOnly && p != null) ...[ + // 좁아서 아이콘만일 때도 진행률은 보이게. 여러 개면 짧은 꼴 "2/3 · 37%" (폰 세로에서 "MKV 3개 중 2번째" 가 잘렸다). + // 폭은 늘 같게 잡아 둔다 - 위 막대는 작업이 시작될 때 재는데, 그때는 진행률이 없어 글이 없다가 나중에 생기면 + // 잰 폭보다 넓어져 왼쪽의 [+ 동영상 추가] 를 가렸다 (a4 934, 폰 세로) + if (iconOnly) ...[ const SizedBox(width: 4), - Text('${(p * 100).round()}%', style: const TextStyle(fontSize: 12, color: JjColors.accent)), + SizedBox( + width: c.batchTotal > 1 ? 72 : 36, + child: Text( + [ + if (c.batchTotal > 1) '${c.batchStarted}/${c.batchTotal}', + if (p != null) '${(p * 100).round()}%', + ].join(' · '), + maxLines: 1, + softWrap: false, + overflow: TextOverflow.fade, + style: const TextStyle(fontSize: 12, color: JjColors.accent), + ), + ), ], if (!iconOnly) const SizedBox(width: 6), if (!iconOnly) diff --git a/test/batch_test.dart b/test/batch_test.dart index f2a7537..04eea85 100644 --- a/test/batch_test.dart +++ b/test/batch_test.dart @@ -755,4 +755,52 @@ void main() { await tester.runAsync(() => building); await tester.pump(); }); + + testWidgets('폰 세로 (노트 10+ 약 411×800): 여러 개를 만드는 동안 위 막대 진행은 "n/3 · 37%" 짧은 꼴 · [+ 동영상 추가] 를 가리지 않는다', (tester) async { + tester.view.physicalSize = const Size(411, 800); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + final tool = _HoldTool(); + final dir = Directory.systemTemp.createTempSync('jj_batch_portrait_'); + addTearDown(() => dir.deleteSync(recursive: true)); + final files = [for (final n in ['a', 'b', 'c']) (File(p.join(dir.path, '$n.mp4'))..writeAsBytesSync([0])).path]; + final c = AppController(PlatformServices(mediaTool: tool, storage: DesktopStorageService())); + late Future building; + await tester.runAsync(() async { + await c.init(); + await c.addVideos(files); + c.selected = c.videos.first; + building = c.buildVideos([...c.videos]); + for (var i = 0; i < 200 && tool.runs.isEmpty; i++) { + await Future.delayed(const Duration(milliseconds: 10)); + } + }); + expect(c.busy, isTrue); + expect(c.batchTotal, 3); + await tester.pumpWidget(MaterialApp(home: HomePage(c: c))); + await tester.pump(); + await tester.pump(); + // 위 막대를 잰 뒤에 진행률이 생긴다 (예전에는 이때 글이 넓어져 [+] 를 가렸다) + for (final v in c.videos.where((v) => v.status == JobStatus.running)) { + v.progress = 0.37; + } + await c.updateSettings((_) {}); + await tester.pump(); + expect(tester.takeException(), isNull); + final prog = find.textContaining(RegExp(r'^\d+/3 · 37%$')); + expect(prog, findsOneWidget); + final add = find.byTooltip(Platform.isAndroid ? '동영상 추가' : '동영상 추가 (탐색기에서 끌어다 놓아도 됩니다)'); + expect(add, findsWidgets); + final r = tester.getRect(prog); + // [+] 는 옆으로 미는 줄 안에 있어 끝이 잘려 보일 수 있다 - 실제로 보이는 부분 (밀기 줄 안쪽) 이 진행 글과 겹치지 않아야 한다 + for (final e in add.evaluate()) { + final a = tester.getRect(find.byWidget(e.widget).first); + final view = tester.getRect(find.ancestor(of: find.byWidget(e.widget).first, matching: find.byType(Scrollable)).first); + final seen = a.intersect(view); + expect(seen.width > 0 && seen.overlaps(r), isFalse, reason: '[+] 의 보이는 부분 $seen 이 진행 글 $r 와 겹침'); + } + c.cancel(); + await tester.runAsync(() => building); + await tester.pump(); + }); }