Files
xianyan/lib/features/weather/weather_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

416 lines
15 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-23
/// 作用: 天气数据 + 天气-诗词关联推荐状态
/// 上次更新: 新增currentTag字段追踪预选词WeatherPoetrySession添加tag字段缓存支持tag
/// ============================================================
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:xianyan/core/utils/safe_json.dart';
import '../../../core/storage/kv_storage.dart';
import '../../../core/utils/logger.dart';
import '../../../core/services/data/jinrishici_sdk_service.dart';
import '../../../core/services/weather/weather_models.dart';
import '../../../core/services/weather/weather_service.dart';
import '../home/services/searchall_service.dart';
class WeatherPoetrySession {
const WeatherPoetrySession({
this.tag = '',
this.jinrishiciContent = '',
this.jinrishiciTitle = '',
this.jinrishiciAuthor = '',
this.matchedPoems = const [],
});
/// 用户发送的预选词(如:雨、春、梅花)
final String tag;
final String jinrishiciContent;
final String jinrishiciTitle;
final String jinrishiciAuthor;
final List<Map<String, dynamic>> matchedPoems;
}
class WeatherHistoryDay {
const WeatherHistoryDay({
required this.date,
required this.weather,
this.mood,
});
final String date;
final WeatherData weather;
final WeatherPoetryMood? mood;
Map<String, dynamic> toJson() => {
'date': date,
'weather': {
'city': weather.city,
'tem': weather.temp,
'wea': weather.weather,
'wea_img': '',
'humidity': weather.humidity,
'win': weather.windDirection,
'win_speed': weather.windPower,
'air': weather.aqi,
'air_level': weather.aqiLevel,
'tem_day': weather.temDay,
'tem_night': weather.temNight,
'update_time': weather.updateTime,
},
if (mood != null) 'moodIndex': mood!.index,
};
factory WeatherHistoryDay.fromJson(Map<String, dynamic> json) {
final weatherJson = json['weather'] as Map<String, dynamic>?;
final moodIndex = json['moodIndex'] == null ? null : SafeJson.parseInt(json['moodIndex']);
final safeMoodIndex = moodIndex?.clamp(0, WeatherPoetryMood.values.length - 1);
return WeatherHistoryDay(
date: json['date'] as String? ?? '',
weather: weatherJson != null ? WeatherData.fromTianqi(weatherJson) : WeatherData.empty(),
mood: safeMoodIndex != null ? WeatherPoetryMood.values[safeMoodIndex] : null,
);
}
}
class WeatherState {
const WeatherState({
this.weather,
this.mood,
this.currentTag = '',
this.matchedPoems = const [],
this.jinrishiciContent = '',
this.jinrishiciTitle = '',
this.jinrishiciAuthor = '',
this.poetrySessions = const [],
this.historyDays = const [],
this.isLoading = false,
this.isRefreshing = false,
this.error,
});
final WeatherData? weather;
final WeatherPoetryMood? mood;
/// 当前诗词对应的预选词
final String currentTag;
final List<Map<String, dynamic>> matchedPoems;
final String jinrishiciContent;
final String jinrishiciTitle;
final String jinrishiciAuthor;
final List<WeatherPoetrySession> poetrySessions;
final List<WeatherHistoryDay> historyDays;
final bool isLoading;
final bool isRefreshing;
final String? error;
bool get hasData => weather != null;
WeatherState copyWith({
WeatherData? weather,
WeatherPoetryMood? mood,
String? currentTag,
List<Map<String, dynamic>>? matchedPoems,
String? jinrishiciContent,
String? jinrishiciTitle,
String? jinrishiciAuthor,
List<WeatherPoetrySession>? poetrySessions,
List<WeatherHistoryDay>? historyDays,
bool? isLoading,
bool? isRefreshing,
String? error,
bool clearError = false,
}) {
return WeatherState(
weather: weather ?? this.weather,
mood: mood ?? this.mood,
currentTag: currentTag ?? this.currentTag,
matchedPoems: matchedPoems ?? this.matchedPoems,
jinrishiciContent: jinrishiciContent ?? this.jinrishiciContent,
jinrishiciTitle: jinrishiciTitle ?? this.jinrishiciTitle,
jinrishiciAuthor: jinrishiciAuthor ?? this.jinrishiciAuthor,
poetrySessions: poetrySessions ?? this.poetrySessions,
historyDays: historyDays ?? this.historyDays,
isLoading: isLoading ?? this.isLoading,
isRefreshing: isRefreshing ?? this.isRefreshing,
error: clearError ? null : (error ?? this.error),
);
}
}
class WeatherNotifier extends Notifier<WeatherState> {
static const _cacheKey = 'weather_poetry_cache';
@override
WeatherState build() {
final cached = _loadFromCache();
if (cached != null) {
Future.microtask(loadWeather).catchError((_) {});
return cached;
}
Future.microtask(loadWeather).catchError((_) {});
return const WeatherState();
}
// ============================================================
// 缓存读写
// ============================================================
WeatherState? _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 weatherJson = json['weather'] as Map<String, dynamic>?;
final moodIndex = json['moodIndex'] == null ? null : SafeJson.parseInt(json['moodIndex']);
final matchedPoems = (json['matchedPoems'] as List<dynamic>?)
?.map((e) => Map<String, dynamic>.from(e as Map))
.toList() ??
[];
final jinrishiciContent = json['jinrishiciContent'] as String? ?? '';
final jinrishiciTitle = json['jinrishiciTitle'] as String? ?? '';
final jinrishiciAuthor = json['jinrishiciAuthor'] as String? ?? '';
final sessions = (json['poetrySessions'] as List<dynamic>?)
?.map((e) => WeatherPoetrySession(
tag: (e as Map<String, dynamic>)['tag'] as String? ?? '',
jinrishiciContent: e['jinrishiciContent'] as String? ?? '',
jinrishiciTitle: e['jinrishiciTitle'] as String? ?? '',
jinrishiciAuthor: e['jinrishiciAuthor'] as String? ?? '',
matchedPoems: (e['matchedPoems'] as List<dynamic>?)
?.map((m) => Map<String, dynamic>.from(m as Map))
.toList() ??
[],
))
.toList() ??
[];
final safeMoodIndex = moodIndex?.clamp(0, WeatherPoetryMood.values.length - 1);
final historyDays = (json['historyDays'] as List<dynamic>?)
?.map((e) => WeatherHistoryDay.fromJson(e as Map<String, dynamic>))
.toList() ??
[];
return WeatherState(
weather: weatherJson != null ? WeatherData.fromTianqi(weatherJson) : null,
mood: safeMoodIndex != null ? WeatherPoetryMood.values[safeMoodIndex] : null,
currentTag: json['currentTag'] as String? ?? '',
matchedPoems: matchedPoems,
jinrishiciContent: jinrishiciContent,
jinrishiciTitle: jinrishiciTitle,
jinrishiciAuthor: jinrishiciAuthor,
poetrySessions: sessions,
historyDays: historyDays,
);
} catch (e) {
Log.e('天气缓存读取失败', e);
return null;
}
}
void _saveToCache(WeatherState state) {
try {
if (!KvStorage.isReady) return;
final json = <String, dynamic>{
'timestamp': DateTime.now().millisecondsSinceEpoch,
if (state.weather != null)
'weather': {
'city': state.weather!.city,
'tem': state.weather!.temp,
'wea': state.weather!.weather,
'wea_img': '',
'humidity': state.weather!.humidity,
'win': state.weather!.windDirection,
'win_speed': state.weather!.windPower,
'air': state.weather!.aqi,
'air_level': state.weather!.aqiLevel,
'tem_day': state.weather!.temDay,
'tem_night': state.weather!.temNight,
'update_time': state.weather!.updateTime,
},
if (state.mood != null) 'moodIndex': state.mood!.index,
'currentTag': state.currentTag,
'matchedPoems': state.matchedPoems,
'jinrishiciContent': state.jinrishiciContent,
'jinrishiciTitle': state.jinrishiciTitle,
'jinrishiciAuthor': state.jinrishiciAuthor,
'poetrySessions': state.poetrySessions
.map((s) => {
'tag': s.tag,
'jinrishiciContent': s.jinrishiciContent,
'jinrishiciTitle': s.jinrishiciTitle,
'jinrishiciAuthor': s.jinrishiciAuthor,
'matchedPoems': s.matchedPoems,
})
.toList(),
'historyDays': state.historyDays.map((h) => h.toJson()).toList(),
};
KvStorage.setString(_cacheKey, jsonEncode(json), box: HiveBoxNames.feedCache);
Log.i('天气数据已缓存');
} catch (e) {
Log.e('天气缓存写入失败', e);
}
}
// ============================================================
// 数据加载
// ============================================================
Future<void> loadWeather() async {
final hasExistingData = state.hasData;
if (hasExistingData) {
state = state.copyWith(isRefreshing: true, clearError: true);
} else {
state = state.copyWith(isLoading: true, clearError: true);
}
try {
final result = await WeatherService.fetchWeatherWithPoetry();
var sessions = List<WeatherPoetrySession>.from(state.poetrySessions);
if (state.jinrishiciContent.isNotEmpty) {
sessions.add(WeatherPoetrySession(
tag: state.currentTag,
jinrishiciContent: state.jinrishiciContent,
jinrishiciTitle: state.jinrishiciTitle,
jinrishiciAuthor: state.jinrishiciAuthor,
matchedPoems: state.matchedPoems,
));
}
if (sessions.length > 20) {
sessions = sessions.sublist(sessions.length - 20);
}
var history = List<WeatherHistoryDay>.from(state.historyDays);
final today = '${DateTime.now().year}-${DateTime.now().month.toString().padLeft(2, '0')}-${DateTime.now().day.toString().padLeft(2, '0')}';
history.removeWhere((h) => h.date == today);
history.insert(0, WeatherHistoryDay(
date: today,
weather: result.weather,
mood: result.mood,
));
if (history.length > 30) {
history = history.sublist(0, 30);
}
state = state.copyWith(
weather: result.weather,
mood: result.mood,
matchedPoems: result.matchedPoems,
jinrishiciContent: result.jinrishiciContent,
jinrishiciTitle: result.jinrishiciTitle,
jinrishiciAuthor: result.jinrishiciAuthor,
poetrySessions: sessions,
historyDays: history,
isLoading: false,
isRefreshing: false,
clearError: true,
);
_saveToCache(state);
Log.i('天气加载成功: ${result.weather.city},诗词历史: ${sessions.length}');
} catch (e) {
Log.e('天气加载失败', e);
state = state.copyWith(
isLoading: false,
isRefreshing: false,
error: e.toString(),
);
}
}
Future<void> refresh() async {
await loadWeather();
}
/// 任务5按预选词获取诗词
///
/// 用户发送预选词后,调用 jinrishici API 获取对应标签的诗词,
/// 替换当前显示的诗词内容,并添加到诗词历史会话中。
Future<void> loadPoetryByTag(String tag) async {
final cleanTag = tag.trim();
if (cleanTag.isEmpty) return;
state = state.copyWith(isRefreshing: true, clearError: true);
try {
// 保存当前诗词到历史会话
var sessions = List<WeatherPoetrySession>.from(state.poetrySessions);
if (state.jinrishiciContent.isNotEmpty) {
sessions.add(WeatherPoetrySession(
tag: state.currentTag,
jinrishiciContent: state.jinrishiciContent,
jinrishiciTitle: state.jinrishiciTitle,
jinrishiciAuthor: state.jinrishiciAuthor,
matchedPoems: state.matchedPoems,
));
}
if (sessions.length > 20) {
sessions = sessions.sublist(sessions.length - 20);
}
// 调用 jinrishici API 获取对应标签的诗词
final jinrishiciData = await JinrishiciSdkService.fetchSentenceByTag(cleanTag);
// 搜索匹配的本地诗词
final matchedPoems = await _searchByKeyword(cleanTag);
// 任务9审计修复直接从 jinrishiciData 提取数据,避免 state.weather!/state.mood! 空指针风险
// WeatherPoetryResult 的 weather/mood 字段在 loadPoetryByTag 场景下未使用,
// 直接提取 jinrishiciContent/Title/Author 即可
final data = jinrishiciData?['data'] as Map<String, dynamic>?;
final origin = data?['origin'] as Map<String, dynamic>?;
final jinrishiciContent = data?['content'] as String? ?? '';
final jinrishiciTitle = origin?['title'] as String? ?? '';
final dynasty = origin?['dynasty'] as String? ?? '';
final author = origin?['author'] as String? ?? '';
final jinrishiciAuthor = dynasty.isNotEmpty ? '$dynasty·$author' : author;
state = state.copyWith(
currentTag: cleanTag,
matchedPoems: matchedPoems,
jinrishiciContent: jinrishiciContent,
jinrishiciTitle: jinrishiciTitle,
jinrishiciAuthor: jinrishiciAuthor,
poetrySessions: sessions,
isRefreshing: false,
clearError: true,
);
_saveToCache(state);
Log.i('按预选词获取诗词成功: tag=$cleanTag, content=$jinrishiciContent');
} catch (e) {
Log.e('按预选词获取诗词失败: tag=$cleanTag', e);
state = state.copyWith(
isRefreshing: false,
error: '获取诗词失败: $e',
);
}
}
/// 按关键词搜索本地诗词库
Future<List<Map<String, dynamic>>> _searchByKeyword(String keyword) async {
try {
final result = await SearchAllService.fieldSearch(
type: 'poetry',
field: 'content',
keyword: keyword,
limit: 1,
);
return result.list.map((item) => {
'content': item.content,
'author': item.author,
'title': item.title,
'keyword': keyword,
}).toList();
} catch (e) {
Log.w('关键词诗词搜索失败: keyword=$keyword', e);
return [];
}
}
}
final weatherProvider = NotifierProvider<WeatherNotifier, WeatherState>(
WeatherNotifier.new,
);