Files
MookNote/lib/widgets/note_plus/slash_command_menu.dart
2026-06-26 05:53:40 +08:00

128 lines
4.3 KiB
Dart

import 'package:flutter/material.dart';
import '../../models/note_plus_models.dart';
/// 斜杠命令菜单
///
/// 输入 `/` 后弹出,显示可插入的 block 类型列表。
/// 支持按中文关键词过滤。
class SlashCommandMenu extends StatefulWidget {
final String query;
final void Function(NoteBlockType) onSelect;
final VoidCallback onDismiss;
const SlashCommandMenu({
super.key,
required this.query,
required this.onSelect,
required this.onDismiss,
});
@override
State<SlashCommandMenu> createState() => _SlashCommandMenuState();
}
class _SlashCommandMenuState extends State<SlashCommandMenu> {
int _selectedIndex = 0;
static const _menuItems = [
_MenuItem(NoteBlockType.paragraph, Icons.notes, '正文', '段落'),
_MenuItem(NoteBlockType.heading1, Icons.title, '标题1', '大标题'),
_MenuItem(NoteBlockType.heading2, Icons.title, '标题2', '中标题'),
_MenuItem(NoteBlockType.heading3, Icons.title, '标题3', '小标题'),
_MenuItem(NoteBlockType.bulletList, Icons.format_list_bulleted, '无序列表', '圆点列表'),
_MenuItem(NoteBlockType.numberedList, Icons.format_list_numbered, '有序列表', '数字列表'),
_MenuItem(NoteBlockType.checklist, Icons.check_box_outlined, '待办', '清单'),
_MenuItem(NoteBlockType.quote, Icons.format_quote, '引用', '引述'),
_MenuItem(NoteBlockType.codeBlock, Icons.code, '代码块', '源码'),
_MenuItem(NoteBlockType.divider, Icons.horizontal_rule, '分割线', '分隔'),
];
List<_MenuItem> get _filtered {
if (widget.query.isEmpty) return _menuItems;
final q = widget.query.toLowerCase();
return _menuItems.where((item) {
return item.label.toLowerCase().contains(q) ||
item.keywords.toLowerCase().contains(q);
}).toList();
}
@override
void didUpdateWidget(SlashCommandMenu oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.query != widget.query) {
final items = _filtered;
if (_selectedIndex >= items.length) {
_selectedIndex = items.isEmpty ? 0 : items.length - 1;
}
}
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final items = _filtered;
if (items.isEmpty) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
child: Text('无匹配项',
style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.4))),
);
}
return ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 320),
child: ListView.builder(
shrinkWrap: true,
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
final isSelected = index == _selectedIndex;
return InkWell(
onTap: () => widget.onSelect(item.type),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
color: isSelected
? colors.primary.withValues(alpha: 0.08)
: null,
child: Row(
children: [
Icon(item.icon, size: 20,
color: isSelected
? colors.primary
: colors.onSurface.withValues(alpha: 0.5)),
const SizedBox(width: 12),
Expanded(
child: Text(item.label,
style: TextStyle(
fontSize: 14,
fontWeight: isSelected ? FontWeight.w500 : FontWeight.w400,
color: isSelected ? colors.primary : colors.onSurface,
)),
),
Text(item.keywords,
style: TextStyle(
fontSize: 11,
color: colors.onSurface.withValues(alpha: 0.3),
)),
],
),
),
);
},
),
);
}
}
class _MenuItem {
final NoteBlockType type;
final IconData icon;
final String label;
final String keywords;
const _MenuItem(this.type, this.icon, this.label, this.keywords);
}