import 'dart:math' as math; import 'package:flutter/material.dart'; import '../../core/file_ops.dart' show samePath; import '../../core/vfs.dart'; import '../../l10n/tr.dart'; import '../app_actions.dart'; import '../folder_picker.dart' show pickFolderOrDav; import '../path_label.dart'; import '../tail_text.dart'; import '../theme.dart'; import 'explorer_buttons.dart'; import 'explorer_controller.dart'; import 'explorer_pane.dart'; // 약속 (E5d-6): 탐색기 위의 막대들 - 화면 위 막대 ([ExplorerTopBar]: 제목 · 경로 막대 (누르면 위 폴더들) · ⋮ 배치 메뉴), // 한 창일 때 복사 · 이동 갈 곳 줄 ([CopyTargetBar]), 좁은 Rsync 화면의 [왼쪽 | 오른쪽] ([RsyncPaneSwitch]). 누르는 일은 버튼 · 창으로 /// 화면 위 막대. X-plore 경로 막대 (4.4x): 경로를 누르면 위 폴더들 (창의 맨 위까지) 이 나와 골라 간다 class ExplorerTopBar extends StatelessWidget { const ExplorerTopBar({super.key, required this.ctl, required this.buttons}); final ExplorerController ctl; final ExplorerButtons buttons; /// 지금 폴더 · 그 위 폴더들 (창의 맨 위 [pane.root] 까지, 가까운 것부터) static List ancestorsOf(ExplorerPane pane) { final out = []; var d = pane.current; for (var i = 0; i < 64; i++) { out.add(d); if (samePath(d, pane.root)) break; final up = vDirname(d); if (up == d || up.isEmpty) break; d = up; } return out; } @override Widget build(BuildContext context) => AppTopBar( nav: AppNavButtons(onExplorerPage: !ctl.rsync, onRsyncPage: ctl.rsync), actions: AppActions(c: ctl.c), // 아주 좁고 글자가 크면 (300 폭 · 1.3 × 150%) 제목도 줄임 - 넘쳐 그리기 오류가 나던 것 (날짜 열 312 조합 시험에서 찾음). // 보통은 제목 폭 그대로 (경로 막대에 80 남짓 남을 때까지) middle: LayoutBuilder(builder: (context, box) => Row(children: [ ConstrainedBox( constraints: BoxConstraints(maxWidth: math.max(40, box.maxWidth - 92)), child: Text(ctl.rsync ? 'Rsync' : tr('파일 탐색기'), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), ), const SizedBox(width: 12), // X-plore 경로 막대 (4.4x): 경로를 누르면 위 폴더들 (창의 맨 위까지) 이 나와 골라 간다 Expanded( child: !ctl.ready ? const SizedBox.shrink() : PopupMenuButton( tooltip: tr('위 폴더로 가기'), position: PopupMenuPosition.under, onSelected: (d) => ctl.goTo(ctl.pane, d), itemBuilder: (_) => [ for (final d in ancestorsOf(ctl.pane)) PopupMenuItem( value: d, enabled: !samePath(d, ctl.pane.current), child: Text(ctl.displayPath(d), maxLines: 1, overflow: TextOverflow.ellipsis), ), ], // 관리자 지시 (10/21, 28 지적): 좁으면 앞을 "…" 로 - 지금 폴더 (끝) 가 늘 보이게 child: TailText(ctl.displayPath(ctl.pane.current), style: TextStyle(color: JjColors.textDim)), ), ), PopupMenuButton( tooltip: tr('창 배치 · 버튼 구성'), icon: const Icon(Icons.more_vert), onSelected: (v) => v is ExplorerButton ? buttons.press(v) : ctl.toggleAppHidden(), itemBuilder: (_) => [ for (final b in [ if (!ctl.rsync) ...[ExplorerButton.layout, ExplorerButton.buttons], ExplorerButton.sort, ExplorerButton.hidden, if (!ctl.rsync) ExplorerButton.storage, if (!ctl.rsync) ExplorerButton.wifiShare, ]) PopupMenuItem( value: b, child: Row(children: [Icon(b.icon, size: 18), const SizedBox(width: 12), Text(tr(b.label))]), ), // X-plore 숨기기: 큰 폴더를 펼쳐 목록 맨 아래가 멀 때도 (관리자 결정 10/18) if (!ctl.rsync && ctl.c.settings.explorer.hiddenItems.isNotEmpty) PopupMenuItem( value: 'appHidden', child: Row(children: [ Icon(ctl.showAppHidden ? Icons.visibility_off_outlined : Icons.visibility_outlined, size: 18), const SizedBox(width: 12), Flexible( child: Text(ctl.showAppHidden ? tr('목록에서 숨긴 항목 다시 숨기기') : trf('목록에서 숨긴 항목 보이기 ({0}개)', [ctl.c.settings.explorer.hiddenItems.length])), ), ]), ), ], ), ])), ); } /// X-plore 비교 a: 한 창일 때 맨 위의 "→ 갈 곳" 줄 - 숨은 창의 폴더 · [지금 폴더로] · [다른 폴더] class CopyTargetBar extends StatelessWidget { const CopyTargetBar({super.key, required this.ctl}); final ExplorerController ctl; /// 폴더 이름 (저장 장치 맨 위면 그 이름) - 좁은 줄에 경로 대신 (전체 경로는 풍선 도움말) String _folderName(String dir) => ctl.volumes.firstWhere((v) => samePath(v.$1, dir), orElse: () => (dir, vBasename(dir).isEmpty ? ctl.displayPath(dir) : vBasename(dir))).$2; @override Widget build(BuildContext context) { final t = ctl.panes[1 - ctl.active]; return Material( color: JjColors.accent.withValues(alpha: 0.10), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), child: Row(children: [ Icon(Icons.arrow_forward, size: 18, color: JjColors.accent), const SizedBox(width: 6), // 412 캡처 (10/18): "갈 곳:" 은 늘 보이고, 폴더 이름이 길면 가운데를 줄인다 (전체 경로는 길게 눌러 · 마우스를 올려) Expanded( child: Tooltip( message: ctl.displayPath(t.current), triggerMode: TooltipTriggerMode.longPress, child: LayoutBuilder(builder: (context, box) { const style = TextStyle(fontSize: 12.5); final label = trf('갈 곳: {0}', ['']); final room = box.maxWidth - textWidth(context, label, style); return Text('$label${middleEllipsis(context, _folderName(t.current), style, room)}', maxLines: 1, overflow: TextOverflow.clip, style: style); }), ), ), TextButton( style: TextButton.styleFrom(visualDensity: VisualDensity.compact), onPressed: () => ctl.goTo(t, ctl.pane.current), child: Text(tr('지금 폴더로')), ), TextButton( style: TextButton.styleFrom(visualDensity: VisualDensity.compact), onPressed: () async { final d = await pickFolderOrDav(context, tr('복사 · 이동 갈 곳'), t.current); if (d != null && context.mounted) await ctl.goTo(t, d); }, child: Text(tr('다른 폴더')), ), ]), ), ); } } /// 191: 좁은 Rsync 화면의 [왼쪽 | 오른쪽] - 그 창에서 고른 폴더 (없으면 지금 폴더) 이름과 함께 class RsyncPaneSwitch extends StatelessWidget { const RsyncPaneSwitch({super.key, required this.ctl}); final ExplorerController ctl; @override Widget build(BuildContext context) => Padding( padding: const EdgeInsets.fromLTRB(4, 4, 4, 0), child: Row(children: [ for (final (k, side) in [(0, tr('왼쪽')), (1, tr('오른쪽'))]) Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 2), child: ChoiceChip( showCheckmark: false, selected: ctl.active == k, onSelected: (_) => ctl ..active = k ..update(), label: SizedBox( width: double.infinity, child: Text( '$side · ${vBasename(!ctl.panes[k].sel.isEmpty ? ctl.panes[k].sel.marked.first : ctl.panes[k].current)}', maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ), ), ), ]), ); }