Files
xianyan/lib/features/progress/progress_provider.dart
Developer f91be94e9c refactor: 完成项目架构重构,统一模块导入路径
- 清理大量废弃的 barrel 导出文件,移除冗余的中间导出层
- 修复所有相对路径导入错误,统一调整为扁平化模块引用
- 更新多平台 pubspec 版本号与依赖库版本
- 补充后端功能问题管理后台与脚本工具
- 调整部分页面的快捷方式文案适配新功能
- 更新部分翻译覆盖率与API文档
2026-06-12 08:53:57 +08:00

570 lines
17 KiB
Dart

/// ============================================================
/// 闲言APP — 进度页面状态管理
/// 创建时间: 2026-05-02
/// 更新时间: 2026-05-30
/// 作用: 进度会话状态 — 系统进度/节日倒计时/用户自定义
/// 上次更新: 使用SafeNotifierInit统一异常保护
/// ============================================================
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:xianyan/core/storage/kv_storage.dart';
import 'package:xianyan/core/utils/logger.dart';
import 'package:xianyan/core/utils/safe_init_mixin.dart';
import 'package:xianyan/features/progress/progress_models.dart';
class ProgressState {
const ProgressState({
this.systemItems = const [],
this.userItems = const [],
this.isLoading = true,
this.sortMode = ProgressSortMode.custom,
this.groupByType = true,
});
final List<ProgressItem> systemItems;
final List<ProgressItem> userItems;
final bool isLoading;
final ProgressSortMode sortMode;
final bool groupByType;
List<dynamic> get timeline {
final now = DateTime.now();
final items = <dynamic>[];
if (!groupByType) {
if (systemItems.isNotEmpty) {
items.add(
ProgressTimeDivider(
text:
'${now.month}${now.day}${now.hour.toString().padLeft(2, '0')}:00 · 系统',
),
);
items.addAll(systemItems);
}
if (userItems.isNotEmpty) {
final validItems = userItems.where((i) => i.createdAt != null).toList();
final latestUser = validItems.isNotEmpty
? validItems.reduce(
(a, b) => a.createdAt!.compareTo(b.createdAt!) > 0 ? a : b,
)
: null;
final userTime = latestUser?.createdAt ?? now;
items.add(
ProgressTimeDivider(
text:
'${userTime.month}${userTime.day}${userTime.hour.toString().padLeft(2, '0')}:${userTime.minute.toString().padLeft(2, '0')} · 用户添加',
),
);
items.addAll(_sortedUserItems);
}
} else {
final allItems = [...systemItems, ..._sortedUserItems];
final grouped = <ProgressItemType, List<ProgressItem>>{};
for (final item in allItems) {
grouped.putIfAbsent(item.type, () => []).add(item);
}
final typeOrder = [
ProgressItemType.timeProgress,
ProgressItemType.holidayCountdown,
ProgressItemType.userCountdown,
ProgressItemType.userProgress,
];
for (final type in typeOrder) {
final group = grouped[type];
if (group == null || group.isEmpty) continue;
items.add(
ProgressTimeDivider(text: type.label, emoji: _typeEmoji(type)),
);
items.addAll(group);
}
}
return items;
}
List<ProgressItem> get _sortedUserItems {
final items = [...userItems];
switch (sortMode) {
case ProgressSortMode.byUrgency:
items.sort((a, b) {
final aRem = a.remaining ?? const Duration(days: 9999);
final bRem = b.remaining ?? const Duration(days: 9999);
return aRem.compareTo(bRem);
});
case ProgressSortMode.byDate:
items.sort((a, b) {
final aDate = a.createdAt ?? DateTime.now();
final bDate = b.createdAt ?? DateTime.now();
return bDate.compareTo(aDate);
});
case ProgressSortMode.byType:
items.sort((a, b) => a.type.index.compareTo(b.type.index));
case ProgressSortMode.custom:
break;
}
return items;
}
String _typeEmoji(ProgressItemType type) {
return switch (type) {
ProgressItemType.timeProgress => '',
ProgressItemType.holidayCountdown => '🎉',
ProgressItemType.userCountdown => '⏱️',
ProgressItemType.userProgress => '📊',
};
}
ProgressState copyWith({
List<ProgressItem>? systemItems,
List<ProgressItem>? userItems,
bool? isLoading,
ProgressSortMode? sortMode,
bool? groupByType,
}) {
return ProgressState(
systemItems: systemItems ?? this.systemItems,
userItems: userItems ?? this.userItems,
isLoading: isLoading ?? this.isLoading,
sortMode: sortMode ?? this.sortMode,
groupByType: groupByType ?? this.groupByType,
);
}
}
class ProgressNotifier extends Notifier<ProgressState> with SafeNotifierInit {
@override
ProgressState build() {
safeNotifierInit(_init, label: 'ProgressNotifier');
return const ProgressState();
}
static const _userItemsKey = 'progress_user_items';
Future<void> _init() async {
_buildSystemItems();
await _loadUserItems();
if (state.isLoading) {
state = state.copyWith(isLoading: false);
}
}
void _buildSystemItems() {
final now = DateTime.now();
final startOfDay = DateTime(now.year, now.month, now.day);
final endOfDay = DateTime(now.year, now.month, now.day, 23, 59, 59);
final dayProgress =
now.difference(startOfDay).inSeconds /
endOfDay.difference(startOfDay).inSeconds;
final startOfWeek = now.subtract(Duration(days: now.weekday - 1));
final startOfWeekNorm = DateTime(
startOfWeek.year,
startOfWeek.month,
startOfWeek.day,
);
final weekProgress = now.difference(startOfWeekNorm).inDays / 7;
final daysInMonth = DateTime(now.year, now.month + 1, 0).day;
final monthProgress = (now.day - 1 + dayProgress) / daysInMonth;
final startOfYear = DateTime(now.year);
final endOfYear = DateTime(now.year, 12);
final yearProgress =
now.difference(startOfYear).inDays /
endOfYear.difference(startOfYear).inDays;
final weekDays = ['', '', '', '', '', '', ''];
final currentWeekday = weekDays[now.weekday - 1];
final holidays = _buildHolidayItems(now);
final systemItems = <ProgressItem>[
ProgressItem(
id: 'today',
type: ProgressItemType.timeProgress,
emoji: '☀️',
title: '今日进度',
subtitle: '今天已经过去了',
progressPct: dayProgress,
displayStyle: ProgressDisplayStyle.progressBar,
statItems: [
ProgressStatItem(value: '${now.hour}', label: '小时已过'),
ProgressStatItem(value: '${23 - now.hour}', label: '小时剩余'),
],
createdAt: now,
),
ProgressItem(
id: 'week',
type: ProgressItemType.timeProgress,
emoji: '📆',
title: '本周进度',
subtitle: '${_weekNumber(now)}周 · 周$currentWeekday',
displayStyle: ProgressDisplayStyle.ringProgress,
ringPct: weekProgress,
ringLabel: '${now.weekday}/7',
ringColor: '#007AFF',
statItems: [
ProgressStatItem(value: '${now.weekday}', label: '天已过'),
ProgressStatItem(value: '${7 - now.weekday}', label: '天剩余'),
],
createdAt: now,
),
ProgressItem(
id: 'month',
type: ProgressItemType.timeProgress,
emoji: '🗓️',
title: '本月进度',
subtitle: '${now.month}月 · ${_seasonLabel(now.month)}',
progressPct: monthProgress,
displayStyle: ProgressDisplayStyle.progressBar,
statItems: [
ProgressStatItem(value: '${now.day - 1}', label: '天已过'),
ProgressStatItem(value: '${daysInMonth - now.day + 1}', label: '天剩余'),
],
createdAt: now,
),
ProgressItem(
id: 'year',
type: ProgressItemType.timeProgress,
emoji: '🎯',
title: '年度进度',
subtitle: '${now.year}',
displayStyle: ProgressDisplayStyle.ringProgress,
ringPct: yearProgress,
ringLabel: '${(yearProgress * 100).toInt()}%',
ringColor: '#FF9500',
summaryRows: [
ProgressSummaryRow(
label: '已过天数',
value: '${now.difference(startOfYear).inDays}',
),
ProgressSummaryRow(
label: '剩余天数',
value: '${endOfYear.difference(now).inDays}',
),
],
createdAt: now,
),
...holidays,
];
state = state.copyWith(systemItems: systemItems);
}
List<ProgressItem> _buildHolidayItems(DateTime now) {
final items = <ProgressItem>[];
final holidays = <Map<String, dynamic>>[
{
'emoji': '👷',
'name': '劳动节',
'date': DateTime(now.year, 5),
'tag': '🎉 劳动最光荣',
'tagColor': '#007AFF',
},
{
'emoji': '🐉',
'name': '端午节',
'date': DateTime(now.year, 5, 31),
'tag': '🥟 吃粽子',
'tagColor': '#D97706',
},
{
'emoji': '🇨🇳',
'name': '国庆节',
'date': DateTime(now.year, 10),
'tag': '🇨🇳 祖国万岁',
'tagColor': '#DC2626',
},
{
'emoji': '🎊',
'name': '元旦',
'date': DateTime(now.year + 1),
'tag': '🎆 新年快乐',
'tagColor': '#F59E0B',
},
{
'emoji': '🧧',
'name': '春节',
'date': DateTime(now.year + 1, 2, 6),
'tag': '🧧 阖家团圆',
'tagColor': '#EF4444',
},
];
for (final h in holidays) {
final date = h['date'] as DateTime;
final diff = date.difference(now);
final isPast = diff.isNegative;
items.add(
ProgressItem(
id: 'holiday_${h['name']}',
type: ProgressItemType.holidayCountdown,
emoji: h['emoji'] as String,
title: h['name'] as String,
subtitle: '${date.month}${date.day}${isPast ? ' · 已过' : ''}',
targetDate: date,
isPast: isPast,
tagText: isPast ? '🎉 刚刚过去 ${-diff.inDays}' : h['tag'] as String?,
tagColor: h['tagColor'] as String?,
displayStyle: isPast
? ProgressDisplayStyle.tagOnly
: ProgressDisplayStyle.countdownGrid,
createdAt: now,
),
);
}
return items;
}
Future<void> _loadUserItems() async {
try {
final raw = KvStorage.getString(_userItemsKey);
if (raw != null && raw.isNotEmpty) {
final decoded = jsonDecode(raw);
if (decoded is List) {
final items = decoded
.map((e) {
try {
return ProgressItem.fromJson(e as Map<String, dynamic>);
} catch (itemError) {
Log.e('单条进度项解析失败,跳过', itemError);
return null;
}
})
.whereType<ProgressItem>()
.toList();
state = state.copyWith(userItems: items);
Log.i('加载用户进度项: ${items.length}');
}
}
} catch (e) {
Log.e('用户进度项加载失败', e);
}
}
Future<void> _saveUserItems() async {
try {
final list = state.userItems.map((e) => e.toJson()).toList();
await KvStorage.setString(_userItemsKey, jsonEncode(list));
Log.i('保存用户进度项: ${list.length}');
} catch (e) {
Log.e('用户进度项保存失败', e);
}
}
void addUserItem({
required String name,
required DateTime targetDate,
required String typeLabel,
String? tagText,
String? note,
}) {
final now = DateTime.now();
final diff = targetDate.difference(now);
final isPast = diff.isNegative;
final isProgressType = typeLabel.contains('进度');
final itemType = isProgressType
? ProgressItemType.userProgress
: ProgressItemType.userCountdown;
final emojis = ['📝', '🎯', '🚀', '💡', '🎂', '🎓', '💼', '🏆'];
final emoji = emojis[name.hashCode.abs() % emojis.length];
final effectiveTag = tagText ?? (isPast ? '已到期' : '进行中');
double? progressPct;
if (isProgressType) {
final totalSpan = targetDate.difference(DateTime(targetDate.year - 1));
if (totalSpan.inSeconds > 0) {
progressPct = (1 - diff.inSeconds / totalSpan.inSeconds).clamp(
0.0,
1.0,
);
} else {
progressPct = 1.0;
}
}
final item = ProgressItem(
id: 'user_${DateTime.now().millisecondsSinceEpoch}',
type: itemType,
emoji: emoji,
title: name,
subtitle:
'${targetDate.year}-${targetDate.month.toString().padLeft(2, '0')}-${targetDate.day.toString().padLeft(2, '0')}',
targetDate: targetDate,
isPast: isPast,
tagText: isPast ? '$effectiveTag' : '🔥 $effectiveTag',
tagColor: '#007AFF',
displayStyle: isProgressType
? ProgressDisplayStyle.progressBar
: ProgressDisplayStyle.countdownGrid,
progressPct: progressPct,
createdAt: now,
note: note,
);
final updated = [...state.userItems, item];
state = state.copyWith(userItems: updated);
_saveUserItems();
Log.i('添加用户进度项: $name');
}
void removeUserItem(String id) {
final updated = state.userItems.where((i) => i.id != id).toList();
state = state.copyWith(userItems: updated);
_saveUserItems();
Log.i('删除用户进度项: $id');
}
void restoreUserItem(ProgressItem item) {
final updated = [...state.userItems, item];
state = state.copyWith(userItems: updated);
_saveUserItems();
Log.i('恢复用户进度项: ${item.id}');
}
void updateUserItem({
required String id,
String? name,
DateTime? targetDate,
String? tagText,
ProgressDisplayStyle? displayStyle,
}) {
final idx = state.userItems.indexWhere((i) => i.id == id);
if (idx == -1) return;
final old = state.userItems[idx];
final now = DateTime.now();
final effectiveDate = targetDate ?? old.targetDate;
final diff = effectiveDate != null
? effectiveDate.difference(now)
: Duration.zero;
final isPast = diff.isNegative;
final updatedItem = old.copyWith(
title: name,
targetDate: targetDate,
tagText: tagText,
displayStyle: displayStyle,
isPast: isPast,
subtitle: effectiveDate != null
? '${effectiveDate.year}-${effectiveDate.month.toString().padLeft(2, '0')}-${effectiveDate.day.toString().padLeft(2, '0')}'
: null,
);
final updatedList = [...state.userItems];
updatedList[idx] = updatedItem;
state = state.copyWith(userItems: updatedList);
_saveUserItems();
Log.i('更新用户进度项: $id');
}
void reorderUserItems(int oldIndex, int newIndex) {
if (oldIndex < newIndex) newIndex -= 1;
final items = [...state.userItems];
final item = items.removeAt(oldIndex);
items.insert(newIndex, item);
state = state.copyWith(userItems: items);
_saveUserItems();
Log.i('重排用户进度项: $oldIndex -> $newIndex');
}
void clearAllUserItems() {
state = state.copyWith(userItems: []);
_saveUserItems();
Log.i('清空所有用户进度项');
}
void refresh() {
_buildSystemItems();
_recordHistorySnapshot();
Log.i('进度数据已刷新');
}
/// 记录历史快照 — 每次刷新时为有进度的用户项记录一个历史点
void _recordHistorySnapshot() {
final now = DateTime.now();
final updated = state.userItems.map((item) {
final pct = item.progressPct ?? item.ringPct;
if (pct == null) return item;
final lastHistory = item.history.isNotEmpty ? item.history.last : null;
if (lastHistory != null && now.difference(lastHistory.date).inHours < 1) {
return item;
}
return item.copyWith(
history: [
...item.history,
ProgressHistoryPoint(date: now, progressPct: pct),
],
);
}).toList();
if (updated.any(
(item) =>
item.history.length !=
state.userItems
.firstWhere((i) => i.id == item.id, orElse: () => item)
.history
.length,
)) {
state = state.copyWith(userItems: updated);
_saveUserItems();
}
}
/// 为进度项添加里程碑
void addMilestone(String itemId, ProgressMilestone milestone) {
final idx = state.userItems.indexWhere((i) => i.id == itemId);
if (idx == -1) return;
final item = state.userItems[idx];
final updated = item.copyWith(milestones: [...item.milestones, milestone]);
final list = [...state.userItems];
list[idx] = updated;
state = state.copyWith(userItems: list);
_saveUserItems();
Log.i('添加里程碑: ${milestone.label}$itemId');
}
/// 设置排序模式
void setSortMode(ProgressSortMode mode) {
state = state.copyWith(sortMode: mode);
Log.i('进度排序模式: ${mode.label}');
}
/// 切换分组模式
void toggleGroupByType() {
state = state.copyWith(groupByType: !state.groupByType);
Log.i('进度分组模式: ${state.groupByType ? "按类型" : "按来源"}');
}
int _weekNumber(DateTime date) {
final startOfYear = DateTime(date.year);
final days = date.difference(startOfYear).inDays;
return ((days + startOfYear.weekday) / 7).ceil();
}
String _seasonLabel(int month) {
return switch (month) {
3 || 4 || 5 => '春末夏初',
6 || 7 || 8 => '盛夏',
9 || 10 || 11 => '金秋',
_ => '寒冬',
};
}
}
final progressProvider = NotifierProvider<ProgressNotifier, ProgressState>(
ProgressNotifier.new,
);