diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index bef2de4..8cc4406 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -134,13 +134,20 @@ enum ExplorerButton { /// Rsync 화면의 버튼 (늘 이대로, 두 창 사이) static const rsyncButtons = [up, refresh, newFolder, toRight, toLeft, both, monitor]; + /// 147 (관리자): 처음 값 (버튼 구성을 바꾸지 않았을 때) 은 자주 쓰는 것부터 - 좁은 화면에서 [더 보기] 로 가는 것은 뒤쪽 + static List get defaultOrder { + const first = [copy, move, delete, newFolder, rename, select, search, orient]; + return [...first, for (final b in explorerValues) if (!first.contains(b)) b]; + } + static List fromSettings(List names) { final out = [ for (final n in names) for (final b in explorerValues) if (b.name == n) b, ]; - return out.isEmpty ? explorerValues : out; + // 사용자가 정한 순서는 그대로 (비어 있을 때만 처음 값) + return out.isEmpty ? defaultOrder : out; } } @@ -2423,7 +2430,7 @@ class _ExplorerPageState extends State { onPressed: () => set(() { order ..clear() - ..addAll(ExplorerButton.explorerValues); + ..addAll(ExplorerButton.defaultOrder); on ..clear() ..addAll(ExplorerButton.explorerValues); @@ -3083,17 +3090,72 @@ class _Toolbar extends StatelessWidget { ); } + // 147: 다 들어가지 않으면 (좁은 · 낮은 화면) 들어가는 만큼 보이고 나머지는 [더 보기 ⋯] 메뉴로 - 밀어야 보이는 것을 몰라 못 찾던 것 + Widget more(List rest) => Builder( + builder: (context) => Tooltip( + message: tr('더 보기'), + child: InkWell( + borderRadius: BorderRadius.circular(6), + onTap: () async { + final box = context.findRenderObject()! as RenderBox; + final at = box.localToGlobal(Offset.zero); + final pick = await showMenu( + context: context, + position: RelativeRect.fromLTRB(at.dx + box.size.width, at.dy, at.dx + box.size.width, at.dy), + items: [ + for (final b in rest) + PopupMenuItem( + value: b, + enabled: enabled(b), + height: 40, + child: Row(children: [ + Icon(_icon(b), size: 18, color: selected(b) ? JjColors.accent : null), + const SizedBox(width: 10), + Text(tr(_label(b))), + ]), + ), + ], + ); + if (pick != null) onPressed(pick); + }, + child: SizedBox( + width: 72, + height: 58, + child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + const Icon(Icons.more_horiz, size: 22), + const SizedBox(height: 2), + Text(tr('더 보기'), style: const TextStyle(fontSize: 10.5, height: 1.1)), + ]), + ), + ), + ), + ); + return Container( color: JjColors.panel, width: vertical ? 76 : null, height: vertical ? null : 62, - child: SingleChildScrollView( - scrollDirection: vertical ? Axis.vertical : Axis.horizontal, - padding: const EdgeInsets.all(2), - child: vertical - ? Column(children: [for (final b in buttons) btn(b)]) - : Row(children: [for (final b in buttons) btn(b)]), - ), + padding: const EdgeInsets.all(2), + child: LayoutBuilder(builder: (context, box) { + final room = vertical ? box.maxHeight : box.maxWidth; + final each = vertical ? 58.0 : 72.0; + var shown = buttons; + var rest = const []; + if (room.isFinite && buttons.length * each > room) { + final fit = (room / each).floor() - 1; // 하나는 [더 보기] 자리 + if (fit >= 1) { + shown = buttons.take(fit).toList(); + rest = buttons.skip(fit).toList(); + // 창 배치 버튼은 늘 줄에 (사용자가 가운데 아이콘 모음에 두라고 함, 10/9) - 넘치면 마지막 자리를 내준다 + if (rest.contains(ExplorerButton.orient)) { + rest = [shown.removeLast(), ...rest.where((b) => b != ExplorerButton.orient)]; + shown = [...shown, ExplorerButton.orient]; + } + } + } + final children = [for (final b in shown) btn(b), if (rest.isNotEmpty) more(rest)]; + return vertical ? Column(children: children) : Row(children: children); + }), ); } } diff --git a/test/explorer_conflict_test.dart b/test/explorer_conflict_test.dart index 7616cf5..c7f3db1 100644 --- a/test/explorer_conflict_test.dart +++ b/test/explorer_conflict_test.dart @@ -323,6 +323,34 @@ void main() { expect(find.textContaining('이미 이 폴더에 있어 건너뜀: b.txt'), findsOneWidget, reason: '끝난 알림에도'); }); + testWidgets('147: 버튼 줄이 다 들어가지 않으면 (낮은 화면) 나머지는 [더 보기] 메뉴로 - "숨은 항목 표시" 도 누를 수 있다', (tester) async { + tester.view.physicalSize = const Size(1200, 560); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + final before = c.settings.explorerShowHidden; + await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c)))); + await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty); + expect(tester.takeException(), isNull, reason: '넘침 없음'); + expect(find.text('더 보기'), findsOneWidget); + expect(find.text('숨은 항목 표시'), findsNothing, reason: '들어가지 않는 버튼은 줄에 없음'); + await tester.tap(find.text('더 보기')); + await tester.pumpAndSettle(); + expect(find.text('숨은 항목 표시'), findsOneWidget); + await tester.runAsync(() => tester.tap(find.text('숨은 항목 표시'))); + await settle(tester, () => c.settings.explorerShowHidden != before, rounds: 20); + expect(c.settings.explorerShowHidden, !before); + // 창 배치 버튼은 좁아도 늘 줄에 (사용자 요청) · 처음 값은 자주 쓰는 것부터 (복사 · 이동 · 삭제 …) + expect(find.byTooltip('창 배치: 좌우 (누르면 위아래)'), findsOneWidget); + expect(ExplorerButton.fromSettings(const []).take(8).map((b) => b.name), + ['copy', 'move', 'delete', 'newFolder', 'rename', 'select', 'search', 'orient']); + expect(ExplorerButton.fromSettings(const ['hidden', 'copy']), [ExplorerButton.hidden, ExplorerButton.copy], + reason: '사용자가 정한 순서는 그대로'); + // 넉넉한 화면에서는 [더 보기] 없이 모두 + tester.view.physicalSize = const Size(1600, 1400); + await settle(tester, () => find.text('더 보기').evaluate().isEmpty, rounds: 20); // 숨은 항목을 바꿔 다시 읽는 중일 수 있어 정해진 만큼만 + expect(find.text('더 보기'), findsNothing); + }); + testWidgets('146 · 37-2: 없는 경로를 넣으면 창을 닫지 않고 칸 아래에 "폴더가 없거나 열 수 없습니다" · 고친 경로로 간다', (tester) async { tester.view.physicalSize = const Size(1600, 900); tester.view.devicePixelRatio = 1; diff --git a/test/explorer_parallel_test.dart b/test/explorer_parallel_test.dart index 62de632..8f07a74 100644 --- a/test/explorer_parallel_test.dart +++ b/test/explorer_parallel_test.dart @@ -214,24 +214,29 @@ void main() { addTearDown(tester.view.reset); await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c)))); await settle(tester, () => find.text('a.txt').evaluate().isNotEmpty); - await act(tester, () => tester.tap(find.byTooltip('두 창으로 보기 (좌우)'))); - await settle(tester, () => false, rounds: 5); - expect((c.settings.explorerLayout, c.settings.explorerOrientation), ('dual', 'side')); - expect(find.byTooltip('창 배치: 좌우 (누르면 위아래)'), findsOneWidget, reason: '폰 세로에서도 좌우'); + // 그 버튼을 누른다: 버튼 줄에 있으면 바로, 다 들어가지 않아 [더 보기] 메뉴에 있으면 메뉴에서 (147) + Future press(String tooltip, String label) async { + if (find.byTooltip(tooltip).evaluate().isNotEmpty) { + await act(tester, () => tester.tap(find.byTooltip(tooltip))); + } else { + await tester.tap(find.text('더 보기')); + await tester.pumpAndSettle(); + expect(find.text(label), findsOneWidget, reason: '[더 보기] 메뉴에 지금 배치가 보임: $label'); + await act(tester, () => tester.tap(find.text(label))); + } + await settle(tester, () => false, rounds: 5); + } + + await press('두 창으로 보기 (좌우)', '두 창으로'); + expect((c.settings.explorerLayout, c.settings.explorerOrientation), ('dual', 'side'), reason: '폰 세로에서도 좌우'); expect(find.text('a.txt'), findsNWidgets(2)); final left = tester.getCenter(find.text('a.txt').first), right = tester.getCenter(find.text('a.txt').last); expect(right.dx, greaterThan(left.dx)); - await act(tester, () => tester.tap(find.byTooltip('창 배치: 좌우 (누르면 위아래)'))); - await settle(tester, () => false, rounds: 5); + await press('창 배치: 좌우 (누르면 위아래)', '좌우 ⇆'); expect(c.settings.explorerOrientation, 'stacked'); - expect(find.byTooltip('창 배치: 위아래 (누르면 좌우)'), findsOneWidget); final top = tester.getCenter(find.text('a.txt').first), bottom = tester.getCenter(find.text('a.txt').last); expect(bottom.dy, greaterThan(top.dy)); - // 위아래면 버튼 줄이 가로로 밀어 보는 줄 - 보이게 한 뒤 누른다 - await tester.ensureVisible(find.byTooltip('창 배치: 위아래 (누르면 좌우)')); - await tester.pump(); - await act(tester, () => tester.tap(find.byTooltip('창 배치: 위아래 (누르면 좌우)'))); - await settle(tester, () => false, rounds: 5); + await press('창 배치: 위아래 (누르면 좌우)', '위아래 ⇅'); expect(c.settings.explorerOrientation, 'side'); }); } diff --git a/test/explorer_test.dart b/test/explorer_test.dart index fe3e7d8..bc2d375 100644 --- a/test/explorer_test.dart +++ b/test/explorer_test.dart @@ -298,6 +298,9 @@ void main() { await tester.pumpAndSettle(); await tester.tap(find.text('버튼 구성').last); await tester.pumpAndSettle(); + // 처음 값 순서 (자주 쓰는 것부터) 에서 뒤쪽이라 목록을 내려 보이게 한 뒤 + await tester.scrollUntilVisible(find.widgetWithText(CheckboxListTile, 'MKV 목록에 추가'), 80, + scrollable: find.descendant(of: find.byType(AlertDialog), matching: find.byType(Scrollable)).first); await tester.tap(find.widgetWithText(CheckboxListTile, 'MKV 목록에 추가')); await tester.pump(); await tester.tap(find.widgetWithText(FilledButton, '확인'));