JJ_MKVMaker/lib/ui/browser/bookmark_panel.dart

292 lines
12 KiB
Dart

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';
import '../jj_header_row.dart';
import '../jj_chip.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<BookmarkPanel> createState() => _BookmarkPanelState();
}
class _BookmarkPanelState extends State<BookmarkPanel> {
String _folderId = BookmarkTree.barId;
BookmarksController get bm => widget.bm;
List<BookmarkNode> _path() {
final out = <BookmarkNode>[];
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: JjHeaderRow(
icon: Icon(Icons.bookmarks_outlined, size: 18, color: JjColors.accent),
title: tr('즐겨찾기'),
style: const TextStyle(fontWeight: FontWeight.w600),
actions: [
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)
JjChip(
label: 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<String>(
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<String>(
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<void> _moveTo(BuildContext context, BookmarkNode n) async {
final target = await showDialog<String>(
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<void> editBookmark(BuildContext context, BookmarksController bm, BookmarkNode n) =>
showBookmarkEditor(context, bm, n);