From 746082db22faee89e6288aed776622a724dd414c Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Fri, 9 Oct 2026 21:21:08 +0900 Subject: [PATCH] =?UTF-8?q?147=20=EC=A2=81=EC=9D=80=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=EC=9D=98=20=EB=B2=84=ED=8A=BC=20=EC=A4=84:=20=EB=93=A4?= =?UTF-8?q?=EC=96=B4=EA=B0=80=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=EC=9D=80=20[=EB=8D=94=20=EB=B3=B4=EA=B8=B0]=20?= =?UTF-8?q?=EB=A9=94=EB=89=B4=EB=A1=9C=20=C2=B7=20=EC=B0=BD=20=EB=B0=B0?= =?UTF-8?q?=EC=B9=98=20=EB=B2=84=ED=8A=BC=EC=9D=80=20=EB=8A=98=20=EC=A4=84?= =?UTF-8?q?=EC=97=90=20=C2=B7=20=EC=B2=98=EC=9D=8C=20=EA=B0=92=EC=9D=80=20?= =?UTF-8?q?=EC=9E=90=EC=A3=BC=20=EC=93=B0=EB=8A=94=20=EA=B2=83=EB=B6=80?= =?UTF-8?q?=ED=84=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 버튼 줄은 밀어 넘기는 줄이라 아래쪽 버튼 (숨은 항목 표시 등) 을 찾지 못했다 - 들어가는 만큼 보이고 나머지는 [더 보기 ⋯] - 창 배치 (두 창으로 / 좌우 ⇆) 는 사용자 요청대로 늘 줄에 (넘치면 마지막 자리를 내줌) - 처음 값 (버튼 구성을 바꾸지 않았을 때 · [기본값]) 순서: 복사 · 이동 · 삭제 · 새 폴더 · 이름 변경 · 선택 · 찾기 · 창 배치 → 나머지. 사용자가 정한 순서는 그대로 - 시험: 낮은 화면에서 [더 보기] · 메뉴로 숨은 항목 표시 · 창 배치 버튼은 줄에 · 처음 값 순서 · 정한 순서 유지 Co-Authored-By: Claude Opus 5.5 --- lib/ui/explorer_page.dart | 80 ++++++++++++++++++++++++++++---- test/explorer_conflict_test.dart | 28 +++++++++++ test/explorer_parallel_test.dart | 29 +++++++----- test/explorer_test.dart | 3 ++ 4 files changed, 119 insertions(+), 21 deletions(-) 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, '확인'));