Files
xianyan/lib/features/widget/presentation/widget_management_page.dart
Developer 6119918185 release: bump version to 6.6.25+2606241
主要变更:
1. 新增桌面端托盘图标支持深色/浅色主题切换
2. 重构应用锁、动画配置、小组件导航服务职责
3. 修复Riverpod初始化断言、防重复点击、工作台模式残留选中态问题
4. 优化诗词服务、阅读进度、搜索结果空状态体验
5. 完善macOS打包配置与错误静默处理逻辑
6. 新增快速卡片多语言适配与动画退出队列管理
2026-06-24 04:26:50 +08:00

1072 lines
34 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// ============================================================
/// 闲言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 '../../../l10n/translation_resolver.dart';
import '../../../l10n/types/t.dart';
import '../../../shared/widgets/adaptive/adaptive_back_button.dart';
import '../../../shared/widgets/beta_testing_overlay.dart';
import '../../../shared/widgets/containers/glass_container.dart';
import '../../../shared/widgets/adaptive/responsive_layout.dart';
import '../../../shared/widgets/feedback/app_toast.dart';
import '../widget_type.dart';
import '../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;
final tw = ref.read(translationsProvider).widget;
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: [
Text(tw.devDialogContent),
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),
Text(tw.dontRemind, style: const TextStyle(fontSize: 14)),
],
),
),
],
),
),
actions: [
CupertinoDialogAction(
child: Text(tw.viewExperimentalFeatures),
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: Text(tw.iKnow),
),
],
),
);
},
);
}
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
final t = ref.watch(translationsProvider);
final tw = t.widget;
final wp.WidgetState widgetState = ref.watch(wp.widgetProvider);
final grouped = <int, List<WidgetType>>{};
for (final wt in WidgetTypeX.all) {
grouped.putIfAbsent(wt.priority, () => []).add(wt);
}
return Stack(
children: [
CupertinoPageScaffold(
backgroundColor: ext.bgPrimary,
child: ResponsiveMaxWidth(
maxWidth: 900,
child: SafeArea(
bottom: false,
child: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
CupertinoSliverNavigationBar(
leading: const AdaptiveBackButton(),
middle: Text(
tw.title,
style: AppTypography.title3.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
largeTitle: Text(
tw.title,
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, tw: tw),
),
),
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, tw),
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,
tw: tw,
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)),
],
),
),
),
),
// Beta 测试全局透明水印蒙版
const BetaTestingOverlay(),
],
);
}
String _priorityLabel(int p, TWidget tw) => switch (p) {
0 => tw.priorityCore,
1 => tw.priorityRecommended,
2 => tw.priorityPractical,
_ => tw.priorityFun,
};
void _handleAddWidget(WidgetType type) async {
// 鸿蒙端点击添加弹出toast提示
if (PlatformHelper.isHarmonyOS) {
AppToast.showInfo(ref.read(translationsProvider).common.inDevelopment);
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) {
final tw = ref.read(translationsProvider).widget;
AppToast.showInfo(tw.addedToDesktop.replaceAll('{0}', type.localizedTitle(tw)));
} 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 tw = ref.read(translationsProvider).widget;
final typeTitle = type.localizedTitle(tw);
final steps = PlatformHelper.isHarmonyOS
? [
tw.harmonyStep1,
tw.harmonyStep2,
tw.harmonyStep3,
tw.harmonyStep4.replaceAll('{0}', typeTitle),
]
: PlatformHelper.isAndroid
? [
tw.androidStep1,
tw.androidStep2,
tw.androidStep3,
tw.androidStep4.replaceAll('{0}', typeTitle),
]
: [
tw.iosStep1,
tw.iosStep2,
tw.iosStep3,
tw.iosStep4.replaceAll('{0}', typeTitle),
];
final guideReason = isUnsupported
? tw.unsupportedAddHint
: tw.failedAddHint;
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(tw.addWidgetTitle.replaceAll('{0}', typeTitle)),
],
),
),
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: Text(tw.pushData),
onPressed: () {
Navigator.of(ctx).pop();
ref.read(wp.widgetProvider.notifier).pushDataToWidget(type);
AppToast.showInfo(tw.pushedDataToWidget.replaceAll('{0}', typeTitle));
},
),
CupertinoDialogAction(
isDefaultAction: true,
child: Text(tw.gotIt),
onPressed: () => Navigator.of(ctx).pop(),
),
],
),
);
}
}
class _ThemeToggle extends ConsumerWidget {
const _ThemeToggle({required this.ext});
final AppThemeExtension ext;
@override
Widget build(BuildContext context, WidgetRef ref) {
final tw = ref.watch(translationsProvider).widget;
return GestureDetector(
onTap: () {
ref.read(wp.widgetProvider.notifier).pushThemeToAllWidgets();
AppToast.showInfo(tw.pushedThemeToWidget);
},
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(
tw.syncTheme,
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, required this.tw});
final AppThemeExtension ext;
final TWidget tw;
@override
State<_PlatformCompatibilityCard> createState() =>
_PlatformCompatibilityCardState();
}
class _PlatformCompatibilityCardState
extends State<_PlatformCompatibilityCard> {
bool _isExpanded = false;
@override
Widget build(BuildContext context) {
final ext = widget.ext;
final tw = widget.tw;
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(
tw.platformCompatTitle,
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: tw.androidCompatDesc,
),
const SizedBox(height: 4),
_PlatformRow(
ext: ext,
icon: '🍎',
name: 'iOS',
desc: tw.iosCompatDesc,
),
const SizedBox(height: 4),
_PlatformRow(
ext: ext,
icon: '🔴',
name: tw.badgeHarmony,
desc: tw.harmonyCompatDesc,
),
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(
tw.syncThemeHint,
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.tw,
required this.type,
required this.isInstalled,
required this.isAdding,
required this.onAdd,
required this.onPin,
});
final AppThemeExtension ext;
final TWidget tw;
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.localizedTitle(tw),
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.localizedSubtitle(tw),
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(
tw.installed,
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(
tw.add,
style: AppTypography.caption2.copyWith(
color: CupertinoColors.white,
fontWeight: FontWeight.w600,
),
),
),
],
),
const SizedBox(height: AppSpacing.xs),
Row(
children: [
_PlatformBadges(ext: ext, tw: tw, type: type),
const Spacer(),
_DeepLinkBadge(ext: ext, route: type.deepLinkRoute),
],
),
const SizedBox(height: AppSpacing.sm),
_WidgetDataPreview(ext: ext, tw: tw, 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.tw, required this.type});
final AppThemeExtension ext;
final TWidget tw;
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: tw.badgeHarmony, 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.tw, required this.type});
final AppThemeExtension ext;
final TWidget tw;
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() {
final tw = widget.tw;
return switch (widget.type) {
WidgetType.dailySentence =>
'${_data['daily_sentence'] ?? tw.previewNoData}',
WidgetType.readlater =>
tw.previewReadlaterCount.replaceAll('{0}', '${_data['readlater_count'] ?? 0}'),
WidgetType.dailyFortune =>
'${_data['fortune_text'] ?? tw.previewNoData}',
WidgetType.countdown =>
'${_data['countdown_title'] ?? tw.previewNoData}',
WidgetType.pomodoro =>
tw.previewPomodoroRemaining.replaceAll('{0}', '${_data['pomodoro_remaining'] ?? 0}'),
WidgetType.solarTerm =>
'${_data['solar_term_name'] ?? tw.previewNoData}',
WidgetType.checkin =>
tw.previewCheckinDays.replaceAll('{0}', '${_data['checkin_days'] ?? 0}'),
WidgetType.dailyWithCharacter =>
'${_data['daily_with_character_content'] ?? tw.previewNoData}',
WidgetType.dailyCard =>
tw.previewDailyCard,
};
}
@override
Widget build(BuildContext context) {
final ext = widget.ext;
final tw = widget.tw;
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(
tw.dataPreview,
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(
tw.refresh,
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,
),
),
],
),
);
}
}