Files
xianyan/lib/features/widget/presentation/widget_management_page.dart
Developer f9c19463f9 chore: 批量更新v6.5.21版本,整合多项功能修复与优化
主要变更:
1. 新增多风格音效资源与管理文档
2. 修复翻译服务空响应处理与Dio日志异常捕获
3. 完善Web端平台适配与路径获取Stub
4. 优化设备配对与文件传输功能
5. 新增角色命名常量与摇一摇检测器
6. 修复Riverpod dispose与鸿蒙导航路由
7. 新增每日通知服务与流体着色器
8. 优化备份服务与数据管理页面
9. 新增隐私设置附近设备发现选项
10. 重构诗词提供者支持历史记录
11. 完善桌面端构建配置与开发脚本
12. 清理旧版工具部署脚本
2026-05-21 00:19:14 +08:00

680 lines
21 KiB
Dart
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
/// ============================================================
/// 闲言APP — 桌面小部件管理页面
/// 创建时间: 2026-05-19
/// 更新时间: 2026-05-19
/// 作用: 管理桌面小部件的安装、数据推送、主题和平台兼容说明
/// 上次更新: 修复Android/鸿蒙端添加小部件无反应改用requestPinWidget+手动指引
/// ============================================================
import 'package:flutter/cupertino.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/theme/app_spacing.dart';
import '../../../core/theme/app_typography.dart';
import '../../../core/theme/app_radius.dart';
import '../../../core/utils/platform_helper.dart';
import '../../../shared/widgets/glass_container.dart';
import '../../../shared/widgets/responsive_layout.dart';
import '../../../shared/widgets/app_toast.dart';
import '../models/widget_type.dart';
import '../providers/widget_provider.dart' as wp;
class WidgetManagementPage extends ConsumerStatefulWidget {
const WidgetManagementPage({super.key});
@override
ConsumerState<WidgetManagementPage> createState() =>
_WidgetManagementPageState();
}
class _WidgetManagementPageState extends ConsumerState<WidgetManagementPage> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(wp.widgetProvider.notifier).loadInstalledWidgets();
});
}
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
final wp.WidgetState widgetState = ref.watch(wp.widgetProvider);
final grouped = <int, List<WidgetType>>{};
for (final t in WidgetTypeX.all) {
grouped.putIfAbsent(t.priority, () => []).add(t);
}
return CupertinoPageScaffold(
backgroundColor: ext.bgPrimary,
child: ResponsiveMaxWidth(
maxWidth: 900,
child: SafeArea(
bottom: false,
child: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
child: Row(
children: [
GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: ext.bgSecondary,
borderRadius: AppRadius.mdBorder,
),
child: Icon(
CupertinoIcons.back,
size: 18,
color: ext.accent,
),
),
),
const SizedBox(width: AppSpacing.sm),
Text(
'桌面小部件',
style: AppTypography.title1.copyWith(
color: ext.textPrimary,
),
),
const Spacer(),
_ThemeToggle(ext: ext),
],
),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
child: _PlatformCompatibilityCard(ext: ext),
),
),
for (final entry in grouped.entries) ...[
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(
AppSpacing.md,
AppSpacing.md,
AppSpacing.md,
AppSpacing.xs,
),
child: Row(
children: [
_PriorityDot(priority: entry.key),
const SizedBox(width: 6),
Text(
_priorityLabel(entry.key),
style: AppTypography.title3.copyWith(
color: ext.textPrimary,
),
),
],
),
),
),
SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
final type = entry.value[index];
final isInstalled = widgetState.installedWidgets.contains(
type,
);
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.xs,
),
child: _WidgetCard(
ext: ext,
type: type,
isInstalled: isInstalled,
onAdd: () => _handleAddWidget(type),
onPin: () => _handlePinWidget(type),
),
);
}, childCount: entry.value.length),
),
],
if (widgetState.error != null)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(AppSpacing.md),
child: Text(
'错误: ${widgetState.error}',
style: AppTypography.footnote.copyWith(
color: CupertinoColors.systemRed,
),
),
),
),
const SliverToBoxAdapter(child: SizedBox(height: 120)),
],
),
),
),
);
}
String _priorityLabel(int p) => switch (p) {
0 => '核心小部件',
1 => '推荐小部件',
2 => '实用小部件',
_ => '趣味小部件',
};
void _handleAddWidget(WidgetType type) async {
final success = await ref.read(wp.widgetProvider.notifier).requestPinWidget(type);
if (!success) {
if (!mounted) return;
_showManualAddGuide(type);
return;
}
ref.read(wp.widgetProvider.notifier).addWidget(type);
AppToast.showInfo('${type.title} 已添加到桌面');
}
void _handlePinWidget(WidgetType type) async {
final success = await ref.read(wp.widgetProvider.notifier).requestPinWidget(type);
if (!success) {
if (!mounted) return;
_showManualAddGuide(type);
return;
}
ref.read(wp.widgetProvider.notifier).addWidget(type);
}
void _showManualAddGuide(WidgetType type) {
final ext = AppTheme.ext(context);
final steps = PlatformHelper.isHarmonyOS
? [
'1⃣ 长按桌面空白处',
'2⃣ 选择「服务卡片」',
'3⃣ 找到「闲言」',
'4⃣ 选择「${type.title}」并添加到桌面',
]
: PlatformHelper.isAndroid
? [
'1⃣ 长按桌面空白处',
'2⃣ 选择「小部件」',
'3⃣ 找到「闲言」',
'4⃣ 选择「${type.title}」拖动到桌面',
]
: [
'1⃣ 向右滑动到今日视图',
'2⃣ 滚动到底部点击「编辑」',
'3⃣ 找到「闲言」',
'4⃣ 选择「${type.title}」并添加',
];
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.sm),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(CupertinoIcons.square_grid_2x2_fill, size: 20, color: ext.accent),
const SizedBox(width: AppSpacing.xs),
Text('添加「${type.title}'),
],
),
),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'当前设备不支持快捷添加,请按以下步骤手动添加到桌面:',
style: AppTypography.footnote.copyWith(
color: ext.textSecondary,
),
),
const SizedBox(height: AppSpacing.md),
...steps.map((s) => Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.xs),
child: Text(
s,
style: AppTypography.subhead.copyWith(
color: ext.textPrimary,
),
),
)),
],
),
actions: [
CupertinoDialogAction(
isDefaultAction: true,
child: const Text('知道了'),
onPressed: () => Navigator.of(ctx).pop(),
),
],
),
);
}
}
class _ThemeToggle extends StatelessWidget {
const _ThemeToggle({required this.ext});
final AppThemeExtension ext;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
final notifier = ProviderScope.containerOf(
context,
).read(wp.widgetProvider.notifier);
notifier.pushThemeToAllWidgets();
AppToast.showInfo('已推送当前主题到小部件');
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: 4,
),
decoration: BoxDecoration(
color: ext.bgSecondary,
borderRadius: AppRadius.pillBorder,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(CupertinoIcons.paintbrush_fill, size: 14, color: ext.accent),
const SizedBox(width: 4),
Text(
'同步主题',
style: AppTypography.caption1.copyWith(
color: ext.accent,
fontWeight: FontWeight.w600,
),
),
],
),
),
);
}
}
class _PriorityDot extends StatelessWidget {
const _PriorityDot({required this.priority});
final int priority;
@override
Widget build(BuildContext context) {
final color = switch (priority) {
0 => CupertinoColors.systemRed,
1 => CupertinoColors.systemOrange,
2 => CupertinoColors.systemBlue,
_ => CupertinoColors.systemGrey,
};
return Container(
width: 8,
height: 8,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
);
}
}
class _PlatformCompatibilityCard extends StatelessWidget {
const _PlatformCompatibilityCard({required this.ext});
final AppThemeExtension ext;
@override
Widget build(BuildContext context) {
return GlassContainer(
depth: GlassDepth.elevated,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
CupertinoIcons.info_circle_fill,
size: 18,
color: ext.accent,
),
const SizedBox(width: AppSpacing.sm),
Text(
'平台兼容说明',
style: AppTypography.subhead.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: AppSpacing.sm),
_PlatformRow(
ext: ext,
icon: '🤖',
name: 'Android',
desc: '功能完整,支持交互式按钮和快捷固定',
),
const SizedBox(height: 4),
_PlatformRow(
ext: ext,
icon: '🍎',
name: 'iOS',
desc: 'WidgetKit + SwiftUI交互需 iOS 17+',
),
const SizedBox(height: 4),
_PlatformRow(
ext: ext,
icon: '🔴',
name: '鸿蒙',
desc: 'FormExtension + ArkUI无快捷固定无实时刷新',
),
const SizedBox(height: AppSpacing.sm),
Container(
padding: const EdgeInsets.all(AppSpacing.sm),
decoration: BoxDecoration(
color: ext.accent.withValues(alpha: 0.06),
borderRadius: AppRadius.smBorder,
),
child: Row(
children: [
Icon(
CupertinoIcons.lightbulb_fill,
size: 14,
color: ext.accent,
),
const SizedBox(width: AppSpacing.xs),
Expanded(
child: Text(
'点击「同步主题」可将当前深色/浅色模式推送到所有已安装小部件',
style: AppTypography.footnote.copyWith(
color: ext.textSecondary,
),
),
),
],
),
),
],
),
);
}
}
class _PlatformRow extends StatelessWidget {
const _PlatformRow({
required this.ext,
required this.icon,
required this.name,
required this.desc,
});
final AppThemeExtension ext;
final String icon;
final String name;
final String desc;
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(icon, style: const TextStyle(fontSize: 16)),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: RichText(
text: TextSpan(
children: [
TextSpan(
text: '$name ',
style: AppTypography.subhead.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
TextSpan(
text: desc,
style: AppTypography.footnote.copyWith(
color: ext.textSecondary,
),
),
],
),
),
),
],
);
}
}
class _WidgetCard extends StatelessWidget {
const _WidgetCard({
required this.ext,
required this.type,
required this.isInstalled,
required this.onAdd,
required this.onPin,
});
final AppThemeExtension ext;
final WidgetType type;
final bool isInstalled;
final VoidCallback onAdd;
final VoidCallback onPin;
@override
Widget build(BuildContext context) {
return GlassContainer(
depth: GlassDepth.elevated,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: ext.accent.withValues(alpha: 0.12),
borderRadius: AppRadius.mdBorder,
),
child: Icon(type.icon, size: 22, color: ext.accent),
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
type.title,
style: AppTypography.subhead.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: AppSpacing.xs),
_PriorityBadge(priority: type.priority),
],
),
const SizedBox(height: 2),
Text(
type.subtitle,
style: AppTypography.footnote.copyWith(
color: ext.textSecondary,
),
),
],
),
),
const SizedBox(width: AppSpacing.sm),
if (isInstalled)
Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: 4,
),
decoration: BoxDecoration(
color: CupertinoColors.systemGreen.withValues(alpha: 0.15),
borderRadius: AppRadius.pillBorder,
),
child: Text(
'已安装',
style: AppTypography.caption2.copyWith(
color: CupertinoColors.systemGreen,
fontWeight: FontWeight.w600,
),
),
)
else
CupertinoButton(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: 4,
),
minimumSize: Size.zero,
borderRadius: AppRadius.pillBorder,
color: ext.accent,
onPressed: onAdd,
child: Text(
'添加',
style: AppTypography.caption2.copyWith(
color: CupertinoColors.white,
fontWeight: FontWeight.w600,
),
),
),
],
),
const SizedBox(height: AppSpacing.xs),
Row(
children: [
_PlatformBadges(ext: ext, type: type),
const Spacer(),
_DeepLinkBadge(ext: ext, route: type.deepLinkRoute),
],
),
],
),
);
}
}
class _PriorityBadge extends StatelessWidget {
const _PriorityBadge({required this.priority});
final int priority;
@override
Widget build(BuildContext context) {
final label = switch (priority) {
0 => 'P0',
1 => 'P1',
2 => 'P2',
_ => 'P3',
};
final color = switch (priority) {
0 => CupertinoColors.systemRed,
1 => CupertinoColors.systemOrange,
2 => CupertinoColors.systemBlue,
_ => CupertinoColors.systemGrey,
};
return Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
borderRadius: AppRadius.xsBorder,
),
child: Text(
label,
style: AppTypography.caption2.copyWith(
color: color,
fontWeight: FontWeight.w700,
fontSize: 9,
),
),
);
}
}
class _PlatformBadges extends StatelessWidget {
const _PlatformBadges({required this.ext, required this.type});
final AppThemeExtension ext;
final WidgetType type;
@override
Widget build(BuildContext context) {
return Row(
children: [
const _Badge(label: 'Android', supported: true),
const SizedBox(width: 4),
const _Badge(label: 'iOS', supported: true),
const SizedBox(width: 4),
_Badge(label: '鸿蒙', supported: type.supportsOhos),
],
);
}
}
class _Badge extends StatelessWidget {
const _Badge({required this.label, required this.supported});
final String label;
final bool supported;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
decoration: BoxDecoration(
color: supported
? CupertinoColors.systemGreen.withValues(alpha: 0.1)
: CupertinoColors.systemGrey.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(3),
),
child: Text(
supported ? label : '$label',
style: AppTypography.caption2.copyWith(
color: supported
? CupertinoColors.systemGreen
: CupertinoColors.systemGrey,
fontSize: 9,
fontWeight: FontWeight.w500,
),
),
);
}
}
class _DeepLinkBadge extends StatelessWidget {
const _DeepLinkBadge({required this.ext, required this.route});
final AppThemeExtension ext;
final String route;
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(CupertinoIcons.link, size: 10, color: ext.textSecondary),
const SizedBox(width: 2),
Text(
route,
style: AppTypography.caption2.copyWith(
color: ext.textSecondary,
fontSize: 9,
),
),
],
);
}
}