196 lines
8.6 KiB
Dart
196 lines
8.6 KiB
Dart
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<String> ancestorsOf(ExplorerPane pane) {
|
||
final out = <String>[];
|
||
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<String>(
|
||
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<Object>(
|
||
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.explorerHiddenItems.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.explorerHiddenItems.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,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
]),
|
||
);
|
||
}
|