马良AI写作初始化仓库

This commit is contained in:
邓滨杰
2025-09-10 00:07:52 +08:00
parent 3c06bb1a03
commit 39c0f8840f
1309 changed files with 318528 additions and 0 deletions

View File

@@ -0,0 +1,520 @@
import 'package:flutter/material.dart';
import 'package:ainoval/models/scene_beat_data.dart';
import 'package:ainoval/models/ai_request_models.dart';
import 'package:ainoval/models/unified_ai_model.dart';
import 'package:ainoval/models/novel_structure.dart';
import 'package:ainoval/models/novel_setting_item.dart';
import 'package:ainoval/models/setting_group.dart';
import 'package:ainoval/models/novel_snippet.dart';
import 'package:ainoval/widgets/editor/overlay_scene_beat_panel.dart';
import 'package:ainoval/utils/logger.dart';
import '../../config/app_config.dart';
// 🚀 新增:导入编辑器状态相关类
import 'package:ainoval/screens/editor/controllers/editor_screen_controller.dart';
import 'package:ainoval/screens/editor/managers/editor_layout_manager.dart';
/// 🚀 重构:纯数据管理器 - 只管理数据不操作UI
/// 全局单例负责场景节拍数据的CRUD操作
class SceneBeatDataManager {
static SceneBeatDataManager? _instance;
static SceneBeatDataManager get instance => _instance ??= SceneBeatDataManager._();
SceneBeatDataManager._();
// 🚀 核心场景节拍数据缓存场景ID -> 数据)
final Map<String, SceneBeatData> _sceneDataCache = {};
// 🚀 核心数据变化通知器场景ID -> 通知器)
final Map<String, ValueNotifier<SceneBeatData>> _dataNotifiers = {};
/// 获取场景数据的通知器用于UI监听
ValueNotifier<SceneBeatData> getDataNotifier(String sceneId) {
return _dataNotifiers.putIfAbsent(sceneId, () {
final data = _sceneDataCache[sceneId] ?? SceneBeatData.createDefault(
userId: AppConfig.userId ?? 'current-user', // 从AppConfig获取当前用户ID
novelId: 'unknown', // TODO: 从场景上下文获取
initialPrompt: '为当前场景生成场景节拍',
);
return ValueNotifier<SceneBeatData>(data);
});
}
/// 获取场景数据纯数据访问不触发UI
SceneBeatData getSceneData(String sceneId) {
final data = _sceneDataCache[sceneId];
if (data != null) {
return data;
}
// 创建默认数据但不立即缓存
return SceneBeatData.createDefault(
userId: AppConfig.userId ?? 'current-user', // 从AppConfig获取当前用户ID
novelId: 'unknown',
initialPrompt: '为当前场景生成场景节拍',
);
}
/// 更新场景数据(纯数据操作)
void updateSceneData(String sceneId, SceneBeatData newData) {
// 🚀 优化:检查数据是否真正发生变化
final currentData = _sceneDataCache[sceneId];
if (currentData != null && _isDataEqual(currentData, newData)) {
AppLogger.v('SceneBeatDataManager', '📊 场景数据无变化,跳过更新: $sceneId');
return;
}
AppLogger.i('SceneBeatDataManager', '🔄 更新场景数据: $sceneId');
// 更新缓存
_sceneDataCache[sceneId] = newData;
// 通知UI如果有监听器的话
final notifier = _dataNotifiers[sceneId];
if (notifier != null) {
notifier.value = newData;
}
}
/// 🚀 判断两个SceneBeatData是否相等基于关键字段
bool _isDataEqual(SceneBeatData data1, SceneBeatData data2) {
return data1.requestData == data2.requestData &&
data1.generatedContentDelta == data2.generatedContentDelta &&
data1.selectedUnifiedModelId == data2.selectedUnifiedModelId &&
data1.selectedLength == data2.selectedLength &&
data1.temperature == data2.temperature &&
data1.topP == data2.topP &&
data1.enableSmartContext == data2.enableSmartContext &&
data1.contextSelectionsData == data2.contextSelectionsData &&
data1.status == data2.status &&
data1.progress == data2.progress;
}
/// 🚀 公开方法判断两个SceneBeatData是否相等
bool isDataEqual(SceneBeatData data1, SceneBeatData data2) {
return _isDataEqual(data1, data2);
}
/// 更新场景状态(便捷方法)
void updateSceneStatus(String sceneId, SceneBeatStatus status) {
final currentData = getSceneData(sceneId);
final updatedData = currentData.updateStatus(status);
updateSceneData(sceneId, updatedData);
}
/// 清理场景数据
void clearSceneData(String sceneId) {
AppLogger.i('SceneBeatDataManager', '🗑️ 清理场景数据: $sceneId');
_sceneDataCache.remove(sceneId);
final notifier = _dataNotifiers.remove(sceneId);
notifier?.dispose();
}
/// 清理所有数据
void clearAllData() {
AppLogger.i('SceneBeatDataManager', '🗑️ 清理所有场景节拍数据');
_sceneDataCache.clear();
for (final notifier in _dataNotifiers.values) {
notifier.dispose();
}
_dataNotifiers.clear();
}
}
/// 🚀 重构UI管理器 - 只管理UI显示/隐藏,不处理数据
/// 全局单例,负责浮动面板的显示状态管理
class OverlaySceneBeatManager {
static OverlaySceneBeatManager? _instance;
static OverlaySceneBeatManager get instance => _instance ??= OverlaySceneBeatManager._();
OverlaySceneBeatManager._();
// 🚀 UI状态当前显示的浮动面板
OverlayEntry? _currentOverlay;
// 🚀 UI状态当前场景IDUI层面的概念
final ValueNotifier<String?> _currentSceneIdNotifier = ValueNotifier<String?>(null);
// 🚀 UI状态显示状态
bool _isVisible = false;
// 🚀 UI参数缓存避免重复传递
Novel? _cachedNovel;
List<NovelSettingItem> _cachedSettings = [];
List<SettingGroup> _cachedSettingGroups = [];
List<NovelSnippet> _cachedSnippets = [];
Function(String, UniversalAIRequest, UnifiedAIModel)? _cachedOnGenerate;
// 🚀 新增:编辑器状态监听
EditorScreenController? _editorController;
EditorLayoutManager? _layoutManager;
VoidCallback? _editorControllerListener;
VoidCallback? _layoutManagerListener;
/// 获取当前场景ID通知器UI监听用
ValueNotifier<String?> get currentSceneIdNotifier => _currentSceneIdNotifier;
/// 获取当前场景ID
String? get currentSceneId => _currentSceneIdNotifier.value;
/// 是否显示中
bool get isVisible => _isVisible;
/// 🚀 新增:绑定编辑器状态监听
void bindEditorState({
EditorScreenController? editorController,
EditorLayoutManager? layoutManager,
}) {
AppLogger.i('OverlaySceneBeatManager', '🔗 绑定编辑器状态监听');
// 清理之前的监听器
unbindEditorState();
_editorController = editorController;
_layoutManager = layoutManager;
// 监听编辑器状态变化
if (_editorController != null) {
_editorControllerListener = () {
_onEditorStateChanged();
};
_editorController!.addListener(_editorControllerListener!);
}
// 监听布局管理器状态变化
if (_layoutManager != null) {
_layoutManagerListener = () {
_onLayoutStateChanged();
};
_layoutManager!.addListener(_layoutManagerListener!);
}
}
/// 🚀 新增:解绑编辑器状态监听
void unbindEditorState() {
if (_editorController != null && _editorControllerListener != null) {
_editorController!.removeListener(_editorControllerListener!);
_editorController = null;
_editorControllerListener = null;
}
if (_layoutManager != null && _layoutManagerListener != null) {
_layoutManager!.removeListener(_layoutManagerListener!);
_layoutManager = null;
_layoutManagerListener = null;
}
}
/// 🚀 新增:处理编辑器状态变化
void _onEditorStateChanged() {
if (_editorController == null || !_isVisible) return;
// 检查是否切换到了其他视图
final bool isInMainEditMode = !_editorController!.isPlanViewActive &&
!_editorController!.isNextOutlineViewActive &&
!_editorController!.isPromptViewActive;
if (!isInMainEditMode) {
AppLogger.i('OverlaySceneBeatManager', '📺 检测到视图切换,隐藏场景节拍面板');
hide();
}
}
/// 🚀 新增:处理布局状态变化
void _onLayoutStateChanged() {
if (_layoutManager == null || !_isVisible) return;
// 检查是否有设置面板显示
if (_layoutManager!.isSettingsPanelVisible) {
AppLogger.i('OverlaySceneBeatManager', '⚙️ 检测到设置面板显示,隐藏场景节拍面板');
hide();
}
// 检查是否有其他重要对话框显示
if (_layoutManager!.isNovelSettingsVisible) {
AppLogger.i('OverlaySceneBeatManager', '📖 检测到小说设置显示,隐藏场景节拍面板');
hide();
}
}
/// 🚀 显示浮动面板只处理UI显示不管理数据
void show({
required BuildContext context,
required String sceneId,
Novel? novel,
List<NovelSettingItem> settings = const [],
List<SettingGroup> settingGroups = const [],
List<NovelSnippet> snippets = const [],
Function(String, UniversalAIRequest, UnifiedAIModel)? onGenerate,
// 🚀 新增:可选的编辑器状态参数
EditorScreenController? editorController,
EditorLayoutManager? layoutManager,
}) {
AppLogger.i('OverlaySceneBeatManager', '🎯 显示场景节拍面板: $sceneId');
// 🚀 绑定编辑器状态监听
bindEditorState(
editorController: editorController,
layoutManager: layoutManager,
);
// 🚀 检查当前是否在主编辑模式
if (editorController != null) {
final bool isInMainEditMode = !editorController.isPlanViewActive &&
!editorController.isNextOutlineViewActive &&
!editorController.isPromptViewActive;
if (!isInMainEditMode) {
AppLogger.w('OverlaySceneBeatManager', '⚠️ 当前不在主编辑模式,跳过显示场景节拍面板');
return;
}
}
// 🚀 检查是否有设置面板显示
if (layoutManager != null && layoutManager.isSettingsPanelVisible) {
AppLogger.w('OverlaySceneBeatManager', '⚠️ 设置面板正在显示,跳过显示场景节拍面板');
return;
}
// 缓存参数
_cachedNovel = novel;
_cachedSettings = settings;
_cachedSettingGroups = settingGroups;
_cachedSnippets = snippets;
_cachedOnGenerate = onGenerate;
// 如果已经显示,只切换场景
if (_isVisible && _currentOverlay != null) {
switchScene(sceneId);
return;
}
// 创建新的浮动面板
_currentOverlay = _createOverlayEntry(context, sceneId);
// 插入到Overlay中
Overlay.of(context).insert(_currentOverlay!);
// 更新状态
_isVisible = true;
_currentSceneIdNotifier.value = sceneId;
AppLogger.i('OverlaySceneBeatManager', '✅ 场景节拍面板已显示');
}
/// 🚀 切换场景只更新场景ID面板自动响应
void switchScene(String sceneId) {
if (_currentSceneIdNotifier.value == sceneId) {
AppLogger.v('OverlaySceneBeatManager', '场景ID相同跳过切换: $sceneId');
return;
}
AppLogger.i('OverlaySceneBeatManager', '🔄 切换场景: ${_currentSceneIdNotifier.value} -> $sceneId');
// 只更新场景IDUI会自动响应
_currentSceneIdNotifier.value = sceneId;
}
/// 🚀 隐藏面板只处理UI隐藏
void hide() {
if (!_isVisible || _currentOverlay == null) {
return;
}
AppLogger.i('OverlaySceneBeatManager', '🫥 隐藏场景节拍面板');
// 移除浮动面板
_currentOverlay!.remove();
_currentOverlay = null;
// 更新状态
_isVisible = false;
_currentSceneIdNotifier.value = null;
AppLogger.i('OverlaySceneBeatManager', '✅ 场景节拍面板已隐藏');
}
/// 🚀 切换显示状态
void toggle({
required BuildContext context,
required String sceneId,
Novel? novel,
List<NovelSettingItem> settings = const [],
List<SettingGroup> settingGroups = const [],
List<NovelSnippet> snippets = const [],
Function(String, UniversalAIRequest, UnifiedAIModel)? onGenerate,
// 🚀 新增:可选的编辑器状态参数
EditorScreenController? editorController,
EditorLayoutManager? layoutManager,
}) {
if (_isVisible) {
hide();
} else {
show(
context: context,
sceneId: sceneId,
novel: novel,
settings: settings,
settingGroups: settingGroups,
snippets: snippets,
onGenerate: onGenerate,
editorController: editorController,
layoutManager: layoutManager,
);
}
}
/// 🚀 创建浮动面板UI新架构UI独立管理
OverlayEntry _createOverlayEntry(BuildContext context, String initialSceneId) {
return OverlayEntry(
builder: (overlayContext) => ValueListenableBuilder<String?>(
valueListenable: _currentSceneIdNotifier,
builder: (context, currentSceneId, child) {
if (currentSceneId == null) {
return const SizedBox.shrink();
}
return SceneBeatFloatingPanel(
sceneId: currentSceneId,
novel: _cachedNovel,
settings: _cachedSettings,
settingGroups: _cachedSettingGroups,
snippets: _cachedSnippets,
onClose: hide,
onGenerate: _cachedOnGenerate,
);
},
),
);
}
/// 🚀 修改:增强的释放资源方法
void dispose() {
AppLogger.i('OverlaySceneBeatManager', '🗑️ 开始释放UI管理器资源');
// 隐藏面板
hide();
// 解绑编辑器状态监听
unbindEditorState();
// 释放通知器
_currentSceneIdNotifier.dispose();
// 清理缓存
_cachedNovel = null;
_cachedSettings = [];
_cachedSettingGroups = [];
_cachedSnippets = [];
_cachedOnGenerate = null;
AppLogger.i('OverlaySceneBeatManager', '✅ UI管理器资源已释放');
}
}
/// 🚀 新增场景节拍浮动面板UI组件
/// 职责纯UI展示通过监听数据管理器获取数据变化
class SceneBeatFloatingPanel extends StatefulWidget {
const SceneBeatFloatingPanel({
super.key,
required this.sceneId,
this.novel,
this.settings = const [],
this.settingGroups = const [],
this.snippets = const [],
this.onClose,
this.onGenerate,
});
final String sceneId;
final Novel? novel;
final List<NovelSettingItem> settings;
final List<SettingGroup> settingGroups;
final List<NovelSnippet> snippets;
final VoidCallback? onClose;
final Function(String, UniversalAIRequest, UnifiedAIModel)? onGenerate;
@override
State<SceneBeatFloatingPanel> createState() => _SceneBeatFloatingPanelState();
}
class _SceneBeatFloatingPanelState extends State<SceneBeatFloatingPanel> {
// 🚀 数据监听器(只监听当前场景的数据变化)
late ValueNotifier<SceneBeatData> _dataNotifier;
@override
void initState() {
super.initState();
_setupDataListener();
}
@override
void didUpdateWidget(SceneBeatFloatingPanel oldWidget) {
super.didUpdateWidget(oldWidget);
// 🚀 只有场景ID变化时才重新设置监听器
if (oldWidget.sceneId != widget.sceneId) {
AppLogger.i('SceneBeatFloatingPanel', '🔄 场景切换,重新设置数据监听: ${oldWidget.sceneId} -> ${widget.sceneId}');
_setupDataListener();
}
}
/// 🚀 设置数据监听器核心数据和UI分离
void _setupDataListener() {
// 获取当前场景的数据通知器
_dataNotifier = SceneBeatDataManager.instance.getDataNotifier(widget.sceneId);
AppLogger.i('SceneBeatFloatingPanel', '📡 设置场景数据监听: ${widget.sceneId}');
}
@override
Widget build(BuildContext context) {
// 🚀 核心:优化重建策略,减少不必要的重建
return ValueListenableBuilder<SceneBeatData>(
valueListenable: _dataNotifier,
// 🚀 使用 child 参数缓存不需要重建的部分
child: _buildStaticContent(),
builder: (context, sceneBeatData, child) {
// 🚀 直接返回面板避免ParentData冲突
return OverlaySceneBeatPanel(
sceneId: widget.sceneId,
data: sceneBeatData,
novel: widget.novel,
settings: widget.settings,
settingGroups: widget.settingGroups,
snippets: widget.snippets,
onClose: widget.onClose,
onGenerate: widget.onGenerate != null
? (request, model) => widget.onGenerate!(widget.sceneId, request, model)
: null,
onDataChanged: (newData) {
// 🚀 避免无谓的更新:只在数据真正改变时才更新
if (_shouldUpdateData(sceneBeatData, newData)) {
SceneBeatDataManager.instance.updateSceneData(widget.sceneId, newData);
}
},
);
},
);
}
/// 🚀 构建静态内容(不需要监听数据变化的部分)
Widget _buildStaticContent() {
// 这里可以放置不依赖于数据的静态组件
return const SizedBox.shrink();
}
/// 🚀 判断是否需要更新数据(避免无意义的更新)
bool _shouldUpdateData(SceneBeatData oldData, SceneBeatData newData) {
// 🚀 简化:利用数据管理器的公开相等性检查方法
return !SceneBeatDataManager.instance.isDataEqual(oldData, newData);
}
@override
void dispose() {
// 🚀 不需要手动dispose _dataNotifier由数据管理器统一管理
super.dispose();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,323 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:ainoval/utils/logger.dart';
/// 斜杠命令类型
enum SlashCommandType {
sceneBeat,
continue_,
summary,
refactor,
dialogue,
sceneDescription;
String get displayName {
switch (this) {
case SlashCommandType.sceneBeat:
return '场景节拍';
case SlashCommandType.continue_:
return '续写';
case SlashCommandType.summary:
return '摘要';
case SlashCommandType.refactor:
return '重构';
case SlashCommandType.dialogue:
return '对话';
case SlashCommandType.sceneDescription:
return '描述';
}
}
IconData get icon {
switch (this) {
case SlashCommandType.sceneBeat:
return Icons.waves_outlined;
case SlashCommandType.continue_:
return Icons.edit_outlined;
case SlashCommandType.summary:
return Icons.summarize_outlined;
case SlashCommandType.refactor:
return Icons.transform_outlined;
case SlashCommandType.dialogue:
return Icons.chat_bubble_outline;
case SlashCommandType.sceneDescription:
return Icons.landscape_outlined;
}
}
String get desc {
switch (this) {
case SlashCommandType.sceneBeat:
return '一个关键时刻,重要的事情发生改变,推动故事发展';
case SlashCommandType.continue_:
return '基于当前上下文继续创作内容';
case SlashCommandType.summary:
return '生成当前内容的摘要';
case SlashCommandType.refactor:
return '重新整理和优化现有内容';
case SlashCommandType.dialogue:
return '生成角色之间的对话';
case SlashCommandType.sceneDescription:
return '添加场景或人物的详细描述';
}
}
}
/// 斜杠命令菜单组件
class SlashCommandMenu extends StatefulWidget {
const SlashCommandMenu({
super.key,
required this.position,
required this.onCommandSelected,
this.onDismiss,
this.availableCommands = SlashCommandType.values,
this.maxWidth = 280,
});
/// 菜单显示位置
final Offset position;
/// 命令被选中时的回调
final Function(SlashCommandType) onCommandSelected;
/// 菜单被取消时的回调
final VoidCallback? onDismiss;
/// 可用的命令列表
final List<SlashCommandType> availableCommands;
/// 菜单最大宽度
final double maxWidth;
@override
State<SlashCommandMenu> createState() => _SlashCommandMenuState();
}
class _SlashCommandMenuState extends State<SlashCommandMenu>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
late Animation<double> _opacityAnimation;
int _selectedIndex = 0;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 0.8,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeOutBack,
));
_opacityAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeOut,
));
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _selectCommand(SlashCommandType command) {
AppLogger.d('SlashCommandMenu', '选择命令: ${command.displayName}');
widget.onCommandSelected(command);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Opacity(
opacity: _opacityAnimation.value,
child: Transform.scale(
scale: _scaleAnimation.value,
alignment: Alignment.topLeft,
child: Material(
elevation: 8,
borderRadius: BorderRadius.circular(12),
color: theme.colorScheme.surface,
child: Container(
constraints: BoxConstraints(
maxWidth: widget.maxWidth,
maxHeight: 400,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: theme.colorScheme.outline.withOpacity(0.2),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Icon(
Icons.flash_on,
size: 18,
color: theme.colorScheme.primary,
),
const SizedBox(width: 8),
Text(
'AI 写作助手',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
color: theme.colorScheme.primary,
),
),
],
),
),
Divider(
height: 1,
color: theme.colorScheme.outline.withOpacity(0.1),
),
// 命令列表
Flexible(
child: ListView.builder(
shrinkWrap: true,
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: widget.availableCommands.length,
itemBuilder: (context, index) {
final command = widget.availableCommands[index];
final isSelected = index == _selectedIndex;
return _buildCommandItem(
theme,
command,
isSelected,
() => _selectCommand(command),
);
},
),
),
// 提示文字
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Text(
'使用 ↑↓ 选择Enter 确认Esc 取消',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
fontSize: 11,
),
),
),
],
),
),
),
),
);
},
);
}
Widget _buildCommandItem(
ThemeData theme,
SlashCommandType command,
bool isSelected,
VoidCallback onTap,
) {
return InkWell(
onTap: onTap,
onHover: (hovering) {
if (hovering) {
setState(() {
_selectedIndex = widget.availableCommands.indexOf(command);
});
}
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
color: isSelected
? theme.colorScheme.primaryContainer.withOpacity(0.3)
: Colors.transparent,
child: Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: isSelected
? theme.colorScheme.primary
: theme.colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
command.icon,
size: 16,
color: isSelected
? theme.colorScheme.onPrimary
: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
command.displayName,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
color: isSelected
? theme.colorScheme.primary
: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 2),
Text(
command.desc,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
fontSize: 11,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
if (isSelected) ...[
const SizedBox(width: 8),
Icon(
Icons.arrow_forward_ios,
size: 12,
color: theme.colorScheme.primary,
),
],
],
),
),
);
}
}

View File

@@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import 'package:ainoval/widgets/editor/slash_command_menu.dart';
/// 斜杠命令覆盖层
/// 用于在编辑器上显示命令选择菜单
class SlashCommandOverlay {
static OverlayEntry? _overlayEntry;
/// 显示斜杠命令菜单
static void show({
required BuildContext context,
required Offset position,
required Function(SlashCommandType) onCommandSelected,
required VoidCallback onDismiss,
required List<SlashCommandType> availableCommands,
}) {
// 如果已经显示了菜单,先隐藏
hide();
_overlayEntry = OverlayEntry(
builder: (context) => Positioned(
left: position.dx,
top: position.dy,
child: Material(
color: Colors.transparent,
child: SlashCommandMenu(
position: position,
onCommandSelected: onCommandSelected,
onDismiss: onDismiss,
availableCommands: availableCommands,
),
),
),
);
Overlay.of(context).insert(_overlayEntry!);
}
/// 隐藏斜杠命令菜单
static void hide() {
_overlayEntry?.remove();
_overlayEntry = null;
}
/// 检查是否正在显示菜单
static bool get isShowing => _overlayEntry != null;
}