Files
xianyan/lib/features/discover/providers/tool_center_provider.dart
2026-06-07 18:20:26 +08:00

629 lines
18 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-04-26
/// 更新时间: 2026-05-30
/// 作用: 工具中心面板状态 — 工具列表/搜索/面板开关/持久化/网络监测
/// 上次更新: 修复隐私问题 — 移除connectivity_plus直接调用改用全局ConnectivityService避免触发附近设备权限
/// ============================================================
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:xianyan/core/storage/kv_storage.dart';
import 'package:xianyan/core/services/network/connectivity_service.dart';
import 'package:xianyan/core/utils/logger.dart';
import '../models/search_type.dart';
import '../models/tool_item.dart';
import '../services/tool_api_service.dart';
/// 工具中心状态
class ToolCenterState {
const ToolCenterState({
this.tools = defaultTools,
this.isPanelOpen = false,
this.searchQuery = '',
this.searchHistory = const [],
this.isLoading = true,
this.healthStatus = const {},
this.isOnline = true,
this.needsPermissionCheck = false,
});
/// 全部工具列表
final List<ToolItem> tools;
/// 面板是否展开
final bool isPanelOpen;
/// 搜索关键词
final String searchQuery;
/// 搜索历史
final List<String> searchHistory;
/// 是否正在加载持久化数据
final bool isLoading;
/// 工具健康状态 {toolId: isHealthy}
final Map<String, bool> healthStatus;
/// 是否在线
final bool isOnline;
/// 是否需要权限检查Android 12+ 附近设备权限未授予时为 true
final bool needsPermissionCheck;
/// 最近使用(按 lastUsedAt 降序最多4个排除未使用的
List<ToolItem> get recentTools {
final used = tools.where((t) => t.lastUsedAt != null).toList();
used.sort((a, b) {
if (a.isPinned != b.isPinned) return a.isPinned ? -1 : 1;
return b.lastUsedAt!.compareTo(a.lastUsedAt!);
});
return used.take(4).toList();
}
/// 推荐工具
List<ToolItem> get recommendedTools {
return tools.where((t) => t.isRecommended).toList();
}
/// 收藏的工具
List<ToolItem> get favoritedTools {
return tools.where((t) => t.isFavorited).toList();
}
/// 已删除的工具
List<ToolItem> get deletedTools {
return tools.where((t) => t.isDeleted).toList();
}
/// 可见工具(未删除)
List<ToolItem> get visibleTools {
return tools.where((t) => !t.isDeleted).toList();
}
/// 全部工具(按使用次数降序,排除已删除)
List<ToolItem> get allToolsSorted {
final sorted = visibleTools.toList();
sorted.sort((a, b) {
if (a.isPinned != b.isPinned) return a.isPinned ? -1 : 1;
return b.useCount.compareTo(a.useCount);
});
return sorted;
}
/// 工具排行榜按使用次数Top10排除已删除
List<ToolItem> get topTools {
final sorted = visibleTools.toList();
sorted.sort((a, b) => b.useCount.compareTo(a.useCount));
return sorted.take(10).toList();
}
/// 搜索过滤结果
List<ToolItem> get searchResults {
if (searchQuery.isEmpty) return [];
final q = searchQuery.toLowerCase();
return tools
.where(
(t) =>
t.name.toLowerCase().contains(q) ||
t.emoji.contains(q) ||
(t.description?.toLowerCase().contains(q) ?? false) ||
t.category.label.contains(q) ||
t.tags.any((tag) => tag.toLowerCase().contains(q)),
)
.toList();
}
Map<ToolCategory, List<ToolItem>> get toolsGroupedByCategory {
final map = <ToolCategory, List<ToolItem>>{};
for (final tool in allToolsSorted) {
(map[tool.category] ??= []).add(tool);
}
return map;
}
ToolCenterState copyWith({
List<ToolItem>? tools,
bool? isPanelOpen,
String? searchQuery,
List<String>? searchHistory,
bool? isLoading,
Map<String, bool>? healthStatus,
bool? isOnline,
bool? needsPermissionCheck,
}) {
return ToolCenterState(
tools: tools ?? this.tools,
isPanelOpen: isPanelOpen ?? this.isPanelOpen,
searchQuery: searchQuery ?? this.searchQuery,
searchHistory: searchHistory ?? this.searchHistory,
isLoading: isLoading ?? this.isLoading,
healthStatus: healthStatus ?? this.healthStatus,
isOnline: isOnline ?? this.isOnline,
needsPermissionCheck: needsPermissionCheck ?? this.needsPermissionCheck,
);
}
}
/// 工具中心 Notifier
class ToolCenterNotifier extends Notifier<ToolCenterState> {
@override
ToolCenterState build() {
ref.onDispose(_onDispose);
Future.microtask(() {
_loadPersistedData().catchError((Object e) {
Log.e('ToolCenterNotifier._loadPersistedData 失败', e);
});
_initConnectivity();
_validateSearchTypes();
}).catchError((_) {});
return const ToolCenterState();
}
static const _keyPrefix = 'tool_';
StreamSubscription<NetworkType>? _connectivitySub;
void _initConnectivity() {
final isOnline = ConnectivityService.isOnline;
if (!isOnline) {
final updatedTools = state.tools.map((t) {
if (t.toolType == ToolType.online) {
return t.copyWith(status: ToolStatus.offline);
}
return t;
}).toList();
state = state.copyWith(isOnline: false, tools: updatedTools);
}
Log.i('ToolCenter: 初始网络状态 在线=$isOnline');
_connectivitySub = ConnectivityService.onTypeChange.listen((type) {
final nowOnline = type != NetworkType.none;
if (nowOnline != state.isOnline) {
final updatedTools = state.tools.map((t) {
if (t.toolType == ToolType.online) {
return t.copyWith(
status: nowOnline ? ToolStatus.available : ToolStatus.offline,
);
}
return t;
}).toList();
state = state.copyWith(isOnline: nowOnline, tools: updatedTools);
Log.i('ToolCenter: 网络状态变更 在线=$nowOnline type=$type');
if (nowOnline) {
checkAllHealth();
}
}
});
}
void _onDispose() {
_connectivitySub?.cancel();
_connectivitySub = null;
}
/// 切换面板开关
void togglePanel() {
state = state.copyWith(isPanelOpen: !state.isPanelOpen);
}
/// 打开面板
void openPanel() {
if (!state.isPanelOpen) {
state = state.copyWith(isPanelOpen: true);
}
}
/// 关闭面板
void closePanel() {
if (state.isPanelOpen) {
state = state.copyWith(isPanelOpen: false, searchQuery: '');
}
}
/// 清除权限检查标志(用户已处理权限后调用)
void clearPermissionCheck() {
if (state.needsPermissionCheck) {
state = state.copyWith(needsPermissionCheck: false);
}
}
/// 记录工具使用
void recordToolUse(String toolId) {
final updated = state.tools.map((t) {
if (t.id == toolId) return t.recordUse();
return t;
}).toList();
state = state.copyWith(tools: updated);
_persistToolUsage(toolId);
}
/// 切换固定状态
void togglePin(String toolId) {
final updated = state.tools.map((t) {
if (t.id == toolId) return t.copyWith(isPinned: !t.isPinned);
return t;
}).toList();
state = state.copyWith(tools: updated);
_persistToolPin(toolId, updated.firstWhere((t) => t.id == toolId).isPinned);
}
/// 切换收藏状态
void toggleFavorite(String toolId) {
final updated = state.tools.map((t) {
if (t.id == toolId) return t.copyWith(isFavorited: !t.isFavorited);
return t;
}).toList();
state = state.copyWith(tools: updated);
_persistToolFavorite(
toolId,
updated.firstWhere((t) => t.id == toolId).isFavorited,
);
}
/// 删除工具(标记为已删除)
void deleteTool(String toolId) {
final updated = state.tools.map((t) {
if (t.id == toolId) return t.copyWith(isDeleted: true);
return t;
}).toList();
state = state.copyWith(tools: updated);
_persistToolDeleted(toolId, true);
Log.i('工具已删除: $toolId');
}
/// 恢复已删除的工具
void restoreTool(String toolId) {
final updated = state.tools.map((t) {
if (t.id == toolId) return t.copyWith(isDeleted: false);
return t;
}).toList();
state = state.copyWith(tools: updated);
_persistToolDeleted(toolId, false);
Log.i('工具已恢复: $toolId');
}
/// 恢复所有已删除的工具
void restoreAllTools() {
final deletedIds = state.deletedTools.map((t) => t.id).toList();
final updated = state.tools.map((t) {
if (t.isDeleted) return t.copyWith(isDeleted: false);
return t;
}).toList();
state = state.copyWith(tools: updated);
for (final id in deletedIds) {
_persistToolDeleted(id, false);
}
Log.i('所有已删除工具已恢复');
}
/// 评分
void rateTool(String toolId, double rating) {
final updated = state.tools.map((t) {
if (t.id == toolId) {
final newCount = t.ratingCount + 1;
final newRating = ((t.rating * t.ratingCount) + rating) / newCount;
return t.copyWith(
rating: (newRating * 10).round() / 10,
ratingCount: newCount,
);
}
return t;
}).toList();
state = state.copyWith(tools: updated);
_persistToolRating(toolId, updated.firstWhere((t) => t.id == toolId));
}
/// 健康检测
Future<void> checkToolHealth(String toolId) async {
final tool = state.tools.firstWhere((t) => t.id == toolId);
if (tool.apiPath == null) {
state = state.copyWith(
healthStatus: {...state.healthStatus, toolId: true},
);
return;
}
try {
final isHealthy = await ToolApiService.healthCheck(tool.apiPath!);
final updated = state.tools.map((t) {
if (t.id == toolId) {
return t.copyWith(
status: isHealthy ? ToolStatus.available : ToolStatus.offline,
);
}
return t;
}).toList();
state = state.copyWith(
tools: updated,
healthStatus: {...state.healthStatus, toolId: isHealthy},
);
} catch (e) {
Log.e('工具健康检测失败: $toolId', e);
state = state.copyWith(
healthStatus: {...state.healthStatus, toolId: false},
);
}
}
/// 批量健康检测
Future<void> checkAllHealth() async {
final onlineTools = state.tools.where((t) => t.apiPath != null).toList();
for (final tool in onlineTools) {
await checkToolHealth(tool.id);
}
}
/// 搜索
void search(String query) {
state = state.copyWith(searchQuery: query);
}
/// 提交搜索(加入历史)
void submitSearch(String query) {
if (query.trim().isEmpty) return;
final history = List<String>.from(state.searchHistory);
history.remove(query);
history.insert(0, query);
if (history.length > 5) history.removeRange(5, history.length);
state = state.copyWith(searchHistory: history, searchQuery: query);
_persistSearchHistory(history);
}
/// 清空搜索
void clearSearch() {
state = state.copyWith(searchQuery: '');
}
/// 清空搜索历史
void clearSearchHistory() {
state = state.copyWith(searchHistory: []);
_removeSearchHistory();
}
/// 换一批推荐
void refreshRecommendations() {
final nonRecommended = state.tools
.where((t) => !t.isRecommended && !t.isNew)
.toList();
final currentRecommended = state.tools
.where((t) => t.isRecommended)
.toList();
if (nonRecommended.isEmpty) return;
nonRecommended.shuffle();
final newRecId = nonRecommended.first.id;
final oldRecId = currentRecommended.isNotEmpty
? currentRecommended.last.id
: null;
final updated = state.tools.map((t) {
if (t.id == newRecId) return t.copyWith(isRecommended: true);
if (t.id == oldRecId) return t.copyWith(isRecommended: false);
return t;
}).toList();
state = state.copyWith(tools: updated);
}
/// 从持久化加载工具使用数据
Future<void> _loadPersistedData() async {
try {
final updated = <ToolItem>[];
for (final tool in defaultTools) {
final useCount = KvStorage.getInt('${_keyPrefix}use_${tool.id}') ?? 0;
final lastUsedMs = KvStorage.getInt('${_keyPrefix}last_${tool.id}');
final isPinned =
KvStorage.getBool('${_keyPrefix}pin_${tool.id}') ?? false;
final isFavorited =
KvStorage.getBool('${_keyPrefix}fav_${tool.id}') ?? false;
// 代码中 isDeleted=true 表示强制隐藏(如歌词大全/故事大全/作文大全),
// 持久化数据不应覆盖强制隐藏状态
final persistedDeleted =
KvStorage.getBool('${_keyPrefix}del_${tool.id}');
final isDeleted = tool.isDeleted || (persistedDeleted ?? false);
final rating =
KvStorage.getDouble('${_keyPrefix}rating_${tool.id}') ?? 0.0;
final ratingCount =
KvStorage.getInt('${_keyPrefix}rating_count_${tool.id}') ?? 0;
updated.add(
tool.copyWith(
useCount: useCount,
lastUsedAt: lastUsedMs != null
? DateTime.fromMillisecondsSinceEpoch(lastUsedMs)
: null,
isPinned: isPinned,
isFavorited: isFavorited,
isDeleted: isDeleted,
rating: rating,
ratingCount: ratingCount,
),
);
}
final history = KvStorage.getSearchHistory();
final sortOrder = KvStorage.getToolSortOrder();
state = state.copyWith(
tools: sortOrder.isNotEmpty
? _applySortOrder(updated, sortOrder)
: updated,
searchHistory: history,
isLoading: false,
);
Log.i('工具中心持久化数据加载完成 (Hive)');
} catch (e) {
Log.e('工具中心持久化数据加载失败', e);
state = state.copyWith(isLoading: false);
}
}
/// 持久化使用次数和时间
Future<void> _persistToolUsage(String toolId) async {
try {
final tool = state.tools.firstWhere((t) => t.id == toolId);
await KvStorage.setInt('${_keyPrefix}use_$toolId', tool.useCount);
if (tool.lastUsedAt != null) {
await KvStorage.setInt(
'${_keyPrefix}last_$toolId',
tool.lastUsedAt!.millisecondsSinceEpoch,
);
}
} catch (e) {
Log.e('持久化工具使用数据失败', e);
}
}
Future<void> _persistToolPin(String toolId, bool isPinned) async {
try {
await KvStorage.setBool('${_keyPrefix}pin_$toolId', isPinned);
} catch (e) {
Log.e('持久化工具固定状态失败', e);
}
}
Future<void> _persistToolFavorite(String toolId, bool isFavorited) async {
try {
await KvStorage.setBool('${_keyPrefix}fav_$toolId', isFavorited);
} catch (e) {
Log.e('持久化工具收藏状态失败', e);
}
}
Future<void> _persistToolDeleted(String toolId, bool isDeleted) async {
try {
await KvStorage.setBool('${_keyPrefix}del_$toolId', isDeleted);
} catch (e) {
Log.e('持久化工具删除状态失败', e);
}
}
Future<void> _persistToolRating(String toolId, ToolItem tool) async {
try {
await KvStorage.setDouble('${_keyPrefix}rating_$toolId', tool.rating);
await KvStorage.setInt(
'${_keyPrefix}rating_count_$toolId',
tool.ratingCount,
);
} catch (e) {
Log.e('持久化工具评分失败', e);
}
}
Future<void> _persistSearchHistory(List<String> history) async {
try {
await KvStorage.setSearchHistory(history);
} catch (e) {
Log.e('持久化搜索历史失败', e);
}
}
Future<void> _removeSearchHistory() async {
try {
await KvStorage.clearSearchHistory();
} catch (e) {
Log.e('删除搜索历史失败', e);
}
}
/// 校验 SearchType 枚举与后端 API 支持的类型是否一致
///
/// 列表来源:
/// - hanziSearchSupported: Hanzi.php $searchMap 的所有 key
/// - searchAllSupported: Searchall.php $sourceMap 中前端使用的 key + 'all'
void _validateSearchTypes() {
SearchType.validateAll(
hanziSearchSupported: const [
'poetry',
'brainteaser',
'couplet',
'wisdom',
'story',
'saying',
'riddle',
'xiehouyu',
'zuowen',
'why',
'drug',
'food',
'herbal',
'pianfang',
'tisana',
'changshi',
'lyric',
],
searchAllSupported: const [
'cs',
'zgjm',
'illness',
'surname',
'jieqi',
'nation',
'joke',
'all',
],
);
}
/// 保存工具排序偏好
Future<void> saveToolSortOrder(List<String> toolIds) async {
try {
await KvStorage.setToolSortOrder(toolIds);
final sorted = _applySortOrder(state.tools, toolIds);
state = state.copyWith(tools: sorted);
Log.i('工具排序偏好已保存');
} catch (e) {
Log.e('保存工具排序偏好失败', e);
}
}
/// 按用户偏好排序工具列表
List<ToolItem> _applySortOrder(List<ToolItem> tools, List<String> order) {
if (order.isEmpty) return tools;
final toolMap = {for (final t in tools) t.id: t};
final sorted = <ToolItem>[];
for (final id in order) {
final tool = toolMap[id];
if (tool != null) {
sorted.add(tool);
toolMap.remove(id);
}
}
sorted.addAll(toolMap.values);
return sorted;
}
/// 按工具ID记录搜索历史
Future<void> recordToolSearch(String toolId, String keyword) async {
if (keyword.trim().isEmpty) return;
try {
await KvStorage.addToolSearchHistory(toolId, keyword);
} catch (e) {
Log.e('记录工具搜索历史失败', e);
}
}
/// 获取指定工具的搜索历史
List<String> getToolSearchHistory(String toolId) {
return KvStorage.getToolSearchHistory(toolId);
}
/// 清除指定工具的搜索历史
Future<void> clearToolSearchHistory(String toolId) async {
try {
await KvStorage.clearToolSearchHistory(toolId);
} catch (e) {
Log.e('清除工具搜索历史失败', e);
}
}
}
/// 工具中心 Provider
final toolCenterProvider =
NotifierProvider<ToolCenterNotifier, ToolCenterState>(
ToolCenterNotifier.new,
);