Files
xianyan/lib/features/mine/settings/presentation/plugin/translate_plugin_page.dart
Developer ae1df22732 feat: v6.10.3 多语言翻译补全 + 17项功能修复
- 引导页协议多语言支持(languageId传递)
- 登录页双书名号修复 + 注册页协议勾选
- 个人中心页面多语言(18个翻译键)
- 网络断开提示增加关闭/刷新按钮
- 了解我们:新增秋叶qy开发者 + ayk签名修改 + 贡献者精简 + 微风暴微信搜索
- iOS快捷按钮重复修复(删除Info.plist静态定义)
- 测试账号123456警告提示
- 扫码登录自动跳转(HTTP轮询+WebSocket双通道)
- 登录页老用户按钮改次要色
- Syncfusion图表崩溃修复(DeferredBuilder+animationDuration:0)
- macOS标题栏跟随软件夜间模式
- 平台兼容分发渠道弹窗
- 软件著作权图片+交叉水印
- 桌面小部件平台兼容说明默认收起
- iOS/macOS图标更新+名称确认为闲言
- 12个语言文件补全roleNative+7个分发渠道翻译字段
2026-06-02 04:50:32 +08:00

1513 lines
50 KiB
Dart

// ============================================================
// 闲言APP — 翻译守护插件详情页
// 创建时间: 2026-05-25
// 更新时间: 2026-05-25
// 作用: 翻译守护插件配置、使用预览和记录
// 上次更新: 新增插件健康状态指示器
// ============================================================
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
import 'package:xianyan/core/theme/app_theme.dart';
import 'package:xianyan/core/theme/app_spacing.dart';
import 'package:xianyan/core/theme/app_radius.dart';
import 'package:xianyan/core/theme/app_typography.dart';
import 'package:xianyan/core/storage/database/app_database.dart';
import 'package:xianyan/features/mine/settings/providers/plugin_provider.dart';
import 'package:xianyan/features/mine/settings/providers/translate_record_provider.dart';
import 'package:xianyan/features/discover/models/translate_language.dart';
import 'package:xianyan/shared/widgets/containers/deferred_builder.dart';
import 'package:xianyan/shared/widgets/containers/glass_container.dart';
import 'package:xianyan/shared/widgets/feedback/app_toast.dart';
import 'package:xianyan/shared/widgets/adaptive/adaptive_back_button.dart';
import 'package:xianyan/features/mine/settings/services/plugin_health_service.dart';
// ============================================================
// 翻译引擎名称映射
// ============================================================
const Map<String, String> _engineNames = {
'bing': 'Bing Translator',
'google': 'Google Translate',
'mymemory': 'MyMemory API',
'libre': 'LibreTranslate',
'appworlds': 'AppWorlds 翻译',
};
const Map<String, String> _engineEmoji = {
'bing': '🅱️',
'google': '🔍',
'mymemory': '💾',
'libre': '📖',
'appworlds': '🌐',
};
const List<String> _fallbackChain = [
'bing',
'mymemory',
'appworlds',
'google',
'libre',
];
// ============================================================
// 翻译守护插件详情页
// ============================================================
class TranslatePluginPage extends ConsumerStatefulWidget {
const TranslatePluginPage({super.key});
@override
ConsumerState<TranslatePluginPage> createState() =>
_TranslatePluginPageState();
}
class _TranslatePluginPageState extends ConsumerState<TranslatePluginPage> {
bool _previewTranslating = false;
bool _previewTranslated = false;
Map<String, int> _trendData = {};
List<HealthCheckResult> _healthResults = [];
@override
void initState() {
super.initState();
Future.microtask(() {
_loadTrendData();
_checkHealth();
}).catchError((_) {});
}
/// 检查翻译插件健康状态
Future<void> _checkHealth() async {
final results = await PluginHealthService.instance.checkTranslateHealth();
if (mounted) {
setState(() => _healthResults = results);
}
}
Future<void> _loadTrendData() async {
final data = await ref
.read(translateRecordListProvider.notifier)
.getCountByDate();
if (mounted) {
setState(() => _trendData = data);
}
}
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
final pluginState = ref.watch(pluginProvider);
final records = ref.watch(translateRecordListProvider);
return CupertinoPageScaffold(
backgroundColor: ext.bgPrimary,
navigationBar: CupertinoNavigationBar(
leading: const AdaptiveBackButton(),
previousPageTitle: '插件',
middle: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(CupertinoIcons.globe, size: 18, color: ext.accent),
const SizedBox(width: 6),
Text(
'翻译守护',
style: AppTypography.title3.copyWith(color: ext.textPrimary),
),
],
),
backgroundColor: ext.bgPrimary.withValues(alpha: 0.85),
border: null,
),
child: SafeArea(
bottom: false,
child: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.md,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(ext),
const SizedBox(height: AppSpacing.md),
_buildPreviewBanner(ext),
const SizedBox(height: AppSpacing.lg),
_buildEnableToggle(ext, pluginState),
const SizedBox(height: AppSpacing.md),
_buildHealthStatus(ext),
const SizedBox(height: AppSpacing.lg),
_buildUsagePreviewSection(ext),
const SizedBox(height: AppSpacing.lg),
_buildTranslateSettingsSection(ext, pluginState),
const SizedBox(height: AppSpacing.lg),
_buildDisplaySettingsSection(ext, pluginState),
const SizedBox(height: AppSpacing.lg),
_buildRecordsSection(ext, pluginState, records),
const SizedBox(height: AppSpacing.lg),
_buildScenariosSection(ext),
const SizedBox(height: AppSpacing.lg),
_buildFeedbackSection(ext),
const SizedBox(height: AppSpacing.xl),
],
),
),
),
],
),
),
);
}
// ============================================================
// 头部 — 图标 + 名称 + 版本
// ============================================================
Widget _buildHeader(AppThemeExtension ext) {
return Center(
child: Column(
children: [
Container(
width: 72,
height: 72,
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF6C63FF), Color(0xFF3B82F6)],
),
borderRadius: AppRadius.xlBorder,
boxShadow: [
BoxShadow(
color: const Color(0xFF6C63FF).withValues(alpha: 0.35),
blurRadius: 16,
offset: const Offset(0, 6),
),
],
),
child: const Center(
child: Icon(
CupertinoIcons.globe,
size: 36,
color: CupertinoColors.white,
),
),
),
const SizedBox(height: AppSpacing.md),
Text(
'翻译守护',
style: AppTypography.title2.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: AppSpacing.xs),
Text(
'v1.0.0 · 翻译增强插件',
style: AppTypography.footnote.copyWith(color: ext.textHint),
),
],
),
);
}
// ============================================================
// 预览横幅 — 渐变背景 + 标语
// ============================================================
Widget _buildPreviewBanner(AppThemeExtension ext) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.lg,
vertical: AppSpacing.lg,
),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
const Color(0xFF6C63FF).withValues(alpha: 0.15),
const Color(0xFF3B82F6).withValues(alpha: 0.15),
const Color(0xFF06B6D4).withValues(alpha: 0.10),
],
),
borderRadius: AppRadius.xlBorder,
border: Border.all(
color: const Color(0xFF6C63FF).withValues(alpha: 0.15),
),
),
child: Column(
children: [
Text(
'🌍 一键翻译,跨越语言',
style: AppTypography.headline.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w700,
),
textAlign: TextAlign.center,
),
const SizedBox(height: AppSpacing.sm),
Text(
'支持30+语言 · 自动检测 · 多引擎降级',
style: AppTypography.subhead.copyWith(color: ext.textSecondary),
textAlign: TextAlign.center,
),
],
),
);
}
// ============================================================
// 启用开关行
// ============================================================
Widget _buildEnableToggle(AppThemeExtension ext, PluginState pluginState) {
return GlassContainer(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.md,
),
child: Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color:
(pluginState.translateEnabled
? ext.successColor
: ext.textHint)
.withValues(alpha: 0.12),
borderRadius: AppRadius.mdBorder,
),
child: Center(
child: Icon(
pluginState.translateEnabled
? CupertinoIcons.checkmark_seal_fill
: CupertinoIcons.checkmark_seal,
size: 18,
color: pluginState.translateEnabled
? ext.successColor
: ext.textHint,
),
),
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'启用翻译守护',
style: AppTypography.subhead.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 2),
Text(
pluginState.translateEnabled
? '翻译守护已开启,将自动为内容提供翻译'
: '开启后可在卡片和列表中快速翻译',
style: AppTypography.caption1.copyWith(color: ext.textHint),
),
],
),
),
CupertinoSwitch(
value: pluginState.translateEnabled,
onChanged: (v) {
ref.read(pluginProvider.notifier).setTranslateEnabled(v);
},
activeTrackColor: ext.successColor,
),
],
),
);
}
// ============================================================
// 健康状态指示器
// ============================================================
/// 插件健康状态指示器
Widget _buildHealthStatus(AppThemeExtension ext) {
if (_healthResults.isEmpty) return const SizedBox.shrink();
final overall = PluginHealthService.instance.getOverallStatus(
_healthResults,
);
final icon = PluginHealthService.instance.getStatusIcon(overall);
final label = PluginHealthService.instance.getStatusLabel(overall);
return Container(
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
decoration: BoxDecoration(
color: overall == HealthStatus.healthy
? ext.successColor.withValues(alpha: 0.08)
: ext.destructiveColor.withValues(alpha: 0.08),
borderRadius: AppRadius.mdBorder,
),
child: Row(
children: [
Text(icon, style: const TextStyle(fontSize: 14)),
const SizedBox(width: AppSpacing.xs),
Text(
'插件状态: $label',
style: AppTypography.footnote.copyWith(
color: overall == HealthStatus.healthy
? ext.successColor
: ext.destructiveColor,
fontWeight: FontWeight.w600,
),
),
const Spacer(),
GestureDetector(
onTap: _checkHealth,
child: Icon(CupertinoIcons.refresh, size: 16, color: ext.textHint),
),
],
),
);
}
// ============================================================
// 使用预览区 — 模拟翻译卡片
// ============================================================
Widget _buildUsagePreviewSection(AppThemeExtension ext) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionHeader(ext, '👁 使用预览'),
const SizedBox(height: AppSpacing.sm),
GlassContainer(
padding: EdgeInsets.zero,
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(AppSpacing.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: ext.accent.withValues(alpha: 0.12),
borderRadius: AppRadius.smBorder,
),
child: const Center(
child: Text('🃏', style: TextStyle(fontSize: 14)),
),
),
const SizedBox(width: AppSpacing.sm),
Text(
'每日推荐卡片',
style: AppTypography.caption1.copyWith(
color: ext.textHint,
),
),
],
),
const SizedBox(height: AppSpacing.md),
Text(
'The only way to do great work is to love what you do.',
style: AppTypography.body.copyWith(
color: ext.textPrimary,
height: 1.5,
),
),
const SizedBox(height: AppSpacing.md),
if (_previewTranslated)
_buildPreviewTranslationResult(ext)
else
_buildPreviewTranslateButton(ext),
],
),
),
],
),
),
],
);
}
Widget _buildPreviewTranslateButton(AppThemeExtension ext) {
return GestureDetector(
onTap: _simulatePreviewTranslate,
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm + 2,
),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
const Color(0xFF6C63FF).withValues(alpha: 0.12),
const Color(0xFF3B82F6).withValues(alpha: 0.12),
],
),
borderRadius: AppRadius.mdBorder,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_previewTranslating
? CupertinoActivityIndicator(color: ext.accent, radius: 8)
: Icon(CupertinoIcons.globe, size: 16, color: ext.accent),
const SizedBox(width: AppSpacing.xs),
Text(
_previewTranslating ? '翻译中…' : '点击翻译',
style: AppTypography.subhead.copyWith(
color: ext.accent,
fontWeight: FontWeight.w600,
),
),
],
),
),
);
}
Widget _buildPreviewTranslationResult(AppThemeExtension ext) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
const Color(0xFF6C63FF).withValues(alpha: 0.06),
const Color(0xFF3B82F6).withValues(alpha: 0.06),
],
),
borderRadius: AppRadius.mdBorder,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
CupertinoIcons.checkmark_circle_fill,
size: 14,
color: ext.successColor,
),
const SizedBox(width: AppSpacing.xs),
Text(
'翻译结果 · Bing',
style: AppTypography.caption2.copyWith(
color: ext.successColor,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: AppSpacing.sm),
Text(
'成就伟大工作的唯一途径,就是热爱你所做的事。',
style: AppTypography.body.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w500,
height: 1.5,
),
),
],
),
);
}
Future<void> _simulatePreviewTranslate() async {
setState(() => _previewTranslating = true);
await Future<void>.delayed(const Duration(milliseconds: 1200));
if (mounted) {
setState(() {
_previewTranslating = false;
_previewTranslated = true;
});
}
}
// ============================================================
// 翻译设置区
// ============================================================
Widget _buildTranslateSettingsSection(
AppThemeExtension ext,
PluginState pluginState,
) {
final currentEngineName =
_engineNames[pluginState.translateEngineId] ?? 'Bing Translator';
final currentEngineEmoji =
_engineEmoji[pluginState.translateEngineId] ?? '🌐';
final targetLang = TranslateLanguage.findByCode(
pluginState.translateTargetLang,
);
final targetLangDisplay = targetLang != null
? '${targetLang.flag} ${targetLang.name}'
: pluginState.translateTargetLang;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionHeader(ext, '⚙️ 翻译设置'),
const SizedBox(height: AppSpacing.sm),
GlassContainer(
padding: EdgeInsets.zero,
child: Column(
children: [
_buildNavigationRow(
ext,
icon: CupertinoIcons.gear_solid,
iconBg: ext.accent.withValues(alpha: 0.12),
iconColor: ext.accent,
emoji: currentEngineEmoji,
title: '翻译引擎',
trailing: Text(
currentEngineName,
style: AppTypography.caption1.copyWith(color: ext.textHint),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
onTap: () => _showEnginePicker(ext, pluginState),
),
_buildDivider(ext),
_buildNavigationRow(
ext,
icon: CupertinoIcons.flag_fill,
iconBg: const Color(0xFF06B6D4).withValues(alpha: 0.12),
iconColor: const Color(0xFF06B6D4),
title: '目标语言',
trailing: Text(
targetLangDisplay,
style: AppTypography.caption1.copyWith(color: ext.textHint),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
onTap: () => _showLanguagePicker(ext, pluginState),
),
_buildDivider(ext),
_buildToggleRow(
ext,
icon: CupertinoIcons.search,
iconBg: ext.successColor.withValues(alpha: 0.12),
iconColor: ext.successColor,
title: '自动检测源语言',
subtitle: '自动识别输入文本的语言',
value: pluginState.autoDetectLang,
onChanged: (v) =>
ref.read(pluginProvider.notifier).setAutoDetectLang(v),
),
_buildDivider(ext),
_buildNavigationRow(
ext,
icon: CupertinoIcons.arrow_2_circlepath,
iconBg: ext.warningColor.withValues(alpha: 0.12),
iconColor: ext.warningColor,
title: '降级策略',
trailing: _buildFallbackChainDisplay(ext),
onTap: () => _showFallbackInfo(ext),
),
],
),
),
],
);
}
Widget _buildFallbackChainDisplay(AppThemeExtension ext) {
return Row(
mainAxisSize: MainAxisSize.min,
children: _fallbackChain.map((id) {
final emoji = _engineEmoji[id] ?? '🔗';
return Padding(
padding: const EdgeInsets.only(right: 2),
child: Text(emoji, style: const TextStyle(fontSize: 12)),
);
}).toList(),
);
}
// ============================================================
// 显示设置区
// ============================================================
Widget _buildDisplaySettingsSection(
AppThemeExtension ext,
PluginState pluginState,
) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionHeader(ext, '🖥 显示设置'),
const SizedBox(height: AppSpacing.sm),
GlassContainer(
padding: EdgeInsets.zero,
child: Column(
children: [
_buildToggleRow(
ext,
icon: CupertinoIcons.bolt_fill,
iconBg: ext.accent.withValues(alpha: 0.12),
iconColor: ext.accent,
title: '自动翻译',
subtitle: '滑动卡片时自动翻译内容',
value: pluginState.autoTranslate,
onChanged: (v) =>
ref.read(pluginProvider.notifier).setAutoTranslate(v),
),
],
),
),
],
);
}
// ============================================================
// 翻译记录区
// ============================================================
Widget _buildRecordsSection(
AppThemeExtension ext,
PluginState pluginState,
List<TranslateRecord> records,
) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionHeader(ext, '📊 翻译记录'),
const SizedBox(height: AppSpacing.sm),
GlassContainer(
padding: EdgeInsets.zero,
child: Column(
children: [
_buildStatRow(ext, pluginState.translateCount),
_buildDivider(ext),
_buildTrendChart(ext),
_buildDivider(ext),
_buildRecentRecords(ext, records),
_buildDivider(ext),
_buildDestructiveRow(
ext,
icon: CupertinoIcons.trash,
title: '清除翻译记录',
onTap: () => _showClearConfirm(ext),
),
],
),
),
],
);
}
Widget _buildStatRow(AppThemeExtension ext, int count) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.md,
),
child: Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: ext.accent.withValues(alpha: 0.12),
borderRadius: AppRadius.mdBorder,
),
child: Center(
child: Icon(
CupertinoIcons.chart_bar_fill,
size: 16,
color: ext.accent,
),
),
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Text(
'累计翻译',
style: AppTypography.subhead.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w500,
),
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: AppSpacing.xs,
),
decoration: BoxDecoration(
color: ext.accent.withValues(alpha: 0.1),
borderRadius: AppRadius.smBorder,
),
child: Text(
'$count',
style: AppTypography.callout.copyWith(
color: ext.accent,
fontWeight: FontWeight.w700,
),
),
),
],
),
);
}
Widget _buildTrendChart(AppThemeExtension ext) {
final now = DateTime.now();
final rawValues = List.generate(7, (i) {
final date = now.subtract(Duration(days: 6 - i));
final key =
'${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
return _trendData[key]?.toDouble() ?? 0.0;
});
final dayLabels = List.generate(7, (i) {
final date = now.subtract(Duration(days: 6 - i));
const weekDays = ['', '', '', '', '', '', ''];
return weekDays[date.weekday - 1];
});
final maxVal = rawValues.isEmpty
? 10.0
: (rawValues.reduce((a, b) => a > b ? a : b) * 1.2).clamp(
1.0,
double.infinity,
);
final barData = List.generate(7, (i) {
final date = now.subtract(Duration(days: 6 - i));
final key =
'${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
return _TranslateDayData(
dayLabels[i],
_trendData[key]?.toDouble() ?? 0.0,
);
});
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.sm),
child: Text(
'最近7天翻译趋势',
style: AppTypography.caption1.copyWith(
color: ext.textHint,
fontWeight: FontWeight.w600,
),
),
),
SizedBox(
height: 120,
child: DeferredBuilder(builder: (context) => SfCartesianChart(
plotAreaBorderWidth: 0,
primaryXAxis: CategoryAxis(
majorGridLines: const MajorGridLines(width: 0),
labelStyle: AppTypography.caption2.copyWith(
color: ext.textHint,
),
),
primaryYAxis: NumericAxis(
maximum: maxVal,
isVisible: false,
majorGridLines: const MajorGridLines(width: 0),
),
tooltipBehavior: TooltipBehavior(),
series: <CartesianSeries<_TranslateDayData, String>>[
ColumnSeries<_TranslateDayData, String>(
dataSource: barData,
xValueMapper: (_TranslateDayData d, _) => d.day,
yValueMapper: (_TranslateDayData d, _) => d.count,
color: ext.accent,
width: 0.6,
animationDuration: 0,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(AppRadius.sm),
topRight: Radius.circular(AppRadius.sm),
),
),
],
),
)),
],
),
);
}
Widget _buildRecentRecords(
AppThemeExtension ext,
List<TranslateRecord> records,
) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.sm),
child: Text(
'最近翻译',
style: AppTypography.caption1.copyWith(
color: ext.textHint,
fontWeight: FontWeight.w600,
),
),
),
if (records.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: AppSpacing.md),
child: Center(
child: Text(
'暂无翻译记录',
style: AppTypography.caption1.copyWith(color: ext.textHint),
),
),
)
else
...records.take(10).map((record) => _buildRecordItem(ext, record)),
],
),
);
}
Widget _buildRecordItem(AppThemeExtension ext, TranslateRecord record) {
final engineEmoji = _engineEmoji[record.engineId] ?? '🌐';
return Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.sm),
child: Slidable(
endActionPane: ActionPane(
motion: const ScrollMotion(),
extentRatio: 0.25,
children: [
SlidableAction(
onPressed: (_) {
ref
.read(translateRecordListProvider.notifier)
.deleteRecord(record.id);
AppToast.showSuccess('已删除记录');
},
backgroundColor: ext.destructiveColor,
foregroundColor: CupertinoColors.white,
icon: CupertinoIcons.delete,
label: '删除',
borderRadius: AppRadius.mdBorder,
),
],
),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: AppSpacing.sm,
),
decoration: BoxDecoration(
color: ext.bgSecondary,
borderRadius: AppRadius.mdBorder,
),
child: Row(
children: [
Text(engineEmoji, style: const TextStyle(fontSize: 14)),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
record.sourceText,
style: AppTypography.caption1.copyWith(
color: ext.textSecondary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
record.translatedText,
style: AppTypography.caption1.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
const SizedBox(width: AppSpacing.xs),
Text(
_formatRelativeTime(record.createdAt),
style: AppTypography.caption2.copyWith(color: ext.textHint),
),
],
),
),
),
);
}
// ============================================================
// 使用场景区
// ============================================================
Widget _buildScenariosSection(AppThemeExtension ext) {
final scenarios = [
{'icon': '🃏', 'label': '每日推荐卡片', 'enabled': true},
{'icon': '📖', 'label': '句子广场列表', 'enabled': true},
{'icon': '📝', 'label': '笔记编辑', 'enabled': false},
{'icon': '🔍', 'label': '搜索结果', 'enabled': false},
];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionHeader(ext, '🎯 使用场景'),
const SizedBox(height: AppSpacing.sm),
Wrap(
spacing: AppSpacing.sm,
runSpacing: AppSpacing.sm,
children: scenarios.map((s) {
final isEnabled = s['enabled'] as bool;
final icon = s['icon'] as String;
final label = s['label'] as String;
return Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
decoration: BoxDecoration(
color: isEnabled
? ext.accent.withValues(alpha: 0.1)
: ext.bgSecondary,
borderRadius: AppRadius.pillBorder,
border: isEnabled
? Border.all(color: ext.accent.withValues(alpha: 0.25))
: null,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(icon, style: const TextStyle(fontSize: 14)),
const SizedBox(width: AppSpacing.xs),
Text(
'$label${isEnabled ? '' : ' (即将支持)'}',
style: AppTypography.caption1.copyWith(
color: isEnabled ? ext.accent : ext.textHint,
fontWeight: isEnabled
? FontWeight.w600
: FontWeight.normal,
),
),
],
),
);
}).toList(),
),
],
);
}
// ============================================================
// 反馈与建议区
// ============================================================
Widget _buildFeedbackSection(AppThemeExtension ext) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionHeader(ext, '💬 反馈与建议'),
const SizedBox(height: AppSpacing.sm),
GlassContainer(
padding: EdgeInsets.zero,
child: _buildNavigationRow(
ext,
icon: CupertinoIcons.chat_bubble_2_fill,
iconBg: ext.accent.withValues(alpha: 0.12),
iconColor: ext.accent,
title: '提交意见',
onTap: () {
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: const Text('提示'),
content: const Text('当前时段不支持提交'),
actions: [
CupertinoDialogAction(
isDefaultAction: true,
child: const Text('好的'),
onPressed: () => Navigator.pop(ctx),
),
],
),
);
},
),
),
],
);
}
// ============================================================
// 通用组件 — 区块标题
// ============================================================
Widget _buildSectionHeader(AppThemeExtension ext, String title) {
return Padding(
padding: const EdgeInsets.only(left: AppSpacing.xs),
child: Text(
title,
style: AppTypography.subhead.copyWith(
color: ext.textSecondary,
fontWeight: FontWeight.w600,
),
),
);
}
// ============================================================
// 通用组件 — 导航行
// ============================================================
Widget _buildNavigationRow(
AppThemeExtension ext, {
required IconData icon,
required Color iconBg,
required Color iconColor,
String? emoji,
required String title,
Widget? trailing,
VoidCallback? onTap,
}) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.md,
),
child: Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: iconBg,
borderRadius: AppRadius.mdBorder,
),
child: Center(
child: emoji != null
? Text(emoji, style: const TextStyle(fontSize: 16))
: Icon(icon, size: 16, color: iconColor),
),
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Text(
title,
style: AppTypography.subhead.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w500,
),
),
),
if (trailing != null) trailing,
const SizedBox(width: AppSpacing.xs),
Icon(CupertinoIcons.chevron_right, size: 14, color: ext.textHint),
],
),
),
);
}
// ============================================================
// 通用组件 — 开关行
// ============================================================
Widget _buildToggleRow(
AppThemeExtension ext, {
required IconData icon,
required Color iconBg,
required Color iconColor,
required String title,
String? subtitle,
required bool value,
required ValueChanged<bool> onChanged,
}) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm + 2,
),
child: Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: iconBg,
borderRadius: AppRadius.mdBorder,
),
child: Center(child: Icon(icon, size: 16, color: iconColor)),
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: AppTypography.subhead.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w500,
),
),
if (subtitle != null)
Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(
subtitle,
style: AppTypography.caption1.copyWith(
color: ext.textHint,
),
),
),
],
),
),
CupertinoSwitch(
value: value,
onChanged: onChanged,
activeTrackColor: ext.successColor,
),
],
),
);
}
// ============================================================
// 通用组件 — 危险操作行
// ============================================================
Widget _buildDestructiveRow(
AppThemeExtension ext, {
required IconData icon,
required String title,
VoidCallback? onTap,
}) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.md,
),
child: Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: ext.destructiveColor.withValues(alpha: 0.12),
borderRadius: AppRadius.mdBorder,
),
child: Center(
child: Icon(icon, size: 16, color: ext.destructiveColor),
),
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Text(
title,
style: AppTypography.subhead.copyWith(
color: ext.destructiveColor,
fontWeight: FontWeight.w500,
),
),
),
Icon(CupertinoIcons.chevron_right, size: 14, color: ext.textHint),
],
),
),
);
}
// ============================================================
// 通用组件 — 分割线
// ============================================================
Widget _buildDivider(AppThemeExtension ext) {
return Padding(
padding: const EdgeInsets.only(left: 60),
child: Divider(height: 0.5, color: ext.overlaySubtle),
);
}
// ============================================================
// 引擎选择弹窗
// ============================================================
void _showEnginePicker(AppThemeExtension ext, PluginState pluginState) {
showCupertinoModalPopup<void>(
context: context,
builder: (ctx) => CupertinoActionSheet(
title: const Text('选择翻译引擎'),
actions: _engineNames.entries.map((entry) {
final isSelected = pluginState.translateEngineId == entry.key;
return CupertinoActionSheetAction(
onPressed: () {
Navigator.pop(ctx);
ref.read(pluginProvider.notifier).setTranslateEngineId(entry.key);
AppToast.showSuccess('已切换至 ${entry.value}');
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('${_engineEmoji[entry.key] ?? "🔗"} ${entry.value}'),
if (isSelected) ...[
const SizedBox(width: 8),
Icon(
CupertinoIcons.checkmark_circle_fill,
size: 16,
color: ext.successColor,
),
],
],
),
);
}).toList(),
cancelButton: CupertinoActionSheetAction(
isDestructiveAction: true,
child: const Text('取消'),
onPressed: () => Navigator.pop(ctx),
),
),
);
}
// ============================================================
// 目标语言选择弹窗
// ============================================================
void _showLanguagePicker(AppThemeExtension ext, PluginState pluginState) {
final popularLangs = TranslateLanguage.popular;
final otherLangs = TranslateLanguage.supported
.where((l) => !l.isPopular)
.toList();
showCupertinoModalPopup<void>(
context: context,
builder: (ctx) => CupertinoActionSheet(
title: const Text('选择目标语言'),
actions: [
CupertinoActionSheetAction(
child: const Text('🔍 自动检测'),
onPressed: () {
Navigator.pop(ctx);
ref.read(pluginProvider.notifier).setTranslateTargetLang('auto');
},
),
...popularLangs.map((lang) {
final isSelected = pluginState.translateTargetLang == lang.code;
return CupertinoActionSheetAction(
onPressed: () {
Navigator.pop(ctx);
ref
.read(pluginProvider.notifier)
.setTranslateTargetLang(lang.code);
AppToast.showSuccess('目标语言: ${lang.flag} ${lang.name}');
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('${lang.flag} ${lang.name}'),
if (isSelected) ...[
const SizedBox(width: 8),
Icon(
CupertinoIcons.checkmark_circle_fill,
size: 16,
color: ext.successColor,
),
],
],
),
);
}),
...otherLangs.map((lang) {
final isSelected = pluginState.translateTargetLang == lang.code;
return CupertinoActionSheetAction(
onPressed: () {
Navigator.pop(ctx);
ref
.read(pluginProvider.notifier)
.setTranslateTargetLang(lang.code);
AppToast.showSuccess('目标语言: ${lang.flag} ${lang.name}');
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('${lang.flag} ${lang.name}'),
if (isSelected) ...[
const SizedBox(width: 8),
Icon(
CupertinoIcons.checkmark_circle_fill,
size: 16,
color: ext.successColor,
),
],
],
),
);
}),
],
cancelButton: CupertinoActionSheetAction(
isDestructiveAction: true,
child: const Text('取消'),
onPressed: () => Navigator.pop(ctx),
),
),
);
}
// ============================================================
// 降级策略说明弹窗
// ============================================================
void _showFallbackInfo(AppThemeExtension ext) {
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: const Text('🔄 降级策略'),
content: Column(
children: [
const SizedBox(height: 12),
Text(
'当首选翻译引擎不可用时,将按以下顺序自动切换:',
style: AppTypography.footnote.copyWith(color: ext.textSecondary),
),
const SizedBox(height: AppSpacing.md),
..._fallbackChain.asMap().entries.map((entry) {
final idx = entry.key;
final engineId = entry.value;
final name = _engineNames[engineId] ?? engineId;
final emoji = _engineEmoji[engineId] ?? '🔗';
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Container(
width: 22,
height: 22,
decoration: BoxDecoration(
color: ext.accent.withValues(alpha: 0.1),
borderRadius: AppRadius.xsBorder,
),
child: Center(
child: Text(
'${idx + 1}',
style: AppTypography.caption2.copyWith(
color: ext.accent,
fontWeight: FontWeight.w700,
),
),
),
),
const SizedBox(width: AppSpacing.sm),
Text(emoji, style: const TextStyle(fontSize: 16)),
const SizedBox(width: AppSpacing.xs),
Text(
name,
style: AppTypography.subhead.copyWith(
color: ext.textPrimary,
),
),
],
),
);
}),
],
),
actions: [
CupertinoDialogAction(
isDefaultAction: true,
child: const Text('知道了'),
onPressed: () => Navigator.pop(ctx),
),
],
),
);
}
// ============================================================
// 清除记录确认弹窗
// ============================================================
void _showClearConfirm(AppThemeExtension ext) {
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: const Text('清除翻译记录'),
content: const Text('确定要清除所有翻译使用记录吗?此操作不可恢复。'),
actions: [
CupertinoDialogAction(
child: const Text('取消'),
onPressed: () => Navigator.pop(ctx),
),
CupertinoDialogAction(
isDestructiveAction: true,
child: const Text('清除'),
onPressed: () {
Navigator.pop(ctx);
ref.read(translateRecordListProvider.notifier).clearAll();
ref.read(pluginProvider.notifier).clearTranslateRecords();
_loadTrendData();
AppToast.showSuccess('翻译记录已清除');
},
),
],
),
);
}
// ============================================================
// 相对时间格式化
// ============================================================
String _formatRelativeTime(int createdAtMs) {
final diff = DateTime.now().millisecondsSinceEpoch - createdAtMs;
if (diff < 60000) return '刚刚';
if (diff < 3600000) return '${diff ~/ 60000}分钟前';
if (diff < 86400000) return '${diff ~/ 3600000}小时前';
return '${diff ~/ 86400000}天前';
}
}
class _TranslateDayData {
final String day;
final double count;
const _TranslateDayData(this.day, this.count);
}