1043 lines
33 KiB
Dart
1043 lines
33 KiB
Dart
/// ============================================================
|
||
/// 闲言APP — 桌面小部件管理页面
|
||
/// 创建时间: 2026-05-19
|
||
/// 更新时间: 2026-06-04
|
||
/// 作用: 管理桌面小部件的安装、数据推送、主题和平台兼容说明
|
||
/// 上次更新: 修复_WidgetDataPreview生命周期问题(framework断言错误),增加disposed保护和超时机制
|
||
/// ============================================================
|
||
|
||
import 'dart:async';
|
||
import 'package:flutter/cupertino.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
|
||
import '../../../core/storage/kv_storage.dart';
|
||
import '../../../core/router/app_nav_extension.dart';
|
||
import '../../../core/router/app_routes.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/services/data/home_widget_service.dart';
|
||
import '../../../core/utils/logger.dart';
|
||
import '../../../core/utils/platform/platform_helper.dart';
|
||
import '../../../shared/widgets/adaptive/adaptive_back_button.dart';
|
||
import '../../../shared/widgets/containers/glass_container.dart';
|
||
import '../../../shared/widgets/adaptive/responsive_layout.dart';
|
||
import '../../../shared/widgets/feedback/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> {
|
||
bool _isAdding = false;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
if (!mounted) return;
|
||
ref.read(wp.widgetProvider.notifier).loadInstalledWidgets();
|
||
_showDevDialogIfNeeded();
|
||
});
|
||
}
|
||
|
||
/// 开发中弹窗提示,支持"不再提醒"
|
||
void _showDevDialogIfNeeded() {
|
||
final dismissed = KvStorage.getBool('widget_dev_dismissed') ?? false;
|
||
if (dismissed) return;
|
||
showCupertinoDialog<void>(
|
||
context: context,
|
||
builder: (ctx) {
|
||
bool dontRemind = false;
|
||
return StatefulBuilder(
|
||
builder: (ctx, setDialogState) => CupertinoAlertDialog(
|
||
title: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(
|
||
CupertinoIcons.hammer,
|
||
size: 20,
|
||
color: CupertinoColors.systemOrange.resolveFrom(ctx),
|
||
),
|
||
const SizedBox(width: 8),
|
||
const Text('Beta'),
|
||
],
|
||
),
|
||
content: Padding(
|
||
padding: const EdgeInsets.only(top: 8),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
const Text(
|
||
'桌面小部件功能正在积极开发中,部分功能可能尚未完善或存在不稳定情况。\n\n'
|
||
'当前支持:基础小部件显示与数据推送\n'
|
||
'即将支持:更多小部件样式、交互操作、跨平台同步',
|
||
),
|
||
const SizedBox(height: 12),
|
||
GestureDetector(
|
||
onTap: () => setDialogState(() => dontRemind = !dontRemind),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(
|
||
dontRemind
|
||
? CupertinoIcons.checkmark_square_fill
|
||
: CupertinoIcons.square,
|
||
size: 20,
|
||
color: dontRemind
|
||
? CupertinoColors.systemGreen.resolveFrom(ctx)
|
||
: CupertinoColors.systemGrey.resolveFrom(ctx),
|
||
),
|
||
const SizedBox(width: 8),
|
||
const Text('不再提醒', style: TextStyle(fontSize: 14)),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
actions: [
|
||
CupertinoDialogAction(
|
||
child: const Text('查看实验功能'),
|
||
onPressed: () {
|
||
Navigator.pop(ctx);
|
||
// 鸿蒙端暂不支持,弹出toast提示
|
||
if (PlatformHelper.isHarmonyOS) {
|
||
AppToast.showInfo('敬请期待');
|
||
return;
|
||
}
|
||
context.appPush(AppRoutes.experimentalFeatures);
|
||
},
|
||
),
|
||
CupertinoDialogAction(
|
||
isDefaultAction: true,
|
||
onPressed: () {
|
||
if (dontRemind) {
|
||
KvStorage.setBool('widget_dev_dismissed', true);
|
||
}
|
||
Navigator.pop(ctx);
|
||
},
|
||
child: const Text('我知道了'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
@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: [
|
||
CupertinoSliverNavigationBar(
|
||
leading: const AdaptiveBackButton(),
|
||
middle: Text(
|
||
'桌面小部件',
|
||
style: AppTypography.title3.copyWith(
|
||
color: ext.textPrimary,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
largeTitle: Text(
|
||
'桌面小部件',
|
||
style: AppTypography.title3.copyWith(
|
||
color: ext.textPrimary,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
trailing: _ThemeToggle(ext: ext),
|
||
backgroundColor: ext.bgPrimary.withValues(alpha: 0.85),
|
||
border: null,
|
||
),
|
||
|
||
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(
|
||
key: ValueKey(type),
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: AppSpacing.md,
|
||
vertical: AppSpacing.xs,
|
||
),
|
||
child: _WidgetCard(
|
||
ext: ext,
|
||
type: type,
|
||
isInstalled: isInstalled,
|
||
isAdding: _isAdding,
|
||
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 {
|
||
// 鸿蒙端点击添加弹出toast提示
|
||
if (PlatformHelper.isHarmonyOS) {
|
||
AppToast.showInfo('敬请期待');
|
||
return;
|
||
}
|
||
if (_isAdding) return;
|
||
_isAdding = true;
|
||
|
||
try {
|
||
final notifier = ref.read(wp.widgetProvider.notifier);
|
||
final wasInstalled = ref
|
||
.read(wp.widgetProvider)
|
||
.installedWidgets
|
||
.contains(type);
|
||
|
||
final result = await notifier.requestPinWidget(type);
|
||
|
||
if (!mounted) return;
|
||
|
||
if (result == wp.PinWidgetResult.unsupported) {
|
||
_showManualAddGuide(type, isUnsupported: true);
|
||
return;
|
||
}
|
||
|
||
if (result == wp.PinWidgetResult.failed) {
|
||
_showManualAddGuide(type, isUnsupported: false);
|
||
return;
|
||
}
|
||
|
||
await Future<void>.delayed(const Duration(seconds: 2));
|
||
|
||
if (!mounted) return;
|
||
await notifier.loadInstalledWidgets();
|
||
|
||
final nowInstalled = ref
|
||
.read(wp.widgetProvider)
|
||
.installedWidgets
|
||
.contains(type);
|
||
if (nowInstalled && !wasInstalled) {
|
||
AppToast.showInfo('${type.title} 已添加到桌面');
|
||
} else if (!nowInstalled) {
|
||
_showManualAddGuide(type, isUnsupported: false);
|
||
}
|
||
} finally {
|
||
_isAdding = false;
|
||
}
|
||
}
|
||
|
||
void _handlePinWidget(WidgetType type) async {
|
||
if (_isAdding) return;
|
||
_isAdding = true;
|
||
|
||
try {
|
||
final notifier = ref.read(wp.widgetProvider.notifier);
|
||
final result = await notifier.requestPinWidget(type);
|
||
|
||
if (!mounted) return;
|
||
|
||
if (result == wp.PinWidgetResult.unsupported) {
|
||
_showManualAddGuide(type, isUnsupported: true);
|
||
return;
|
||
}
|
||
|
||
if (result == wp.PinWidgetResult.failed) {
|
||
_showManualAddGuide(type, isUnsupported: false);
|
||
return;
|
||
}
|
||
|
||
await Future<void>.delayed(const Duration(seconds: 2));
|
||
|
||
if (!mounted) return;
|
||
await notifier.loadInstalledWidgets();
|
||
|
||
final nowInstalled = ref
|
||
.read(wp.widgetProvider)
|
||
.installedWidgets
|
||
.contains(type);
|
||
if (!nowInstalled) {
|
||
_showManualAddGuide(type, isUnsupported: false);
|
||
}
|
||
} finally {
|
||
_isAdding = false;
|
||
}
|
||
}
|
||
|
||
void _showManualAddGuide(WidgetType type, {required bool isUnsupported}) {
|
||
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}」并添加',
|
||
];
|
||
|
||
final guideReason = isUnsupported
|
||
? '当前设备不支持快捷添加,请按以下步骤手动添加到桌面:'
|
||
: '快捷添加失败,请按以下步骤手动添加到桌面:';
|
||
|
||
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(
|
||
guideReason,
|
||
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(
|
||
child: const Text('推送数据'),
|
||
onPressed: () {
|
||
Navigator.of(ctx).pop();
|
||
ref.read(wp.widgetProvider.notifier).pushDataToWidget(type);
|
||
AppToast.showInfo('已推送数据到${type.title}小部件');
|
||
},
|
||
),
|
||
CupertinoDialogAction(
|
||
isDefaultAction: true,
|
||
child: const Text('知道了'),
|
||
onPressed: () => Navigator.of(ctx).pop(),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _ThemeToggle extends ConsumerWidget {
|
||
const _ThemeToggle({required this.ext});
|
||
final AppThemeExtension ext;
|
||
|
||
@override
|
||
Widget build(BuildContext context, WidgetRef ref) {
|
||
return GestureDetector(
|
||
onTap: () {
|
||
ref.read(wp.widgetProvider.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 StatefulWidget {
|
||
const _PlatformCompatibilityCard({required this.ext});
|
||
final AppThemeExtension ext;
|
||
|
||
@override
|
||
State<_PlatformCompatibilityCard> createState() =>
|
||
_PlatformCompatibilityCardState();
|
||
}
|
||
|
||
class _PlatformCompatibilityCardState
|
||
extends State<_PlatformCompatibilityCard> {
|
||
bool _isExpanded = false;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final ext = widget.ext;
|
||
return GlassContainer(
|
||
depth: GlassDepth.elevated,
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
GestureDetector(
|
||
onTap: () => setState(() => _isExpanded = !_isExpanded),
|
||
behavior: HitTestBehavior.opaque,
|
||
child: Row(
|
||
children: [
|
||
Icon(
|
||
CupertinoIcons.info_circle_fill,
|
||
size: 18,
|
||
color: ext.accent,
|
||
),
|
||
const SizedBox(width: AppSpacing.sm),
|
||
Expanded(
|
||
child: Text(
|
||
'平台兼容说明',
|
||
style: AppTypography.subhead.copyWith(
|
||
color: ext.textPrimary,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
),
|
||
AnimatedRotation(
|
||
turns: _isExpanded ? 0.0 : -0.25,
|
||
duration: const Duration(milliseconds: 250),
|
||
curve: Curves.easeInOut,
|
||
child: Icon(
|
||
CupertinoIcons.chevron_down,
|
||
size: 16,
|
||
color: ext.textHint,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
AnimatedCrossFade(
|
||
firstChild: const SizedBox.shrink(key: ValueKey('collapsed')),
|
||
secondChild: Padding(
|
||
key: const ValueKey('expanded'),
|
||
padding: const EdgeInsets.only(top: AppSpacing.sm),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
_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,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
crossFadeState: _isExpanded
|
||
? CrossFadeState.showSecond
|
||
: CrossFadeState.showFirst,
|
||
duration: const Duration(milliseconds: 250),
|
||
sizeCurve: Curves.easeInOut,
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
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.isAdding,
|
||
required this.onAdd,
|
||
required this.onPin,
|
||
});
|
||
final AppThemeExtension ext;
|
||
final WidgetType type;
|
||
final bool isInstalled;
|
||
final bool isAdding;
|
||
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: isAdding ? null : onAdd,
|
||
child: isAdding
|
||
? const CupertinoActivityIndicator(radius: 8)
|
||
: 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),
|
||
],
|
||
),
|
||
const SizedBox(height: AppSpacing.sm),
|
||
_WidgetDataPreview(ext: ext, type: type),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
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,
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class _WidgetDataPreview extends ConsumerStatefulWidget {
|
||
const _WidgetDataPreview({required this.ext, required this.type});
|
||
final AppThemeExtension ext;
|
||
final WidgetType type;
|
||
|
||
@override
|
||
ConsumerState<_WidgetDataPreview> createState() => _WidgetDataPreviewState();
|
||
}
|
||
|
||
class _WidgetDataPreviewState extends ConsumerState<_WidgetDataPreview> {
|
||
Map<String, dynamic> _data = {};
|
||
bool _loading = true;
|
||
bool _disposed = false;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
// 使用addPostFrameCallback确保element完全挂载后再访问ref
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
if (!mounted || _disposed) return;
|
||
_loadPreview();
|
||
});
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_disposed = true;
|
||
super.dispose();
|
||
}
|
||
|
||
Future<void> _loadPreview() async {
|
||
if (_disposed || !mounted) return;
|
||
try {
|
||
final service = ref.read(homeWidgetServiceProvider);
|
||
final data = await service.debugGetAllData().timeout(
|
||
const Duration(seconds: 5),
|
||
);
|
||
if (_disposed || !mounted) return;
|
||
setState(() {
|
||
_data = data;
|
||
_loading = false;
|
||
});
|
||
} on TimeoutException {
|
||
if (_disposed || !mounted) return;
|
||
setState(() => _loading = false);
|
||
Log.w('WidgetDataPreview: 加载预览数据超时');
|
||
} catch (e) {
|
||
if (_disposed || !mounted) return;
|
||
setState(() => _loading = false);
|
||
Log.e('WidgetDataPreview: 加载预览数据失败', e);
|
||
}
|
||
}
|
||
|
||
Future<void> _refreshData() async {
|
||
if (_disposed || !mounted) return;
|
||
setState(() => _loading = true);
|
||
try {
|
||
final service = ref.read(homeWidgetServiceProvider);
|
||
await service
|
||
.updateWidget(widget.type)
|
||
.timeout(const Duration(seconds: 5));
|
||
await _loadPreview();
|
||
} on TimeoutException {
|
||
if (_disposed || !mounted) return;
|
||
setState(() => _loading = false);
|
||
Log.w('WidgetDataPreview: 刷新数据超时');
|
||
} catch (e) {
|
||
if (_disposed || !mounted) return;
|
||
setState(() => _loading = false);
|
||
Log.e('WidgetDataPreview: 刷新数据失败', e);
|
||
}
|
||
}
|
||
|
||
String _getPreviewText() {
|
||
return switch (widget.type) {
|
||
WidgetType.dailySentence => '${_data['daily_sentence'] ?? '暂无数据'}',
|
||
WidgetType.readlater => '未读 ${_data['readlater_count'] ?? 0} 条',
|
||
WidgetType.dailyFortune => '${_data['fortune_text'] ?? '暂无数据'}',
|
||
WidgetType.countdown => '${_data['countdown_title'] ?? '暂无数据'}',
|
||
WidgetType.pomodoro => '剩余 ${_data['pomodoro_remaining'] ?? 0}s',
|
||
WidgetType.solarTerm => '${_data['solar_term_name'] ?? '暂无数据'}',
|
||
WidgetType.checkin => '连续 ${_data['checkin_days'] ?? 0} 天',
|
||
WidgetType.dailyWithCharacter =>
|
||
'${_data['daily_with_character_content'] ?? '暂无数据'}',
|
||
WidgetType.dailyCard => '日签卡片',
|
||
};
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final ext = widget.ext;
|
||
return Container(
|
||
padding: const EdgeInsets.all(AppSpacing.sm),
|
||
decoration: BoxDecoration(
|
||
color: ext.bgSecondary.withValues(alpha: 0.5),
|
||
borderRadius: AppRadius.mdBorder,
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Icon(CupertinoIcons.eye_fill, size: 12, color: ext.textSecondary),
|
||
const SizedBox(width: 4),
|
||
Text(
|
||
'数据预览',
|
||
style: AppTypography.caption2.copyWith(
|
||
color: ext.textSecondary,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
const Spacer(),
|
||
GestureDetector(
|
||
onTap: _loading ? null : _refreshData,
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(CupertinoIcons.refresh, size: 12, color: ext.accent),
|
||
const SizedBox(width: 2),
|
||
Text(
|
||
'刷新',
|
||
style: AppTypography.caption2.copyWith(
|
||
color: ext.accent,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: AppSpacing.xs),
|
||
if (_loading)
|
||
const CupertinoActivityIndicator(radius: 6)
|
||
else
|
||
Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: AppSpacing.sm,
|
||
vertical: AppSpacing.xs,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: ext.bgCard.withValues(alpha: 0.6),
|
||
borderRadius: AppRadius.smBorder,
|
||
),
|
||
child: Text(
|
||
_getPreviewText(),
|
||
style: AppTypography.footnote.copyWith(color: ext.textPrimary),
|
||
maxLines: 2,
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|