147 좁은 화면의 버튼 줄: 들어가지 않는 버튼은 [더 보기] 메뉴로 · 창 배치 버튼은 늘 줄에 · 처음 값은 자주 쓰는 것부터

- 버튼 줄은 밀어 넘기는 줄이라 아래쪽 버튼 (숨은 항목 표시 등) 을 찾지 못했다 - 들어가는 만큼 보이고 나머지는 [더 보기 ⋯]
- 창 배치 (두 창으로 / 좌우 ⇆) 는 사용자 요청대로 늘 줄에 (넘치면 마지막 자리를 내줌)
- 처음 값 (버튼 구성을 바꾸지 않았을 때 · [기본값]) 순서: 복사 · 이동 · 삭제 · 새 폴더 · 이름 변경 · 선택 · 찾기 · 창 배치 → 나머지.
  사용자가 정한 순서는 그대로
- 시험: 낮은 화면에서 [더 보기] · 메뉴로 숨은 항목 표시 · 창 배치 버튼은 줄에 · 처음 값 순서 · 정한 순서 유지

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
묶음3
RyuJongJin 2026-10-09 21:21:08 +09:00
parent 6a5aea5ff2
commit 746082db22
4 changed files with 119 additions and 21 deletions

View File

@ -134,13 +134,20 @@ enum ExplorerButton {
/// Rsync 화면의 버튼 (늘 이대로, 두 창 사이) /// Rsync 화면의 버튼 (늘 이대로, 두 창 사이)
static const rsyncButtons = [up, refresh, newFolder, toRight, toLeft, both, monitor]; static const rsyncButtons = [up, refresh, newFolder, toRight, toLeft, both, monitor];
/// 147 (관리자): 처음 값 (버튼 구성을 바꾸지 않았을 때) 은 자주 쓰는 것부터 - 좁은 화면에서 [더 보기] 로 가는 것은 뒤쪽
static List<ExplorerButton> 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<ExplorerButton> fromSettings(List<String> names) { static List<ExplorerButton> fromSettings(List<String> names) {
final out = [ final out = [
for (final n in names) for (final n in names)
for (final b in explorerValues) for (final b in explorerValues)
if (b.name == n) b, if (b.name == n) b,
]; ];
return out.isEmpty ? explorerValues : out; // 사용자가 정한 순서는 그대로 (비어 있을 때만 처음 값)
return out.isEmpty ? defaultOrder : out;
} }
} }
@ -2423,7 +2430,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
onPressed: () => set(() { onPressed: () => set(() {
order order
..clear() ..clear()
..addAll(ExplorerButton.explorerValues); ..addAll(ExplorerButton.defaultOrder);
on on
..clear() ..clear()
..addAll(ExplorerButton.explorerValues); ..addAll(ExplorerButton.explorerValues);
@ -3083,17 +3090,72 @@ class _Toolbar extends StatelessWidget {
); );
} }
// 147: 다 들어가지 않으면 (좁은 · 낮은 화면) 들어가는 만큼 보이고 나머지는 [더 보기 ⋯] 메뉴로 - 밀어야 보이는 것을 몰라 못 찾던 것
Widget more(List<ExplorerButton> 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<ExplorerButton>(
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( return Container(
color: JjColors.panel, color: JjColors.panel,
width: vertical ? 76 : null, width: vertical ? 76 : null,
height: vertical ? null : 62, height: vertical ? null : 62,
child: SingleChildScrollView(
scrollDirection: vertical ? Axis.vertical : Axis.horizontal,
padding: const EdgeInsets.all(2), padding: const EdgeInsets.all(2),
child: vertical child: LayoutBuilder(builder: (context, box) {
? Column(children: [for (final b in buttons) btn(b)]) final room = vertical ? box.maxHeight : box.maxWidth;
: Row(children: [for (final b in buttons) btn(b)]), final each = vertical ? 58.0 : 72.0;
), var shown = buttons;
var rest = const <ExplorerButton>[];
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);
}),
); );
} }
} }

View File

@ -323,6 +323,34 @@ void main() {
expect(find.textContaining('이미 이 폴더에 있어 건너뜀: b.txt'), findsOneWidget, reason: '끝난 알림에도'); 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 { testWidgets('146 · 37-2: 없는 경로를 넣으면 창을 닫지 않고 칸 아래에 "폴더가 없거나 열 수 없습니다" · 고친 경로로 간다', (tester) async {
tester.view.physicalSize = const Size(1600, 900); tester.view.physicalSize = const Size(1600, 900);
tester.view.devicePixelRatio = 1; tester.view.devicePixelRatio = 1;

View File

@ -214,24 +214,29 @@ void main() {
addTearDown(tester.view.reset); addTearDown(tester.view.reset);
await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c)))); await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c))));
await settle(tester, () => find.text('a.txt').evaluate().isNotEmpty); await settle(tester, () => find.text('a.txt').evaluate().isNotEmpty);
await act(tester, () => tester.tap(find.byTooltip('두 창으로 보기 (좌우)'))); // 그 버튼을 누른다: 버튼 줄에 있으면 바로, 다 들어가지 않아 [더 보기] 메뉴에 있으면 메뉴에서 (147)
Future<void> 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 settle(tester, () => false, rounds: 5);
expect((c.settings.explorerLayout, c.settings.explorerOrientation), ('dual', 'side')); }
expect(find.byTooltip('창 배치: 좌우 (누르면 위아래)'), findsOneWidget, reason: '폰 세로에서도 좌우');
await press('두 창으로 보기 (좌우)', '두 창으로');
expect((c.settings.explorerLayout, c.settings.explorerOrientation), ('dual', 'side'), reason: '폰 세로에서도 좌우');
expect(find.text('a.txt'), findsNWidgets(2)); expect(find.text('a.txt'), findsNWidgets(2));
final left = tester.getCenter(find.text('a.txt').first), right = tester.getCenter(find.text('a.txt').last); final left = tester.getCenter(find.text('a.txt').first), right = tester.getCenter(find.text('a.txt').last);
expect(right.dx, greaterThan(left.dx)); expect(right.dx, greaterThan(left.dx));
await act(tester, () => tester.tap(find.byTooltip('창 배치: 좌우 (누르면 위아래)'))); await press('창 배치: 좌우 (누르면 위아래)', '좌우 ⇆');
await settle(tester, () => false, rounds: 5);
expect(c.settings.explorerOrientation, 'stacked'); 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); final top = tester.getCenter(find.text('a.txt').first), bottom = tester.getCenter(find.text('a.txt').last);
expect(bottom.dy, greaterThan(top.dy)); expect(bottom.dy, greaterThan(top.dy));
// 위아래면 버튼 줄이 가로로 밀어 보는 줄 - 보이게 한 뒤 누른다 await press('창 배치: 위아래 (누르면 좌우)', '위아래 ⇅');
await tester.ensureVisible(find.byTooltip('창 배치: 위아래 (누르면 좌우)'));
await tester.pump();
await act(tester, () => tester.tap(find.byTooltip('창 배치: 위아래 (누르면 좌우)')));
await settle(tester, () => false, rounds: 5);
expect(c.settings.explorerOrientation, 'side'); expect(c.settings.explorerOrientation, 'side');
}); });
} }

View File

@ -298,6 +298,9 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.tap(find.text('버튼 구성').last); await tester.tap(find.text('버튼 구성').last);
await tester.pumpAndSettle(); 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.tap(find.widgetWithText(CheckboxListTile, 'MKV 목록에 추가'));
await tester.pump(); await tester.pump();
await tester.tap(find.widgetWithText(FilledButton, '확인')); await tester.tap(find.widgetWithText(FilledButton, '확인'));