JJ_MKVMaker/lib/ui/explorer_bookmarks.dart

187 lines
7.5 KiB
Dart

import 'package:flutter/material.dart';
import '../app/app_controller.dart';
import '../app/settings.dart';
import '../core/file_ops.dart' show samePath;
import '../core/vfs.dart';
import '../l10n/tr.dart';
import 'theme.dart';
/// 73: 파일 탐색기 즐겨찾기 - 지금 폴더를 넣고 창 위에서 한 번에 간다.
/// Rsync 화면에서는 "왼쪽 A + 오른쪽 B" 짝을 넣고 한 번에 연다.
/// [right] 가 있으면 Rsync 화면 (지금 오른쪽 폴더). [go] 는 이 창으로, [goPair] 는 두 창을 함께.
Future<void> showFolderBookmarks(
BuildContext context,
AppController c, {
required String current,
String? right,
required void Function(String dir) go,
void Function(String left, String right)? goPair,
}) async {
await showDialog<void>(
context: context,
builder: (ctx) => _BookmarksDialog(c: c, current: current, right: right, go: go, goPair: goPair),
);
}
class _BookmarksDialog extends StatefulWidget {
final AppController c;
final String current;
final String? right;
final void Function(String dir) go;
final void Function(String left, String right)? goPair;
const _BookmarksDialog({required this.c, required this.current, this.right, required this.go, this.goPair});
@override
State<_BookmarksDialog> createState() => _BookmarksDialogState();
}
class _BookmarksDialogState extends State<_BookmarksDialog> {
AppSettings get s => widget.c.settings;
bool get _rsync => widget.right != null && widget.goPair != null;
Future<void> _save(List<FolderBookmark> list) async {
await widget.c.updateSettings((x) => x.general.folderBookmarks = list);
if (mounted) setState(() {});
}
Future<String?> _askName(String initial, String title) async {
final name = await showDialog<String>(context: context, builder: (_) => _NameDialog(initial: initial, title: title));
final t = name?.trim();
return t == null || t.isEmpty ? null : t;
}
Future<void> _add({required bool pair}) async {
final b = pair
? FolderBookmark(name: '${vBasename(widget.current)} ⇄ ${vBasename(widget.right!)}', path: widget.current, right: widget.right)
: FolderBookmark(name: vBasename(widget.current).isEmpty ? widget.current : vBasename(widget.current), path: widget.current);
final name = await _askName(b.name, pair ? tr('지금 짝을 즐겨찾기에') : tr('지금 폴더를 즐겨찾기에'));
if (name == null) return;
await _save([...s.general.folderBookmarks, b.copyWith(name: name)]);
}
void _open(FolderBookmark b) {
Navigator.pop(context);
if (b.isPair && widget.goPair != null) {
widget.goPair!(b.path, b.right!);
} else {
widget.go(b.path);
}
}
@override
Widget build(BuildContext context) {
final mine = [for (final b in s.general.folderBookmarks) if (_rsync || !b.isPair) b];
// 바로 가기: 다운로드 폴더 · MKV 결과 폴더 (넣지 않아도)
final shortcuts = [
(tr('다운로드'), s.resolvedDownloadRoot()),
if (s.mkv.outputRoot?.isNotEmpty ?? false) (tr('MKV 결과 폴더'), s.mkv.outputRoot!),
].where((x) => !mine.any((b) => !b.isPair && samePath(b.path, x.$2))).toList();
final already = mine.any((b) => !b.isPair && samePath(b.path, widget.current));
final pairAlready = _rsync && mine.any((b) => b.isPair && samePath(b.path, widget.current) && samePath(b.right!, widget.right!));
return AlertDialog(
title: Text(tr('즐겨찾기')),
contentPadding: const EdgeInsets.fromLTRB(8, 12, 8, 0),
content: SizedBox(
width: 480,
child: ListView(shrinkWrap: true, children: [
if (mine.isEmpty)
Padding(
padding: const EdgeInsets.all(12),
child: Text(tr('아직 없습니다. 아래에서 지금 폴더를 넣으세요.'), style: TextStyle(color: JjColors.textDim)),
),
for (final b in mine)
ListTile(
dense: true,
leading: Icon(b.isPair ? Icons.compare_arrows : Icons.star, color: JjColors.amber, size: 20),
title: Text(b.name, maxLines: 1, overflow: TextOverflow.ellipsis),
subtitle: Text(b.isPair ? '${vDisplay(b.path)}\n⇄ ${vDisplay(b.right!)}' : vDisplay(b.path),
maxLines: b.isPair ? 2 : 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 11)),
onTap: () => _open(b),
trailing: Row(mainAxisSize: MainAxisSize.min, children: [
IconButton(
tooltip: tr('이름 바꾸기'),
visualDensity: VisualDensity.compact,
icon: const Icon(Icons.edit_outlined, size: 18),
onPressed: () async {
final name = await _askName(b.name, tr('이름 바꾸기'));
if (name != null) await _save([for (final x in s.general.folderBookmarks) identical(x, b) ? x.copyWith(name: name) : x]);
},
),
IconButton(
tooltip: tr('즐겨찾기에서 빼기 (폴더는 그대로)'),
visualDensity: VisualDensity.compact,
icon: const Icon(Icons.close, size: 18),
onPressed: () => _save([for (final x in s.general.folderBookmarks) if (!identical(x, b)) x]),
),
]),
),
if (shortcuts.isNotEmpty) ...[
const Divider(),
for (final (name, dir) in shortcuts)
ListTile(
dense: true,
leading: const Icon(Icons.folder_special_outlined, size: 20),
title: Text(name),
subtitle: Text(vDisplay(dir), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 11)),
onTap: () {
Navigator.pop(context);
widget.go(dir);
},
),
],
]),
),
actions: [
if (_rsync)
TextButton.icon(
onPressed: pairAlready ? null : () => _add(pair: true),
icon: const Icon(Icons.compare_arrows, size: 18),
label: Text(tr('지금 짝 추가')),
),
TextButton.icon(
onPressed: already || widget.current.isEmpty ? null : () => _add(pair: false),
icon: const Icon(Icons.star_border, size: 18),
label: Text(tr('지금 폴더 추가')),
),
TextButton(onPressed: () => Navigator.pop(context), child: Text(tr('닫기'))),
],
);
}
}
/// 표시 이름 넣기 (글 칸은 창이 닫히는 동안에도 그려지므로 창이 스스로 가지고 버린다)
class _NameDialog extends StatefulWidget {
final String initial;
final String title;
const _NameDialog({required this.initial, required this.title});
@override
State<_NameDialog> createState() => _NameDialogState();
}
class _NameDialogState extends State<_NameDialog> {
late final _ctl = TextEditingController(text: widget.initial);
@override
void dispose() {
_ctl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => AlertDialog(
title: Text(widget.title),
content: TextField(
controller: _ctl,
autofocus: true,
decoration: InputDecoration(labelText: tr('표시 이름')),
onSubmitted: (v) => Navigator.pop(context, v),
),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text(tr('취소'))),
FilledButton(onPressed: () => Navigator.pop(context, _ctl.text), child: Text(tr('확인'))),
],
);
}