/// ============================================================ /// 闲言APP — 倒计时页面 /// 创建时间: 2026-05-02 /// 更新时间: 2026-06-02 /// 作用: 倒计时事件列表 + 新增/编辑 + 分类筛选 + 灵动岛聚焦 /// 上次更新: 集成灵动岛聚焦模式,卡片添加灵动岛按钮 /// ============================================================ import 'package:flutter/cupertino.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/services/device/live_activity_provider.dart'; import '../../../core/theme/app_radius.dart'; import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_theme.dart'; import '../../../core/theme/app_typography.dart'; import '../../../shared/widgets/adaptive/keyboard_safe_sheet.dart'; import '../models/countdown_models.dart'; import '../providers/countdown_provider.dart'; import '../../../../shared/widgets/adaptive/adaptive_back_button.dart'; class CountdownPage extends ConsumerStatefulWidget { const CountdownPage({super.key}); @override ConsumerState createState() => _CountdownPageState(); } class _CountdownPageState extends ConsumerState { @override Widget build(BuildContext context) { final state = ref.watch(countdownProvider); final ext = AppTheme.ext(context); return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( leading: const AdaptiveBackButton(), middle: Text( '⏰ 倒计时', style: AppTypography.title3.copyWith(color: ext.textPrimary), ), backgroundColor: ext.bgPrimary.withValues(alpha: 0.85), border: null, trailing: CupertinoButton( padding: EdgeInsets.zero, minimumSize: const Size(44, 44), onPressed: () => _showAddSheet(context), child: Icon(CupertinoIcons.add, color: ext.accent, size: 24), ), ), child: SafeArea( child: state.isLoading ? Center( child: const CupertinoActivityIndicator( radius: 16, ).animate().fadeIn(duration: 300.ms), ) : _buildContent(state, ext), ), ); } // ============================================================ // 主内容 // ============================================================ Widget _buildContent(CountdownState state, AppThemeExtension ext) { if (state.events.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text( '⏰', style: TextStyle(fontSize: 48), ).animate().scale(duration: 500.ms, begin: const Offset(0.5, 0.5)), const SizedBox(height: AppSpacing.md), Text( '暂无倒计时', style: AppTypography.body.copyWith(color: ext.textHint), ), const SizedBox(height: AppSpacing.md), CupertinoButton.filled( onPressed: () => _showAddSheet(context), child: const Text('添加倒计时'), ), ], ), ); } return ListView( padding: const EdgeInsets.symmetric( horizontal: AppSpacing.md, vertical: 12, ), children: [ if (state.focusedEventId != null) _buildFocusBanner(state, ext), if (state.pinned.isNotEmpty) ...[ _buildSectionTitle('📌 置顶', ext), ...state.pinned.asMap().entries.map( (e) => _buildEventCard(e.value, ext, isPinned: true, index: e.key), ), const SizedBox(height: AppSpacing.md), ], if (state.upcoming.isNotEmpty) ...[ _buildSectionTitle('🔜 即将到来', ext), ...state.upcoming.asMap().entries.map( (e) => _buildEventCard(e.value, ext, index: e.key), ), const SizedBox(height: AppSpacing.md), ], if (state.past.isNotEmpty) ...[ _buildSectionTitle('📅 已过去', ext), ...state.past.asMap().entries.map( (e) => _buildEventCard(e.value, ext, isPast: true, index: e.key), ), ], const SizedBox(height: 60), ], ); } // ============================================================ // 灵动岛聚焦横幅 // ============================================================ Widget _buildFocusBanner(CountdownState state, AppThemeExtension ext) { final event = state.focusedEvent; if (event == null) return const SizedBox.shrink(); return Container( margin: const EdgeInsets.only(bottom: AppSpacing.md), padding: const EdgeInsets.symmetric( horizontal: AppSpacing.md, vertical: AppSpacing.sm + 2, ), decoration: BoxDecoration( color: ext.accent.withValues(alpha: 0.1), borderRadius: AppRadius.lgBorder, border: Border.all(color: ext.accent.withValues(alpha: 0.25)), ), child: Row( children: [ Text(event.emoji, style: const TextStyle(fontSize: 18)), const SizedBox(width: AppSpacing.sm), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '灵动岛已开启', style: AppTypography.caption1.copyWith( color: ext.accent, fontWeight: FontWeight.w600, ), ), Text( '${event.title} · ${event.remainingLabel}', style: AppTypography.caption2.copyWith( color: ext.textSecondary, ), ), ], ), ), CupertinoButton( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), minimumSize: Size.zero, onPressed: () => ref.read(countdownProvider.notifier).unfocusEvent(), child: Icon( CupertinoIcons.xmark_circle_fill, color: ext.accent, size: 20, ), ), ], ), ).animate().fadeIn(duration: 300.ms).slideY(begin: -0.1, end: 0); } // ============================================================ // 分区标题 // ============================================================ Widget _buildSectionTitle(String title, AppThemeExtension ext) { return Padding( padding: const EdgeInsets.only(bottom: 10), child: Text( title, style: AppTypography.callout.copyWith( fontWeight: FontWeight.w600, color: ext.textSecondary, ), ), ); } // ============================================================ // 事件卡片 // ============================================================ Widget _buildEventCard( CountdownEvent event, AppThemeExtension ext, { bool isPinned = false, bool isPast = false, int index = 0, }) { final color = _parseColor(event.colorHex); final isFocused = ref.watch(countdownProvider).focusedEventId == event.id; final isLiveActivitySupported = ref.watch(liveActivitySupportedProvider); return GestureDetector( onLongPress: () => _showEventActions(event), child: Container( margin: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( color: ext.bgCard, borderRadius: AppRadius.lgBorder, border: isFocused ? Border.all(color: ext.accent.withValues(alpha: 0.5)) : isPinned ? Border.all(color: color.withValues(alpha: 0.3)) : null, ), child: Row( children: [ Container( width: 48, height: 48, decoration: BoxDecoration( color: color.withValues(alpha: 0.12), borderRadius: AppRadius.mdBorder, ), child: Center( child: Text( event.emoji, style: const TextStyle(fontSize: 24), ), ), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( event.title, style: AppTypography.body.copyWith( fontWeight: FontWeight.w600, color: isPast ? ext.textHint : ext.textPrimary, ), ), const SizedBox(height: AppSpacing.xs), Text( _formatDate(event.targetDate), style: AppTypography.footnote.copyWith( color: ext.textHint, ), ), ], ), ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Row( mainAxisSize: MainAxisSize.min, children: [ if (isLiveActivitySupported && !isPast) Padding( padding: const EdgeInsets.only(right: 6), child: GestureDetector( onTap: () => ref .read(countdownProvider.notifier) .focusEvent(event.id), child: Icon( isFocused ? CupertinoIcons.bell_fill : CupertinoIcons.bell, size: 16, color: isFocused ? ext.accent : ext.textDisabled, ), ), ), Text( event.isToday ? '🎉' : '${event.daysRemaining.abs()}', style: AppTypography.title1.copyWith( fontWeight: FontWeight.w300, color: isPast ? ext.textDisabled : color, fontSize: event.isToday ? 24 : 28, ), ), ], ), Text( event.remainingLabel, style: AppTypography.caption1.copyWith( color: isPast ? ext.textDisabled : ext.textSecondary, ), ), ], ), ], ), ), ) .animate() .fadeIn(duration: 350.ms, delay: (index * 50).ms) .slideX(begin: 0.1, end: 0, duration: 350.ms, delay: (index * 50).ms); } // ============================================================ // 工具方法 // ============================================================ Color _parseColor(String hex) { try { final code = hex.replaceAll('#', ''); return Color(int.parse('FF$code', radix: 16)); } catch (_) { return const Color(0xFFFF6B6B); } } String _formatDate(DateTime date) => '${date.year}年${date.month}月${date.day}日'; // ============================================================ // 事件操作 // ============================================================ void _showEventActions(CountdownEvent event) { final isFocused = ref.read(countdownProvider).focusedEventId == event.id; final isLiveActivitySupported = ref.read(liveActivitySupportedProvider); showCupertinoModalPopup( context: context, builder: (ctx) => CupertinoActionSheet( actions: [ if (isLiveActivitySupported && !event.isPast) CupertinoActionSheetAction( onPressed: () { Navigator.pop(ctx); ref.read(countdownProvider.notifier).focusEvent(event.id); }, child: Text(isFocused ? '关闭灵动岛' : '🔔 显示到灵动岛'), ), CupertinoActionSheetAction( onPressed: () { Navigator.pop(ctx); ref.read(countdownProvider.notifier).togglePin(event.id); }, child: Text(event.isPinned ? '取消置顶' : '置顶'), ), CupertinoActionSheetAction( onPressed: () { Navigator.pop(ctx); _showEditSheet(event); }, child: const Text('编辑'), ), CupertinoActionSheetAction( isDestructiveAction: true, onPressed: () { Navigator.pop(ctx); _confirmDelete(event); }, child: const Text('删除'), ), ], cancelButton: CupertinoActionSheetAction( onPressed: () => Navigator.pop(ctx), child: const Text('取消'), ), ), ); } void _confirmDelete(CountdownEvent event) { showCupertinoDialog( context: context, builder: (ctx) => CupertinoAlertDialog( title: Text('删除「${event.title}」?'), content: const Text('删除后无法恢复'), actions: [ CupertinoDialogAction( onPressed: () => Navigator.pop(ctx), child: const Text('取消'), ), CupertinoDialogAction( isDestructiveAction: true, onPressed: () { Navigator.pop(ctx); ref.read(countdownProvider.notifier).deleteEvent(event.id); }, child: const Text('删除'), ), ], ), ); } // ============================================================ // 新增/编辑表单 // ============================================================ void _showAddSheet(BuildContext context) { _showEventForm(null); } void _showEditSheet(CountdownEvent event) { _showEventForm(event); } void _showEventForm(CountdownEvent? existing) { final titleCtl = TextEditingController(text: existing?.title ?? ''); var selectedDate = existing?.targetDate ?? DateTime.now().add(const Duration(days: 30)); var selectedCategory = existing?.category ?? CountdownCategory.custom; var selectedEmoji = existing?.emoji ?? '📌'; final selectedColor = existing?.colorHex ?? '#FF6B6B'; showCupertinoModalPopup( context: context, builder: (ctx) { return StatefulBuilder( builder: (ctx, setModalState) { final ext = AppTheme.ext(context); return KeyboardSafeBuilder( builder: (ctx, _) => Container( constraints: BoxConstraints( maxHeight: MediaQuery.of(ctx).size.height * 0.7, ), decoration: BoxDecoration( color: ext.bgPrimary, borderRadius: const BorderRadius.vertical( top: Radius.circular(20), ), ), child: SingleChildScrollView( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Text( existing != null ? '编辑倒计时' : '新建倒计时', style: AppTypography.headline.copyWith( fontWeight: FontWeight.w600, color: ext.textPrimary, ), ), ), const SizedBox(height: 20), Row( children: [ GestureDetector( onTap: () => _showEmojiPicker( ctx, selectedEmoji, (e) => setModalState(() => selectedEmoji = e), ), child: Container( width: 44, height: 44, decoration: BoxDecoration( color: _parseColor( selectedColor, ).withValues(alpha: 0.12), borderRadius: AppRadius.mdBorder, ), child: Center( child: Text( selectedEmoji, style: const TextStyle(fontSize: 22), ), ), ), ), const SizedBox(width: 12), Expanded( child: CupertinoTextField( controller: titleCtl, placeholder: '事件名称', padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: ext.bgCard, borderRadius: AppRadius.mdBorder, ), ), ), ], ), const SizedBox(height: AppSpacing.md), Row( children: [ Text( '📅 日期', style: AppTypography.subhead.copyWith( color: ext.textSecondary, ), ), const Spacer(), GestureDetector( onTap: () async { final picked = await showCupertinoModalPopup( context: ctx, builder: (c) => Container( height: 260, color: CupertinoColors.systemBackground .resolveFrom(c), child: CupertinoDatePicker( mode: CupertinoDatePickerMode.date, initialDateTime: selectedDate, onDateTimeChanged: (d) => setModalState( () => selectedDate = d, ), ), ), ); if (picked != null) setModalState(() => selectedDate = picked); }, child: Text( _formatDate(selectedDate), style: AppTypography.subhead.copyWith( color: ext.accent, fontWeight: FontWeight.w500, ), ), ), ], ), const SizedBox(height: AppSpacing.md), Text( '分类', style: AppTypography.subhead.copyWith( color: ext.textSecondary, ), ), const SizedBox(height: AppSpacing.sm), Wrap( spacing: AppSpacing.sm, runSpacing: AppSpacing.sm, children: CountdownCategory.values.map((cat) { final isActive = cat == selectedCategory; return GestureDetector( onTap: () => setModalState(() => selectedCategory = cat), child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6, ), decoration: BoxDecoration( color: isActive ? ext.accent.withValues(alpha: 0.15) : ext.bgCard, borderRadius: AppRadius.lgBorder, border: isActive ? Border.all( color: ext.accent.withValues( alpha: 0.3, ), ) : null, ), child: Text( '${cat.emoji} ${cat.label}', style: AppTypography.footnote.copyWith( color: isActive ? ext.accent : ext.textSecondary, ), ), ), ); }).toList(), ), const Spacer(), Row( children: [ Expanded( child: CupertinoButton( onPressed: () => Navigator.pop(ctx), child: const Text('取消'), ), ), const SizedBox(width: 12), Expanded( child: CupertinoButton.filled( onPressed: () { if (titleCtl.text.trim().isEmpty) return; final event = CountdownEvent( id: existing?.id ?? 'cd_${DateTime.now().millisecondsSinceEpoch}', title: titleCtl.text.trim(), targetDate: selectedDate, category: selectedCategory, emoji: selectedEmoji, colorHex: selectedColor, isPinned: existing?.isPinned ?? false, createdAt: existing?.createdAt ?? DateTime.now(), ); if (existing != null) { ref .read(countdownProvider.notifier) .updateEvent(event); } else { ref .read(countdownProvider.notifier) .addEvent(event); } Navigator.pop(ctx); }, child: Text(existing != null ? '保存' : '添加'), ), ), ], ), ], ), ), ), ); }, ); }, ); } void _showEmojiPicker( BuildContext context, String current, ValueChanged onSelected, ) { const emojis = [ '📌', '🎉', '🎂', '💝', '📝', '✈️', '⏰', '🎆', '🧧', '🎄', '🎃', '🎓', '💍', '🏠', '🚗', '💻', '📱', '🎮', ]; final ext = AppTheme.ext(context); showCupertinoModalPopup( context: context, builder: (ctx) => Container( height: 200, padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( color: ext.bgPrimary, borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), child: Wrap( spacing: 12, runSpacing: 12, children: emojis .map( (e) => GestureDetector( onTap: () { onSelected(e); Navigator.pop(ctx); }, child: Container( width: 40, height: 40, decoration: BoxDecoration( color: e == current ? ext.accent.withValues(alpha: 0.15) : ext.bgCard, borderRadius: AppRadius.smBorder, ), child: Center( child: Text(e, style: const TextStyle(fontSize: 22)), ), ), ), ) .toList(), ), ), ); } }