Files
xianyan/lib/features/poetry/poetry_provider.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

466 lines
14 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-02
/// 更新时间: 2026-06-22
/// 作用: 今日诗词数据 + 用户信息状态 + 统一聊天记录
/// 上次更新: 合并 poetryHistory 和 chatRecords 为统一聊天记录系统,支持多标签组合选择
/// ============================================================
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/storage/kv_storage.dart';
import '../../core/utils/logger.dart';
import 'jinrishici_models.dart';
import 'poetry_service.dart';
// ============================================================
// 聊天记录模型 — 统一持久化保存
// ============================================================
/// 聊天记录模型 — 统一保存所有消息(用户标签、系统提示、诗词推荐)
class PoetryChatRecord {
const PoetryChatRecord({
required this.type,
required this.content,
required this.time,
this.poetry,
this.tags,
this.id,
});
/// 消息类型: user / system / poetry
final String type;
/// 消息内容
final String content;
/// 时间
final String time;
/// 诗词完整数据(仅 poetry 类型有值)
final JinrishiciPoetry? poetry;
/// 诗词标签(仅 poetry 类型有值)
final List<String>? tags;
/// 唯一标识(用于删除)
final String? id;
Map<String, dynamic> toJson() => {
'type': type,
'content': content,
'time': time,
if (poetry != null) 'poetry': _poetryToMap(poetry!),
if (tags != null) 'tags': tags,
if (id != null) 'id': id,
};
factory PoetryChatRecord.fromJson(Map<String, dynamic> json) =>
PoetryChatRecord(
type: json['type'] as String? ?? 'system',
content: json['content'] as String? ?? '',
time: json['time'] as String? ?? '',
poetry: json['poetry'] != null
? _poetryFromMap(json['poetry'] as Map<String, dynamic>)
: null,
tags: (json['tags'] as List<dynamic>?)
?.map((e) => e.toString())
.toList(),
id: json['id'] as String?,
);
// 序列化辅助
static Map<String, dynamic> _poetryToMap(JinrishiciPoetry p) => {
'content': p.content,
'title': p.title,
'dynasty': p.dynasty,
'author': p.author,
'fullContent': p.fullContent,
'translate': p.translate,
'matchTags': p.matchTags,
'recommendedReason': p.recommendedReason,
'id': p.id,
'popularity': p.popularity,
};
static JinrishiciPoetry _poetryFromMap(Map<String, dynamic> m) =>
JinrishiciPoetry(
content: m['content'] as String? ?? '',
title: m['title'] as String? ?? '',
dynasty: m['dynasty'] as String? ?? '',
author: m['author'] as String? ?? '',
fullContent: (m['fullContent'] as List<dynamic>?)
?.map((e) => e.toString())
.toList() ??
[],
translate: (m['translate'] as List<dynamic>?)
?.map((e) => e.toString())
.toList() ??
[],
matchTags: (m['matchTags'] as List<dynamic>?)
?.map((e) => e.toString())
.toList() ??
[],
recommendedReason: m['recommendedReason'] as String? ?? '',
id: m['id']?.toString() ?? '',
popularity: m['popularity'] as int? ?? 0,
);
}
// ============================================================
// 诗词状态
// ============================================================
class PoetryState {
const PoetryState({
this.poetry,
this.userInfo,
this.isLoading = false,
this.isRefreshing = false,
this.error,
this.selectedTags = const [],
this.city,
this.chatRecords = const [],
});
/// 当前诗词(最新一首)
final JinrishiciPoetry? poetry;
/// 用户信息IP/地区/天气)
final JinrishiciUserInfo? userInfo;
final bool isLoading;
final bool isRefreshing;
final String? error;
/// 当前选中的预选词标签列表(支持多选组合)
final List<String> selectedTags;
/// 当前城市(来自设置页面)
final String? city;
/// 统一聊天记录 — 替代原 poetryHistory + chatRecords
final List<PoetryChatRecord> chatRecords;
JinrishiciPoetry get currentPoetry => poetry ?? JinrishiciPoetry.empty();
bool get hasData => poetry != null;
/// 聊天记录数量
int get recordCount => chatRecords.length;
/// 是否接近上限
bool get isNearLimit => chatRecords.length > 180;
/// 从聊天记录中提取所有诗词(用于日历视图等)
List<PoetryChatRecord> get poetryRecords =>
chatRecords.where((r) => r.type == 'poetry').toList();
PoetryState copyWith({
JinrishiciPoetry? poetry,
JinrishiciUserInfo? userInfo,
bool? isLoading,
bool? isRefreshing,
String? error,
bool clearError = false,
List<String>? selectedTags,
bool clearSelectedTags = false,
String? city,
bool clearCity = false,
List<PoetryChatRecord>? chatRecords,
}) {
return PoetryState(
poetry: poetry ?? this.poetry,
userInfo: userInfo ?? this.userInfo,
isLoading: isLoading ?? this.isLoading,
isRefreshing: isRefreshing ?? this.isRefreshing,
error: clearError ? null : (error ?? this.error),
selectedTags: clearSelectedTags ? [] : (selectedTags ?? this.selectedTags),
city: clearCity ? null : (city ?? this.city),
chatRecords: chatRecords ?? this.chatRecords,
);
}
}
// ============================================================
// 诗词状态管理器
// ============================================================
class PoetryNotifier extends Notifier<PoetryState> {
static const _cacheKey = 'poetry_cache_v2';
@override
PoetryState build() {
final cached = _loadFromCache();
if (cached != null) {
Future.microtask(loadPoetry).catchError((_) {});
return cached;
}
Future.microtask(loadPoetry).catchError((_) {});
return const PoetryState();
}
// ============================================================
// 缓存读写
// ============================================================
PoetryState? _loadFromCache() {
try {
if (!KvStorage.isReady) return null;
final raw = KvStorage.getString(_cacheKey, box: HiveBoxNames.feedCache);
if (raw == null || raw.isEmpty) return null;
final json = jsonDecode(raw) as Map<String, dynamic>;
final poetryJson = json['poetry'] as Map<String, dynamic>?;
final userInfoJson = json['userInfo'] as Map<String, dynamic>?;
return PoetryState(
poetry: poetryJson != null
? PoetryChatRecord._poetryFromMap(poetryJson)
: null,
userInfo: userInfoJson != null ? _userInfoFromMap(userInfoJson) : null,
selectedTags: (json['selectedTags'] as List<dynamic>?)
?.map((e) => e.toString())
.toList() ??
[],
city: json['city'] as String?,
chatRecords: (json['chatRecords'] as List<dynamic>?)
?.map((e) =>
PoetryChatRecord.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
);
} catch (e) {
Log.e('诗词缓存读取失败', e);
return null;
}
}
void _saveToCache(PoetryState state) {
try {
if (!KvStorage.isReady) return;
final json = <String, dynamic>{
'timestamp': DateTime.now().millisecondsSinceEpoch,
if (state.poetry != null)
'poetry': PoetryChatRecord._poetryToMap(state.poetry!),
if (state.userInfo != null)
'userInfo': _userInfoToMap(state.userInfo!),
'selectedTags': state.selectedTags,
if (state.city != null) 'city': state.city,
'chatRecords': state.chatRecords.map((r) => r.toJson()).toList(),
};
KvStorage.setString(
_cacheKey, jsonEncode(json), box: HiveBoxNames.feedCache);
} catch (e) {
Log.e('诗词缓存写入失败', e);
}
}
// ============================================================
// 序列化辅助
// ============================================================
Map<String, dynamic> _userInfoToMap(JinrishiciUserInfo u) => {
'ipAddress': u.ipAddress,
'region': u.region,
'weather': u.weather,
'temperature': u.temperature,
};
JinrishiciUserInfo _userInfoFromMap(Map<String, dynamic> m) =>
JinrishiciUserInfo(
ipAddress: m['ipAddress'] as String? ?? '',
region: m['region'] as String? ?? '',
weather: m['weather'] as String? ?? '',
temperature: m['temperature'] as String? ?? '',
);
// ============================================================
// 数据加载
// ============================================================
Future<void> loadPoetry() async {
final hasExistingData = state.hasData;
if (hasExistingData) {
state = state.copyWith(isRefreshing: true, clearError: true);
} else {
state = state.copyWith(isLoading: true, clearError: true);
}
JinrishiciPoetry? newPoetry;
JinrishiciUserInfo? newUserInfo;
try {
newPoetry = await PoetryService.fetchTodayPoetry();
} catch (e) {
Log.e('今日诗词获取失败', e);
}
try {
newUserInfo = await PoetryService.fetchUserInfo();
} catch (e) {
Log.e('用户信息获取失败', e);
}
if (newPoetry == null && newUserInfo == null) {
state = state.copyWith(
isLoading: false,
isRefreshing: false,
error: '诗词和用户信息均获取失败',
);
return;
}
// 添加诗词到聊天记录
final updatedRecords = List<PoetryChatRecord>.from(state.chatRecords);
if (newPoetry != null) {
updatedRecords.add(PoetryChatRecord(
type: 'system',
content: '今日诗词推荐',
time: DateTime.now().toIso8601String(),
id: 'sys_${DateTime.now().millisecondsSinceEpoch}',
));
updatedRecords.add(PoetryChatRecord(
type: 'poetry',
content: newPoetry.content,
time: DateTime.now().toIso8601String(),
poetry: newPoetry,
tags: newPoetry.matchTags,
id: 'poetry_${newPoetry.id}_${DateTime.now().millisecondsSinceEpoch}',
));
}
// 最多保留 200 条记录
if (updatedRecords.length > 200) {
updatedRecords.removeRange(0, updatedRecords.length - 200);
}
state = state.copyWith(
poetry: newPoetry ?? state.poetry,
userInfo: newUserInfo ?? state.userInfo,
chatRecords: updatedRecords,
isLoading: false,
isRefreshing: false,
clearError: true,
);
_saveToCache(state);
Log.i('今日诗词加载完成,聊天记录: ${updatedRecords.length}');
}
/// 按标签获取诗词 — 支持多标签组合
Future<void> loadPoetryByTag(String tag) async {
state = state.copyWith(
isRefreshing: true,
clearError: true,
selectedTags: [...state.selectedTags, tag],
);
JinrishiciPoetry? newPoetry;
JinrishiciUserInfo? newUserInfo;
try {
newPoetry = await PoetryService.fetchPoetryByTag(tag);
} catch (e) {
Log.e('按标签获取诗词失败: $tag', e);
}
try {
newUserInfo = await PoetryService.fetchUserInfo();
} catch (e) {
Log.e('用户信息获取失败', e);
}
if (newPoetry == null) {
state = state.copyWith(
isRefreshing: false,
error: '获取诗词失败,请稍后重试',
);
return;
}
// 添加诗词到聊天记录
final updatedRecords = List<PoetryChatRecord>.from(state.chatRecords);
updatedRecords.add(PoetryChatRecord(
type: 'poetry',
content: newPoetry.content,
time: DateTime.now().toIso8601String(),
poetry: newPoetry,
tags: newPoetry.matchTags,
id: 'poetry_${newPoetry.id}_${DateTime.now().millisecondsSinceEpoch}',
));
// 最多保留 200 条记录
if (updatedRecords.length > 200) {
updatedRecords.removeRange(0, updatedRecords.length - 200);
}
state = state.copyWith(
poetry: newPoetry,
userInfo: newUserInfo ?? state.userInfo,
chatRecords: updatedRecords,
isRefreshing: false,
clearError: true,
);
_saveToCache(state);
Log.i('按标签「$tag」获取诗词完成,聊天记录: ${updatedRecords.length}');
}
/// 更新城市设置
void setCity(String? city) {
state = state.copyWith(
city: city,
clearCity: city == null,
);
_saveToCache(state);
}
/// 添加聊天记录
void addChatRecord(PoetryChatRecord record) {
final updated = List<PoetryChatRecord>.from(state.chatRecords);
updated.add(record);
if (updated.length > 200) {
updated.removeRange(0, updated.length - 200);
}
state = state.copyWith(chatRecords: updated);
_saveToCache(state);
}
/// 删除单条聊天记录
void removeChatRecord(String id) {
final updated =
state.chatRecords.where((r) => r.id != id).toList();
state = state.copyWith(chatRecords: updated);
_saveToCache(state);
}
/// 清空聊天记录
void clearChatRecords() {
state = state.copyWith(chatRecords: []);
_saveToCache(state);
Log.i('诗词聊天记录已清空');
}
/// 清除选中标签
void clearSelectedTags() {
state = state.copyWith(clearSelectedTags: true);
_saveToCache(state);
}
/// 移除单个选中标签
void removeSelectedTag(String tag) {
final updated = state.selectedTags.where((t) => t != tag).toList();
state = state.copyWith(selectedTags: updated);
_saveToCache(state);
}
Future<void> refresh() async {
await loadPoetry();
}
}
final poetryProvider = NotifierProvider<PoetryNotifier, PoetryState>(
PoetryNotifier.new,
);