- 清理大量废弃的 barrel 导出文件,移除冗余的中间导出层 - 修复所有相对路径导入错误,统一调整为扁平化模块引用 - 更新多平台 pubspec 版本号与依赖库版本 - 补充后端功能问题管理后台与脚本工具 - 调整部分页面的快捷方式文案适配新功能 - 更新部分翻译覆盖率与API文档
570 lines
17 KiB
Dart
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,
|
|
);
|