diff --git a/lib/ui/app_actions.dart b/lib/ui/app_actions.dart index 808cd71..48d10f3 100644 --- a/lib/ui/app_actions.dart +++ b/lib/ui/app_actions.dart @@ -25,20 +25,44 @@ bool isCompact(BuildContext context) => MediaQuery.sizeOf(context).width < compa /// 모든 화면의 위쪽 막대: [nav] [middle (남는 너비)] [actions]. /// 좁은 화면에서는 두 줄 - 위: [nav] … [actions], 아래: [middle] 이 한 줄을 다 쓴다. -class AppTopBar extends StatelessWidget { +/// [minMiddleWidth] (201): 한 줄일 때 middle 이 이보다 좁아지면 (폰 가로처럼 넓지만 버튼이 폭을 다 차지할 때) 두 줄로. +/// 브라우저 주소 칸처럼 middle 이 꼭 보여야 하는 화면이 준다 (0 이면 폭만으로 정함) +class AppTopBar extends StatefulWidget { final Widget nav; final Widget middle; final Widget actions; - const AppTopBar({super.key, required this.nav, required this.middle, required this.actions}); + final double minMiddleWidth; + const AppTopBar({super.key, required this.nav, required this.middle, required this.actions, this.minMiddleWidth = 0}); + + @override + State createState() => _AppTopBarState(); +} + +class _AppTopBarState extends State { + final _mid = GlobalKey(); + + /// 한 줄에서 middle 이 모자랐던 화면 폭 - 이 폭 이하이면 두 줄 + double? _twoRowsAtOrBelow; + + void _check(double width) { + if (!mounted || widget.minMiddleWidth <= 0) return; + final box = _mid.currentContext?.findRenderObject() as RenderBox?; + if (box == null || !box.hasSize) return; + if (box.size.width < widget.minMiddleWidth) setState(() => _twoRowsAtOrBelow = width); + } @override Widget build(BuildContext context) { - if (!isCompact(context)) { + final nav = widget.nav, middle = widget.middle, actions = widget.actions; + final width = MediaQuery.sizeOf(context).width; + final narrow = _twoRowsAtOrBelow != null && width <= _twoRowsAtOrBelow!; + if (!isCompact(context) && !narrow) { + if (widget.minMiddleWidth > 0) WidgetsBinding.instance.addPostFrameCallback((_) => _check(width)); return Container( height: appBarHeight, color: JjColors.panel, padding: const EdgeInsets.only(left: 8, right: appBarRightPadding), - child: Row(children: [nav, const SizedBox(width: 8), Expanded(child: middle), actions]), + child: Row(children: [nav, const SizedBox(width: 8), Expanded(child: KeyedSubtree(key: _mid, child: middle)), actions]), ); } return Container( diff --git a/lib/ui/browser_page.dart b/lib/ui/browser_page.dart index 40c6cbc..f3c5232 100644 --- a/lib/ui/browser_page.dart +++ b/lib/ui/browser_page.dart @@ -591,7 +591,9 @@ class _BrowserPageState extends State with RouteAware { ); // 주소 칸이 좁아지면 (폰 세로 · 태블릿 세로 · 폰 가로 100%) 자주 안 쓰는 버튼은 ⋮ 메뉴로 - 주소가 "http" 만 보이거나 // 아예 사라지던 것. 버튼을 없애지는 않는다. + // 201: 폰 가로처럼 넓어도 버튼이 폭을 다 차지하면 주소 칸이 사라졌다 - 그때는 두 줄 (아래 줄을 주소 칸이 다 씀) return AppTopBar( + minMiddleWidth: 380, nav: const AppNavButtons(onBrowserPage: true), actions: AppActions(c: widget.c), middle: LayoutBuilder(builder: (context, box) { diff --git a/lib/ui/downloads_page.dart b/lib/ui/downloads_page.dart index ca26f60..8667423 100644 --- a/lib/ui/downloads_page.dart +++ b/lib/ui/downloads_page.dart @@ -88,7 +88,9 @@ class _DownloadsPageState extends State { body: SwipeNav( current: 'downloads', child: Column(children: [ + // 200: 폰 가로에서 버튼이 폭을 다 차지해 주소 칸이 눌려 주소가 보이지 않았다 - 모자라면 두 줄 (아래 줄을 주소 칸이 다 씀) AppTopBar( + minMiddleWidth: 420, nav: const AppNavButtons(onDownloadsPage: true), actions: const AppActions(), middle: Row(children: [ diff --git a/test/app_top_bar_test.dart b/test/app_top_bar_test.dart new file mode 100644 index 0000000..89a15c4 --- /dev/null +++ b/test/app_top_bar_test.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:jj_mkvmaker/ui/app_actions.dart'; + +/// 201: 넓은 화면 (폰 가로) 이어도 버튼이 폭을 다 차지해 가운데 (브라우저 주소 칸) 가 모자라면 두 줄로 +void main() { + Widget bar({double minMiddle = 0}) => MaterialApp( + home: Scaffold( + body: Column(children: [ + AppTopBar( + minMiddleWidth: minMiddle, + nav: const SizedBox(width: 420, height: 40), + actions: const SizedBox(width: 360, height: 40), + middle: const SizedBox(key: ValueKey('mid'), height: 36, child: Text('주소')), + ), + ]), + ), + ); + + testWidgets('201: 가운데가 최소 폭보다 좁아지면 두 줄 (아래 줄을 가운데가 다 씀)', (tester) async { + tester.view.physicalSize = const Size(900, 400); // 폰 가로 (compactWidth 720 보다 넓음) + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + await tester.pumpWidget(bar(minMiddle: 380)); + await tester.pump(); + await tester.pump(); + expect(tester.getSize(find.byKey(const ValueKey('mid'))).width, greaterThan(800)); + // 넓은 화면에서는 한 줄 그대로 + tester.view.physicalSize = const Size(1600, 900); + await tester.pump(); + await tester.pump(); + expect(tester.getSize(find.byKey(const ValueKey('mid'))).width, lessThan(900)); + expect(tester.getSize(find.byKey(const ValueKey('mid'))).width, greaterThanOrEqualTo(380)); + }); + + testWidgets('201: 최소 폭을 주지 않은 화면은 예전처럼 (폭만으로 정함)', (tester) async { + tester.view.physicalSize = const Size(900, 400); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + await tester.pumpWidget(bar()); + await tester.pump(); + await tester.pump(); + expect(tester.getSize(find.byKey(const ValueKey('mid'))).width, lessThan(380)); + }); +}