import 'dart:math' as math; import 'package:flutter/material.dart'; import 'explorer_bars.dart'; import 'explorer_buttons.dart'; import 'explorer_controller.dart'; import 'explorer_flows.dart'; import 'explorer_layout.dart'; import 'explorer_select.dart'; import 'pane_view.dart'; import 'toolbar.dart'; /// 약속 (E5d-6): 창들과 기능 버튼 줄의 배치 - 설정 (두 창 · 폴더 + 파일 목록 · 한 창, 좌우 · 위아래, 버튼 줄 사이 · 끝 · 숨김) 과 /// 폭 (좁으면 잠깐 한 창 · 좁은 트리 · 좁은 Rsync 는 창 하나씩) 을 [ExplorerLayout] 에 적고 그린다. 창은 배치가 바뀌어도 같은 위젯 /// (스크롤이 맨 위로 가지 않게). 모양이 바뀌면 보던 폴더가 다시 보이게 한다 class ExplorerPanesBody extends StatelessWidget { const ExplorerPanesBody( {super.key, required this.ctl, required this.sel, required this.layout, required this.parts, required this.buttons, required this.flows}); final ExplorerController ctl; final ExplorerSelect sel; final ExplorerLayout layout; final ExplorerPaneParts parts; final ExplorerButtons buttons; final ExplorerFlows flows; @override Widget build(BuildContext context) { final s = ctl.c.settings; final shape = '${s.explorerStyle}|${s.explorerLayout}|${s.explorerOrientation}|${s.explorerToolbar}'; if (ctl.ready && shape != layout.shape) { if (layout.shape.isNotEmpty) { for (final pane in ctl.panes) { layout.reveal(pane, pane.current); } } layout.shape = shape; } final dual = layout.dual; return LayoutBuilder(builder: (context, box) { // 두 창이 다 들어가는지: 트리 (화살표 · 아이콘 · 여백 78 + 이름 40) + 목록 (아이콘 · ✔ · 여백 104 + 이름 50), 이름은 글자 배율만큼. // 1배면 360 폭에도 들어가고, 기기 글자 1.3 × 150% (약 2배) 면 412 폭에도 모자란다 final wantSplit = !ctl.rsync && s.explorerLayout == 'split'; final textScale = MediaQuery.textScalerOf(context).scale(14) / 14 * layout.listTextScale; final barW = s.explorerToolbar == 'hidden' ? 0.0 : 72.0; final sideNow = switch (s.explorerOrientation) { 'side' => true, 'stacked' => false, _ => box.maxWidth >= box.maxHeight }; final tightWas = layout.splitTight; layout.splitTight = wantSplit && sideNow && box.maxWidth - barW < 182 + 90 * textScale; // 바뀌면 이 builder 밖 (버튼 이름 "두 창" 등) 도 한 번 다시 그린다 if (tightWas != layout.splitTight) { ctl.host.afterFrame(() { if (ctl.host.alive) ctl.update(); }); } final split = layout.split; // 폭이 바뀌면 (좁은 한 창 ↔ 두 창) 이 builder 만 다시 불린다 - 그때도 새로 붙은 창을 보이게 if (ctl.ready) layout.revealNewViews(); final side = !(dual || split) || switch (s.explorerOrientation) { 'side' => true, 'stacked' => false, _ => box.maxWidth >= box.maxHeight, }; // 191: Rsync 화면이 좁으면 (폴드 접은 화면 · 폰 세로의 좌우 두 창) 창 하나씩 - [왼쪽 | 오른쪽] 으로 바꿔 본다 // (두 창 + 가운데 버튼 줄이면 창이 140px 남짓이라 폴더 이름이 잘렸다) final rsyncOne = ctl.rsync && side && box.maxWidth < rsyncTwoPaneMinWidth; layout.narrowSplit = split && side && box.maxWidth < ExplorerLayout.narrowSplitWidth; final bar = s.explorerToolbar == 'hidden' && !ctl.rsync ? null : ExplorerToolbar( // X-plore b: 길게 누르기 = 짝 동작 (Rsync 화면은 없음) onLongPress: ctl.rsync ? null : buttons.longPress, // Rsync 화면: 늘 같은 버튼 (→ ← ⇄ 모니터링) 을 두 창 사이에 buttons: ctl.rsync ? ExplorerButton.rsyncButtons : ExplorerButton.fromSettings(s.explorerButtons), vertical: side && !rsyncOne, panesSide: side, onPressed: buttons.press, enabled: (b) => switch (b) { ExplorerButton.paste => flows.transfers.clip.isNotEmpty, _ => true, }, selected: (b) => (b == ExplorerButton.hidden && s.explorerShowHidden) || (b == ExplorerButton.select && sel.selecting), twoPanes: dual || split, pinned: {ExplorerButton.orient, if (flows.transfers.clip.isNotEmpty) ExplorerButton.paste}, ); final middle = bar != null && (ctl.rsync || s.explorerToolbar == 'middle'); final edge = bar != null && !ctl.rsync && s.explorerToolbar == 'edge'; // 180: 창마다 같은 열쇠 - 가로 ↔ 세로 배치가 바뀌어도 (창 크기 · 작업 현황이 열림) 창을 새로 만들지 않는다 (스크롤이 맨 위로 가지 않게) Widget keep(int k, Widget w) => KeyedSubtree(key: layout.viewKeys[k], child: w); if (rsyncOne) { final i = ctl.active.clamp(0, 1); return Column(children: [ RsyncPaneSwitch(ctl: ctl), ?bar, Expanded(child: keep(i, ExplorerPaneView(ctl: ctl, sel: sel, parts: parts, index: i, foldersOnly: true))), ]); } final children = [ if (dual) ...[ Expanded(child: keep(0, ExplorerPaneView(ctl: ctl, sel: sel, parts: parts, index: 0, foldersOnly: ctl.rsync))), if (middle) bar, Expanded(child: keep(1, ExplorerPaneView(ctl: ctl, sel: sel, parts: parts, index: 1, foldersOnly: ctl.rsync))), if (edge) bar, ] else if (split) ...[ // 왼쪽 폴더 트리 (좁게) · 오른쪽 그 폴더의 파일 목록. 좁은 화면은 트리에 최소 폭 (130px 또는 버튼 줄 (72) 을 뺀 폭의 42%) - // 버튼 줄까지 넣어 38% 로 하면 360 폭에서 오른쪽 목록의 파일 이름이 두세 글자만 남았다 if (layout.narrowSplit) SizedBox( width: math.max(130.0, (box.maxWidth - (middle || edge ? 72 : 0)) * 0.42), child: keep(0, ExplorerPaneView(ctl: ctl, sel: sel, parts: parts, index: 0, foldersOnly: true))) else Expanded(flex: 2, child: keep(0, ExplorerPaneView(ctl: ctl, sel: sel, parts: parts, index: 0, foldersOnly: true))), if (middle) bar, Expanded(flex: 3, child: keep(2, ExplorerListView(ctl: ctl, sel: sel, parts: parts, pane: ctl.panes[0]))), if (edge) bar, ] else ...[ if (middle) bar, Expanded( child: layout.hiddenTarget ? Column(children: [ListenableBuilder(listenable: ctl.panes[1], builder: (_, _) => CopyTargetBar(ctl: ctl)), Expanded(child: keep(0, ExplorerPaneView(ctl: ctl, sel: sel, parts: parts, index: 0)))]) : keep(0, ExplorerPaneView(ctl: ctl, sel: sel, parts: parts, index: 0))), if (edge) bar, ], ]; return side ? Row(children: children) : Column(children: children); }); } }