187 lines
7.5 KiB
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.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.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.folderBookmarks) if (_rsync || !b.isPair) b];
|
|
// 바로 가기: 다운로드 폴더 · MKV 결과 폴더 (넣지 않아도)
|
|
final shortcuts = [
|
|
(tr('다운로드'), s.resolvedDownloadRoot()),
|
|
if (s.mkvOutputRoot?.isNotEmpty ?? false) (tr('MKV 결과 폴더'), s.mkvOutputRoot!),
|
|
].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: const TextStyle(color: JjColors.textDim)),
|
|
),
|
|
for (final b in mine)
|
|
ListTile(
|
|
dense: true,
|
|
leading: Icon(b.isPair ? Icons.compare_arrows : Icons.star, color: Colors.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.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.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('확인'))),
|
|
],
|
|
);
|
|
}
|