Files
MaliangAINovalWriter/AINoval/lib/widgets/editor/slash_command_menu.dart
2025-09-10 00:07:52 +08:00

323 lines
9.7 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
),
],
],
),
),
);
}
}