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

487 lines
15 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:ainoval/models/preset_models.dart';
import 'package:ainoval/services/ai_preset_service.dart';
import 'package:ainoval/utils/web_theme.dart';
import 'package:ainoval/utils/logger.dart';
/// 预设下拉框按钮组件
/// 替换原有的预设按钮,提供下拉框选择预设的功能
class PresetDropdownButton extends StatefulWidget {
/// 构造函数
const PresetDropdownButton({
super.key,
required this.featureType,
this.currentPreset,
this.onPresetSelected,
this.onCreatePreset,
this.onManagePresets,
this.novelId,
this.label = '预设',
});
/// AI功能类型用于过滤预设
final String featureType;
/// 当前选中的预设
final AIPromptPreset? currentPreset;
/// 预设选择回调
final ValueChanged<AIPromptPreset>? onPresetSelected;
/// 创建预设回调
final VoidCallback? onCreatePreset;
/// 管理预设回调
final VoidCallback? onManagePresets;
/// 小说ID用于过滤预设
final String? novelId;
/// 按钮标签
final String label;
@override
State<PresetDropdownButton> createState() => _PresetDropdownButtonState();
}
class _PresetDropdownButtonState extends State<PresetDropdownButton> {
final String _tag = 'PresetDropdownButton';
List<AIPromptPreset> _recentPresets = [];
List<AIPromptPreset> _favoritePresets = [];
List<AIPromptPreset> _recommendedPresets = [];
bool _isLoading = false;
OverlayEntry? _overlayEntry;
final LayerLink _layerLink = LayerLink();
final GlobalKey _buttonKey = GlobalKey();
@override
void initState() {
super.initState();
_loadPresets();
}
@override
void dispose() {
_removeOverlay();
super.dispose();
}
/// 加载预设数据
Future<void> _loadPresets() async {
if (_isLoading) return;
setState(() {
_isLoading = true;
});
try {
final presetService = AIPresetService();
// 使用新的统一接口获取功能预设列表
final presetListResponse = await presetService.getFeaturePresetList(
widget.featureType,
novelId: widget.novelId,
);
setState(() {
_recentPresets = presetListResponse.recentUsed.map((item) => item.preset).toList();
_favoritePresets = presetListResponse.favorites.map((item) => item.preset).toList();
_recommendedPresets = presetListResponse.recommended.map((item) => item.preset).toList();
_isLoading = false;
});
AppLogger.d(_tag, '预设数据加载完成: 最近${_recentPresets.length}个, 收藏${_favoritePresets.length}个, 推荐${_recommendedPresets.length}');
} catch (e) {
setState(() {
_isLoading = false;
});
AppLogger.e(_tag, '加载预设数据失败', e);
}
}
/// 显示下拉菜单
void _showDropdown() {
if (_overlayEntry != null) {
_removeOverlay();
return;
}
_overlayEntry = _createOverlayEntry();
Overlay.of(context).insert(_overlayEntry!);
}
/// 移除下拉菜单
void _removeOverlay() {
_overlayEntry?.remove();
_overlayEntry = null;
}
/// 创建下拉菜单覆盖层
OverlayEntry _createOverlayEntry() {
final RenderBox renderBox = _buttonKey.currentContext?.findRenderObject() as RenderBox;
final size = renderBox.size;
return OverlayEntry(
builder: (context) => GestureDetector(
onTap: _removeOverlay,
behavior: HitTestBehavior.translucent,
child: Stack(
children: [
// 透明背景,点击关闭
Positioned.fill(
child: Container(color: Colors.transparent),
),
// 下拉菜单
CompositedTransformFollower(
link: _layerLink,
showWhenUnlinked: false,
offset: Offset(0, size.height + 2),
child: Material(
elevation: 4,
borderRadius: BorderRadius.circular(6),
color: Colors.transparent,
child: Container(
width: 240, // 减小宽度使其更紧凑
constraints: const BoxConstraints(maxHeight: 320), // 减小最大高度
decoration: BoxDecoration(
color: WebTheme.getSurfaceColor(context),
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: WebTheme.isDarkMode(context)
? WebTheme.darkGrey300
: WebTheme.grey300,
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: _buildDropdownContent(),
),
),
),
],
),
),
);
}
/// 构建下拉菜单内容
Widget _buildDropdownContent() {
return ClipRRect(
borderRadius: BorderRadius.circular(6),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 当前预设(如果有)
if (widget.currentPreset != null) ...[
_buildSectionHeader('当前预设'),
_buildPresetItem(
widget.currentPreset!,
isSelected: true,
showCheckmark: true,
),
_buildDivider(),
],
// 最近使用预设
if (_recentPresets.isNotEmpty) ...[
_buildSectionHeader('最近使用'),
..._recentPresets.take(3).map((preset) => _buildPresetItem(preset)), // 减少显示数量
if (_favoritePresets.isNotEmpty || _recommendedPresets.isNotEmpty) _buildDivider(),
],
// 收藏预设
if (_favoritePresets.isNotEmpty) ...[
_buildSectionHeader('收藏预设'),
..._favoritePresets.take(3).map((preset) => _buildPresetItem(preset)), // 减少显示数量
if (_recommendedPresets.isNotEmpty) _buildDivider(),
],
// 推荐预设
if (_recommendedPresets.isNotEmpty) ...[
_buildSectionHeader('推荐预设'),
..._recommendedPresets.take(3).map((preset) => _buildPresetItem(preset)), // 减少显示数量
_buildDivider(),
],
// 空状态
if (_recentPresets.isEmpty && _favoritePresets.isEmpty && _recommendedPresets.isEmpty && widget.currentPreset == null) ...[
_buildEmptyState(),
_buildDivider(),
],
// 操作按钮
_buildActionButtons(),
],
),
);
}
/// 构建分组标题
Widget _buildSectionHeader(String title) {
return Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(12, 8, 12, 4), // 减少内边距
child: Text(
title,
style: WebTheme.labelSmall.copyWith(
color: WebTheme.getSecondaryTextColor(context),
fontWeight: FontWeight.w600,
),
),
);
}
/// 构建预设项
Widget _buildPresetItem(
AIPromptPreset preset, {
bool isSelected = false,
bool showCheckmark = false,
}) {
return WebTheme.getMaterialWrapper(
child: InkWell(
onTap: () {
_removeOverlay();
widget.onPresetSelected?.call(preset);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), // 减少内边距
child: Row(
children: [
// 预设信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Flexible( // 使用 Flexible 而不是 Expanded 避免溢出
child: Text(
preset.displayName,
style: WebTheme.bodySmall.copyWith(
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
color: isSelected
? WebTheme.getTextColor(context, isPrimary: true)
: WebTheme.getTextColor(context, isPrimary: false),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
if (preset.isFavorite) ...[
const SizedBox(width: 4),
Icon(
Icons.star,
size: 10,
color: Colors.amber.shade600,
),
],
],
),
if (preset.presetDescription != null && preset.presetDescription!.isNotEmpty) ...[
const SizedBox(height: 2),
Text(
preset.presetDescription!,
style: WebTheme.labelSmall.copyWith(
color: WebTheme.getSecondaryTextColor(context),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
// 选中标记
if (showCheckmark) ...[
const SizedBox(width: 6),
Icon(
Icons.check,
size: 14,
color: WebTheme.getTextColor(context, isPrimary: false),
),
],
],
),
),
),
);
}
/// 构建分割线
Widget _buildDivider() {
return Container(
height: 1,
margin: const EdgeInsets.symmetric(horizontal: 8),
color: WebTheme.isDarkMode(context)
? WebTheme.darkGrey200
: WebTheme.grey200,
);
}
/// 构建空状态
Widget _buildEmptyState() {
return Container(
padding: const EdgeInsets.all(16), // 减少内边距
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.info_outline,
size: 24, // 减小图标尺寸
color: WebTheme.getSecondaryTextColor(context),
),
const SizedBox(height: 6),
Text(
'暂无预设',
style: WebTheme.bodySmall.copyWith(
fontWeight: FontWeight.w500,
color: WebTheme.getTextColor(context, isPrimary: false),
),
),
const SizedBox(height: 2),
Text(
'创建第一个预设来快速重用配置',
style: WebTheme.labelSmall.copyWith(
color: WebTheme.getSecondaryTextColor(context),
),
textAlign: TextAlign.center,
),
],
),
);
}
/// 构建操作按钮
Widget _buildActionButtons() {
return Container(
padding: const EdgeInsets.all(8), // 减少内边距
child: Row(
children: [
// 创建预设按钮
Expanded(
child: TextButton.icon(
onPressed: () {
_removeOverlay();
widget.onCreatePreset?.call();
},
icon: Icon(
Icons.add,
size: 14,
color: WebTheme.getSecondaryTextColor(context),
),
label: Text(
'创建',
style: WebTheme.labelSmall.copyWith(
color: WebTheme.getSecondaryTextColor(context),
),
),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), // 减少内边距
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
),
),
),
const SizedBox(width: 4),
// 管理预设按钮
Expanded(
child: TextButton.icon(
onPressed: () {
_removeOverlay();
widget.onManagePresets?.call();
},
icon: Icon(
Icons.settings,
size: 14,
color: WebTheme.getSecondaryTextColor(context),
),
label: Text(
'管理',
style: WebTheme.labelSmall.copyWith(
color: WebTheme.getSecondaryTextColor(context),
),
),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), // 减少内边距
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
),
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
// 获取当前预设的显示名称,如果太长则截断
String displayText = widget.currentPreset?.presetName ?? widget.label;
if (displayText.length > 8) { // 限制显示长度避免溢出
displayText = '${displayText.substring(0, 6)}...';
}
return CompositedTransformTarget(
link: _layerLink,
child: WebTheme.getMaterialWrapper(
child: InkWell(
key: _buttonKey,
onTap: _showDropdown,
borderRadius: BorderRadius.circular(4),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), // 大幅减少内边距
constraints: const BoxConstraints(
minWidth: 60,
maxWidth: 120, // 限制最大宽度避免溢出
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.tune,
size: 14, // 减小图标尺寸
color: WebTheme.getSecondaryTextColor(context),
),
const SizedBox(width: 4),
Flexible( // 使用 Flexible 而不是固定宽度
child: Text(
displayText,
style: WebTheme.labelSmall.copyWith(
fontWeight: FontWeight.w600,
color: WebTheme.getSecondaryTextColor(context),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 2),
Icon(
Icons.keyboard_arrow_down,
size: 12, // 减小图标尺寸
color: WebTheme.getSecondaryTextColor(context),
),
],
),
),
),
),
);
}
}