import 'package:flutter/material.dart'; import '../../models/note.dart'; import '../../widgets/cute_icon.dart'; /// 目录选择面板 /// /// 从底部弹出,展示「全部 / 未分类 / 目录树」三层结构。 /// 选中后 pop 出对应的 [NoteLocation];直接关掉则返回 null。 class FolderPickerSheet extends StatefulWidget { const FolderPickerSheet({ super.key, required this.tree, required this.current, this.title = '选择目录', this.showAll = true, }); final FolderTree tree; final NoteLocation current; final String title; /// 是否显示「全部」这一项。 /// 给笔记选目标目录时要关掉 —— 「全部」不是一个能存放笔记的位置。 final bool showAll; /// 弹出面板的便捷方法 static Future show( BuildContext context, { required FolderTree tree, required NoteLocation current, String title = '选择目录', bool showAll = true, }) { return showModalBottomSheet( context: context, backgroundColor: Colors.white, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (_) => FolderPickerSheet( tree: tree, current: current, title: title, showAll: showAll, ), ); } @override State createState() => _FolderPickerSheetState(); } class _FolderPickerSheetState extends State { final Set _expanded = {}; @override void initState() { super.initState(); // 默认展开当前所在目录的所有上级,用户一眼能看到自己在哪 if (widget.current.scope == NoteScope.folder && widget.current.folderId != null) { final path = widget.tree.findPath(widget.current.folderId!); if (path != null) { for (final node in path) { _expanded.add(node.id); } } } } bool _isCurrent(NoteScope scope, int? folderId) { if (widget.current.scope != scope) return false; if (scope != NoteScope.folder) return true; return widget.current.folderId == folderId; } @override Widget build(BuildContext context) { return SafeArea( child: ConstrainedBox( constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.75, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 10), Container( width: 40, height: 4, decoration: BoxDecoration( color: const Color(0xFFE0D6CC), borderRadius: BorderRadius.circular(2), ), ), Padding( padding: const EdgeInsets.fromLTRB(20, 14, 20, 6), child: Align( alignment: Alignment.centerLeft, child: Text( widget.title, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF5D4037), ), ), ), ), Flexible( child: ListView( shrinkWrap: true, padding: const EdgeInsets.fromLTRB(12, 4, 12, 20), children: [ if (widget.showAll) _row( icon: 'education', name: '全部', count: widget.tree.totalCount, depth: 0, selected: _isCurrent(NoteScope.all, null), onTap: () => Navigator.pop(context, NoteLocation.all), ), _row( icon: 'income', name: '未分类', count: widget.tree.uncategorizedCount, depth: 0, selected: _isCurrent(NoteScope.uncategorized, null), onTap: () => Navigator.pop(context, NoteLocation.uncategorized), ), if (widget.tree.folders.isNotEmpty) const Padding( padding: EdgeInsets.fromLTRB(8, 12, 8, 4), child: Divider(height: 1, color: Color(0xFFF0E6D8)), ), ...widget.tree.folders.map((n) => _buildNode(n, 0)), ], ), ), ], ), ), ); } /// 递归渲染目录节点 Widget _buildNode(FolderNode node, int depth) { final hasChildren = node.children.isNotEmpty; final open = _expanded.contains(node.id); return Column( mainAxisSize: MainAxisSize.min, children: [ _row( icon: (open && hasChildren) ? 'folder-open' : 'folder', name: node.name, count: node.noteCount, depth: depth, selected: _isCurrent(NoteScope.folder, node.id), trailing: hasChildren ? GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { setState(() { if (open) { _expanded.remove(node.id); } else { _expanded.add(node.id); } }); }, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8), child: AnimatedRotation( turns: open ? 0.25 : 0, duration: const Duration(milliseconds: 150), child: const Icon( Icons.chevron_right, size: 18, color: Color(0xFFBCAAA4), ), ), ), ) : null, onTap: () => Navigator.pop( context, NoteLocation( scope: NoteScope.folder, folderId: node.id, folderName: node.name, ), ), ), if (open && hasChildren) ...node.children.map((c) => _buildNode(c, depth + 1)), ], ); } Widget _row({ required String icon, required String name, required int count, required int depth, required bool selected, required VoidCallback onTap, Widget? trailing, }) { return Padding( padding: EdgeInsets.only(left: depth * 18.0), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(10), child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), decoration: BoxDecoration( color: selected ? const Color(0xFFFFF3E6) : null, borderRadius: BorderRadius.circular(10), ), child: Row( children: [ CuteIcon(icon, size: 17), const SizedBox(width: 10), Expanded( child: Text( name, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 14.5, fontWeight: selected ? FontWeight.w600 : FontWeight.normal, color: selected ? const Color(0xFFC8956E) : const Color(0xFF5D4037), ), ), ), Text( '$count', style: const TextStyle(fontSize: 12, color: Color(0xFFBCAAA4)), ), ?trailing, ], ), ), ), ); } }