import 'dart:async'; import 'package:flutter/material.dart'; import '../../app/bookmarks_controller.dart'; import '../../core/bookmarks.dart'; import '../../l10n/tr.dart'; import '../bookmark_ui.dart'; import '../key_hint.dart'; import '../theme.dart'; // ───────── 즐겨찾기 관리 패널 (Chrome 즐겨찾기 관리자 참고) ───────── class BookmarkPanel extends StatefulWidget { final BookmarksController bm; final String currentUrl; final String currentTitle; final void Function(String url) onOpen; final void Function(String url) onOpenExternal; final VoidCallback onClose; /// 즐겨찾기 관리자 (전체 화면) 열기 final void Function({String? folderId})? onOpenManager; const BookmarkPanel({ super.key, required this.bm, required this.currentUrl, required this.currentTitle, required this.onOpen, required this.onOpenExternal, required this.onClose, this.onOpenManager, }); @override State createState() => _BookmarkPanelState(); } class _BookmarkPanelState extends State { String _folderId = BookmarkTree.barId; BookmarksController get bm => widget.bm; List _path() { final out = []; var id = _folderId; while (true) { final n = bm.tree.find(id); if (n == null) break; out.insert(0, n); final parent = bm.tree.parentOf(id); if (parent == null) break; id = parent.id; } return out; } @override Widget build(BuildContext context) { final folder = bm.tree.find(_folderId) ?? bm.tree.bar; final items = folder.children!; return Material( color: JjColors.panel, child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Padding( padding: const EdgeInsets.fromLTRB(12, 8, 4, 0), child: Row(children: [ Icon(Icons.bookmarks_outlined, size: 18, color: JjColors.accent), const SizedBox(width: 6), Text(tr('즐겨찾기'), style: TextStyle(fontWeight: FontWeight.w600)), const Spacer(), if (widget.onOpenManager != null) IconButton( tooltip: keyHint(tr('즐겨찾기 관리자'), 'Ctrl+Shift+O'), iconSize: 18, icon: const Icon(Icons.open_in_full), onPressed: () => widget.onOpenManager!(folderId: _folderId), ), IconButton(tooltip: tr('닫기'), iconSize: 18, icon: const Icon(Icons.close), onPressed: widget.onClose), ]), ), // 최상위 폴더 선택 Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Wrap(spacing: 4, children: [ for (final r in bm.tree.roots) ChoiceChip( label: Text(r.title, style: const TextStyle(fontSize: 11)), selected: _path().first.id == r.id, onSelected: (_) => setState(() => _folderId = r.id), ), ]), ), // 현재 폴더 경로 Padding( padding: const EdgeInsets.fromLTRB(8, 4, 8, 4), child: Wrap(crossAxisAlignment: WrapCrossAlignment.center, children: [ for (final (i, n) in _path().indexed) ...[ if (i > 0) Icon(Icons.chevron_right, size: 14, color: JjColors.textDim), BookmarkFolderDrop( bm: bm, folderId: n.id, child: InkWell( onTap: () => setState(() => _folderId = n.id), child: Padding( padding: const EdgeInsets.all(2), child: Text(n.title, style: TextStyle(fontSize: 12, color: n.id == _folderId ? JjColors.text : JjColors.accent)), ), ), ), ], ]), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Wrap(spacing: 4, runSpacing: 4, children: [ OutlinedButton.icon( onPressed: widget.currentUrl.startsWith('http') ? () => bm.addLink(widget.currentTitle.isEmpty ? widget.currentUrl : widget.currentTitle, widget.currentUrl, parentId: _folderId) : null, icon: const Icon(Icons.add, size: 16), label: Text(tr('현재 페이지'), style: TextStyle(fontSize: 12)), ), OutlinedButton.icon( onPressed: () async { await showNewFolderDialog(context, bm, _folderId); }, icon: const Icon(Icons.create_new_folder_outlined, size: 16), label: Text(tr('폴더'), style: TextStyle(fontSize: 12)), ), PopupMenuButton( tooltip: tr('다른 브라우저의 즐겨찾기 가져오기'), itemBuilder: (_) => [ PopupMenuItem(value: 'chrome', child: Text(tr('Chrome 에서 가져오기'))), PopupMenuItem(value: 'edge', child: Text(tr('Edge 에서 가져오기'))), PopupMenuItem(value: 'whale', child: Text(tr('Whale 에서 가져오기'))), ], onSelected: (b) async { final n = await bm.importFrom(b); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(n < 0 ? tr('이 브라우저의 즐겨찾기 파일을 찾지 못했습니다.') : trf('{0}개를 "기타 즐겨찾기" 에 가져왔습니다.', [n])))); if (n > 0) setState(() => _folderId = BookmarkTree.otherId); }, child: Padding( padding: EdgeInsets.symmetric(horizontal: 10, vertical: 8), child: Row(mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.download_for_offline_outlined, size: 16), SizedBox(width: 4), Text(tr('가져오기'), style: TextStyle(fontSize: 12)), ]), ), ), ]), ), const Divider(height: 12), Expanded( child: items.isEmpty ? Center(child: Text(tr('비어 있는 폴더'), style: TextStyle(color: JjColors.textDim))) : ReorderableListView.builder( buildDefaultDragHandles: false, itemCount: items.length, onReorderItem: (o, n) => bm.reorder(folder.id, o, n), itemBuilder: (_, i) { final n = items[i]; // 오른쪽 클릭 · 길게 누르기 (터치 화면) 로 같은 메뉴 void menuAt(Offset at) => showBookmarkMenu(context, bm: bm, globalPosition: at, n: n, folderId: folder.id, onOpen: widget.onOpen, onOpenExternal: widget.onOpenExternal, currentUrl: widget.currentUrl, currentTitle: widget.currentTitle); final tile = ListTile( dense: true, leading: Row(mainAxisSize: MainAxisSize.min, children: [ ReorderableDragStartListener( index: i, child: Icon(Icons.drag_indicator, size: 16, color: JjColors.textDim), ), const SizedBox(width: 4), // 아이콘을 끌어 폴더 줄 · 위 경로에 놓으면 그 폴더로 BookmarkDraggable( n: n, onMenu: menuAt, child: Icon(n.isFolder ? Icons.folder : Icons.public, size: 18, color: n.isFolder ? JjColors.amber : JjColors.textDim), ), ]), title: Text(n.title, maxLines: 1, overflow: TextOverflow.ellipsis), subtitle: n.isFolder ? Text(trf('{0}개', [n.children!.length]), style: const TextStyle(fontSize: 11)) : Text(n.url!, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 11)), onTap: () => n.isFolder ? setState(() => _folderId = n.id) : widget.onOpen(n.url!), trailing: PopupMenuButton( tooltip: tr('더 보기'), iconSize: 18, itemBuilder: (_) => [ if (!n.isFolder) PopupMenuItem(value: 'ext', child: Text(tr('외부 브라우저로 열기'))), PopupMenuItem(value: 'edit', child: Text(tr('수정'))), PopupMenuItem(value: 'move', child: Text(tr('다른 폴더로 이동'))), if (i > 0) PopupMenuItem(value: 'up', child: Text(tr('위로'))), if (i < items.length - 1) PopupMenuItem(value: 'down', child: Text(tr('아래로'))), const PopupMenuDivider(), PopupMenuItem(value: 'del', child: Text(tr('삭제'))), ], onSelected: (v) async { switch (v) { case 'ext': widget.onOpenExternal(n.url!); case 'edit': await editBookmark(context, bm, n); case 'move': await _moveTo(context, n); case 'up': bm.move(n.id, folder.id, i - 1); case 'down': bm.move(n.id, folder.id, i + 2); case 'del': removeBookmarkWithUndo(context, bm, n); } }, ), ); return KeyedSubtree( key: ValueKey(n.id), child: GestureDetector( onSecondaryTapDown: (d) => menuAt(d.globalPosition), onLongPressStart: (d) => menuAt(d.globalPosition), child: n.isFolder ? BookmarkFolderDrop(bm: bm, folderId: n.id, child: tile) : tile, ), ); }, ), ), Padding( padding: EdgeInsets.all(8), child: Text(tr('⋮⋮ 를 끌어 순서 바꾸기 · 아이콘을 끌어 폴더에 넣기 · 오른쪽 클릭 (길게 누르기) 메뉴'), textAlign: TextAlign.center, style: TextStyle(fontSize: 11, color: JjColors.textDim)), ), ]), ); } Future _moveTo(BuildContext context, BookmarkNode n) async { final target = await showDialog( context: context, builder: (ctx) => SimpleDialog( title: Text(trf('"{0}" 이동', [n.title])), children: [ for (final (f, depth) in bm.tree.folders()) if (f.id != n.id && !bm.tree.isInside(f.id, n.id)) SimpleDialogOption( onPressed: () => Navigator.pop(ctx, f.id), child: Padding( padding: EdgeInsets.only(left: depth * 16.0), child: Row(children: [ const Icon(Icons.folder_outlined, size: 16), const SizedBox(width: 6), Text(f.title), ]), ), ), ], ), ); if (target != null) { final dest = bm.tree.find(target)!; bm.move(n.id, target, dest.children!.length); } } } // ───────── 공용 (bookmark_ui.dart) ───────── /// 즐겨찾기 수정 (이름 · 주소 · 폴더) Future editBookmark(BuildContext context, BookmarksController bm, BookmarkNode n) => showBookmarkEditor(context, bm, n);