JJ_MKVMaker/lib/ui/explorer/explorer_bars.dart

196 lines
8.6 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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.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,
),
),
),
),
),
]),
);
}