主要变更: 1. 新增多风格音效资源与管理文档 2. 修复翻译服务空响应处理与Dio日志异常捕获 3. 完善Web端平台适配与路径获取Stub 4. 优化设备配对与文件传输功能 5. 新增角色命名常量与摇一摇检测器 6. 修复Riverpod dispose与鸿蒙导航路由 7. 新增每日通知服务与流体着色器 8. 优化备份服务与数据管理页面 9. 新增隐私设置附近设备发现选项 10. 重构诗词提供者支持历史记录 11. 完善桌面端构建配置与开发脚本 12. 清理旧版工具部署脚本
680 lines
21 KiB
Dart
680 lines
21 KiB
Dart
/// ============================================================
|
||
/// 闲言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,
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|