288 lines
12 KiB
Dart
288 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';
|
|
|
|
// ───────── 즐겨찾기 관리 패널 (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: 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<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);
|