Files
xianyan/lib/features/countdown/providers/countdown_provider.dart
Developer 10df6b705c 同步
2026-06-02 03:52:54 +08:00

289 lines
7.8 KiB
Dart
Raw 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-02
/// 更新时间: 2026-06-02
/// 作用: 倒计时事件 CRUD + 持久化 + 排序 + 灵动岛聚焦模式
/// 上次更新: 集成灵动岛Live Activity支持聚焦倒计时实时显示
/// ============================================================
import 'dart:async';
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/storage/kv_storage.dart';
import '../../../core/utils/logger.dart';
import '../../../core/services/device/live_activity_service.dart';
import '../models/countdown_models.dart';
class CountdownState {
const CountdownState({
this.events = const [],
this.isLoading = true,
this.focusedEventId,
});
final List<CountdownEvent> events;
final bool isLoading;
final String? focusedEventId;
CountdownEvent? get focusedEvent {
if (focusedEventId == null) return null;
try {
return events.firstWhere((e) => e.id == focusedEventId);
} catch (_) {
return null;
}
}
List<CountdownEvent> get pinned =>
events.where((e) => e.isPinned).toList()
..sort((a, b) => a.daysRemaining.compareTo(b.daysRemaining));
List<CountdownEvent> get upcoming =>
events.where((e) => !e.isPinned && !e.isPast).toList()
..sort((a, b) => a.daysRemaining.compareTo(b.daysRemaining));
List<CountdownEvent> get past =>
events.where((e) => !e.isPinned && e.isPast).toList()
..sort((a, b) => b.daysRemaining.compareTo(a.daysRemaining));
CountdownState copyWith({
List<CountdownEvent>? events,
bool? isLoading,
String? focusedEventId,
bool clearFocused = false,
}) {
return CountdownState(
events: events ?? this.events,
isLoading: isLoading ?? this.isLoading,
focusedEventId: clearFocused ? null : (focusedEventId ?? this.focusedEventId),
);
}
}
class CountdownNotifier extends Notifier<CountdownState> {
Timer? _updateTimer;
@override
CountdownState build() {
Future.microtask(() => _loadEvents()).catchError((_) {});
ref.onDispose(_onDispose);
return const CountdownState(isLoading: false);
}
static const _key = 'countdown_events';
// ============================================================
// 数据持久化
// ============================================================
Future<void> _loadEvents() async {
try {
final raw = KvStorage.getString(_key);
if (raw != null && raw.isNotEmpty) {
final list = (jsonDecode(raw) as List<dynamic>)
.map((e) => CountdownEvent.fromJson(e as Map<String, dynamic>))
.toList();
state = state.copyWith(events: list, isLoading: false);
} else {
state = state.copyWith(events: _defaultEvents(), isLoading: false);
await _saveEvents();
}
} catch (e) {
Log.e('倒计时加载失败', e);
state = state.copyWith(isLoading: false);
}
}
Future<void> _saveEvents() async {
try {
final json = jsonEncode(state.events.map((e) => e.toJson()).toList());
await KvStorage.setString(_key, json);
} catch (e) {
Log.e('倒计时保存失败', e);
}
}
List<CountdownEvent> _defaultEvents() {
final now = DateTime.now();
return [
CountdownEvent(
id: 'd1',
title: '新年',
targetDate: DateTime(now.year + 1),
category: CountdownCategory.festival,
emoji: '🎆',
isPinned: true,
),
CountdownEvent(
id: 'd2',
title: '春节',
targetDate: DateTime(now.year + 1, 1, 29),
category: CountdownCategory.festival,
emoji: '🧧',
colorHex: '#FF4444',
isPinned: true,
),
];
}
// ============================================================
// 事件 CRUD
// ============================================================
Future<void> addEvent(CountdownEvent event) async {
state = state.copyWith(events: [...state.events, event]);
await _saveEvents();
}
Future<void> updateEvent(CountdownEvent event) async {
state = state.copyWith(
events: state.events.map((e) => e.id == event.id ? event : e).toList(),
);
await _saveEvents();
if (state.focusedEventId == event.id) {
_updateLiveActivity();
}
}
Future<void> deleteEvent(String id) async {
final wasFocused = state.focusedEventId == id;
state = state.copyWith(
events: state.events.where((e) => e.id != id).toList(),
clearFocused: wasFocused,
);
await _saveEvents();
if (wasFocused) {
_endLiveActivity();
_updateTimer?.cancel();
_updateTimer = null;
}
}
Future<void> togglePin(String id) async {
state = state.copyWith(
events: state.events
.map((e) => e.id == id ? e.copyWith(isPinned: !e.isPinned) : e)
.toList(),
);
await _saveEvents();
}
// ============================================================
// 灵动岛聚焦模式
// ============================================================
Future<void> focusEvent(String id) async {
final event = state.events.where((e) => e.id == id).firstOrNull;
if (event == null) return;
if (event.isPast) return;
if (state.focusedEventId == id) {
await unfocusEvent();
return;
}
if (state.focusedEventId != null) {
_endLiveActivity();
_updateTimer?.cancel();
}
state = state.copyWith(focusedEventId: id);
_startLiveActivity();
_updateTimer?.cancel();
_updateTimer = Timer.periodic(const Duration(minutes: 1), (_) {
_updateLiveActivity();
_checkEventArrival();
});
Log.i('CountdownNotifier: 聚焦倒计时 "${event.title}"');
}
Future<void> unfocusEvent() async {
_endLiveActivity();
_updateTimer?.cancel();
_updateTimer = null;
state = state.copyWith(clearFocused: true);
Log.i('CountdownNotifier: 取消聚焦倒计时');
}
void _startLiveActivity() {
final service = LiveActivityService.instance;
if (!service.isSupported) return;
final event = state.focusedEvent;
if (event == null) return;
final remainingMinutes = _calculateRemainingMinutes(event);
service.startCountdownActivity(
title: event.title,
remainingMinutes: remainingMinutes,
emoji: event.emoji,
);
}
void _updateLiveActivity() {
final service = LiveActivityService.instance;
if (!service.isSupported || !service.hasActiveActivity) return;
final event = state.focusedEvent;
if (event == null) {
_endLiveActivity();
return;
}
final remainingMinutes = _calculateRemainingMinutes(event);
service.updateCountdownActivity(
title: event.title,
remainingMinutes: remainingMinutes,
emoji: event.emoji,
);
}
void _endLiveActivity() {
final service = LiveActivityService.instance;
if (!service.isSupported) return;
service.endCountdownActivity();
}
int _calculateRemainingMinutes(CountdownEvent event) {
final now = DateTime.now();
final target = DateTime(
event.targetDate.year,
event.targetDate.month,
event.targetDate.day,
23,
59,
59,
);
final diff = target.difference(now);
return diff.inMinutes.clamp(0, diff.inMinutes);
}
void _checkEventArrival() {
final event = state.focusedEvent;
if (event == null) return;
if (event.isPast || event.isToday) {
_endLiveActivity();
_updateTimer?.cancel();
_updateTimer = null;
state = state.copyWith(clearFocused: true);
Log.i('CountdownNotifier: 倒计时 "${event.title}" 已到达,结束灵动岛');
}
}
void _onDispose() {
_updateTimer?.cancel();
_endLiveActivity();
}
}
final countdownProvider = NotifierProvider<CountdownNotifier, CountdownState>(
CountdownNotifier.new,
);