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 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( 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 _save(List list) async { await widget.c.updateSettings((x) => x.folderBookmarks = list); if (mounted) setState(() {}); } Future _askName(String initial, String title) async { final name = await showDialog(context: context, builder: (_) => _NameDialog(initial: initial, title: title)); final t = name?.trim(); return t == null || t.isEmpty ? null : t; } Future _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.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.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('확인'))), ], ); }