Files
xianyan/lib/features/profile/presentation/spotlight_search/spotlight_search_provider.dart
Developer 83720002e6 feat: 新增工作台模式、系统托盘,修复多平台兼容性问题
1. 新增工作台三栏布局模式,适配宽屏设备
2. 添加跨平台系统托盘支持,新增托盘图标资源
3. 修复工作台模式下导航返回异常问题
4. 统一JSON类型安全解析,替换硬类型转换
5. 增加macOS深度链接支持,统一渠道分发信息
6. 优化部分页面生命周期和状态加载逻辑
7. 移除废弃的nearby_connections依赖
2026-06-19 06:43:55 +08:00

319 lines
9.1 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 — Spotlight搜索状态管理
/// 创建时间: 2026-06-07
/// 更新时间: 2026-06-19
/// 作用: 管理Spotlight搜索的查询、结果、选中索引、最近搜索等状态
/// 上次更新: 类型安全修复(int vs num): _loadRecentVisits 使用 SafeJson.parseInt
/// ============================================================
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/utils/platform/platform_utils.dart' as pu;
import 'spotlight_search_data.dart';
// ============================================================
// 搜索状态
// ============================================================
/// Spotlight搜索状态
class SpotlightSearchState {
const SpotlightSearchState({
this.query = '',
this.results = const [],
this.selectedIndex = -1,
this.recentSearches = const [],
this.suggestions = const [],
this.isOpen = false,
});
/// 当前搜索关键词
final String query;
/// 搜索结果列表
final List<SpotlightItem> results;
/// 当前选中索引(-1表示无选中
final int selectedIndex;
/// 最近搜索关键词
final List<String> recentSearches;
/// 搜索建议列表输入2字即触发
final List<SpotlightItem> suggestions;
/// 浮层是否打开
final bool isOpen;
/// 按分类分组的结果
Map<SpotlightCategory, List<SpotlightItem>> get groupedResults =>
SpotlightSearchData.groupByCategory(results);
/// 展平后的分组结果(按分类顺序排列,含分类标题)
List<SpotlightGroupEntry> get flatEntries {
final entries = <SpotlightGroupEntry>[];
for (final cat in SpotlightCategory.values) {
final items = groupedResults[cat];
if (items != null && items.isNotEmpty) {
entries.add(SpotlightCategoryEntry(cat));
for (final item in items) {
entries.add(SpotlightItemEntry(item));
}
}
}
return entries;
}
/// 复制并修改
SpotlightSearchState copyWith({
String? query,
List<SpotlightItem>? results,
int? selectedIndex,
List<String>? recentSearches,
List<SpotlightItem>? suggestions,
bool? isOpen,
}) {
return SpotlightSearchState(
query: query ?? this.query,
results: results ?? this.results,
selectedIndex: selectedIndex ?? this.selectedIndex,
recentSearches: recentSearches ?? this.recentSearches,
suggestions: suggestions ?? this.suggestions,
isOpen: isOpen ?? this.isOpen,
);
}
}
// ============================================================
// 分组条目(用于展平显示)
// ============================================================
/// 分组条目 — 分类标题或搜索项
sealed class SpotlightGroupEntry {
const SpotlightGroupEntry();
}
/// 分类标题条目
class SpotlightCategoryEntry extends SpotlightGroupEntry {
const SpotlightCategoryEntry(this.category);
final SpotlightCategory category;
}
/// 搜索项条目
class SpotlightItemEntry extends SpotlightGroupEntry {
const SpotlightItemEntry(this.item);
final SpotlightItem item;
}
// ============================================================
// 搜索Notifier
// ============================================================
/// Spotlight搜索状态管理器
class SpotlightSearchNotifier extends Notifier<SpotlightSearchState> {
static const _kRecentSearches = 'spotlight_recent_searches';
static const _kRecentVisits = 'spotlight_recent_visits';
static const _maxRecentCount = 8;
@override
SpotlightSearchState build() {
return SpotlightSearchState(
recentSearches: _loadRecentSearches(),
);
}
// ---- 浮层控制 ----
/// 打开搜索浮层
void open() {
state = state.copyWith(isOpen: true, selectedIndex: -1);
}
/// 关闭搜索浮层
void close() {
state = state.copyWith(
isOpen: false,
query: '',
results: [],
selectedIndex: -1,
);
}
// ---- 搜索操作 ----
/// 更新搜索关键词并执行搜索
void updateQuery(String query) {
final trimmed = query.trim();
var results = SpotlightSearchData.search(trimmed);
// iOS端不返回工具中心结果
if (pu.isIOS) {
results = results
.where((item) => item.category != SpotlightCategory.tool)
.toList();
}
final sortedResults = _sortByRecentVisit(results);
final suggestions = trimmed.length >= 2
? SpotlightSearchData.suggest(trimmed)
: <SpotlightItem>[];
state = state.copyWith(
query: trimmed,
results: sortedResults,
suggestions: suggestions,
selectedIndex: sortedResults.isNotEmpty ? 0 : -1,
);
}
// ---- 键盘导航 ----
/// 选中上一项
void selectPrevious() {
if (state.results.isEmpty) return;
final current = state.selectedIndex;
final newIndex = current <= 0
? state.results.length - 1
: current - 1;
state = state.copyWith(selectedIndex: newIndex);
}
/// 选中下一项
void selectNext() {
if (state.results.isEmpty) return;
final current = state.selectedIndex;
final newIndex = current >= state.results.length - 1
? 0
: current + 1;
state = state.copyWith(selectedIndex: newIndex);
}
/// 确认当前选中项返回选中的Item
SpotlightItem? confirmSelection() {
if (state.selectedIndex < 0 ||
state.selectedIndex >= state.results.length) {
return null;
}
final item = state.results[state.selectedIndex];
_addRecentSearch(item.name);
_addRecentVisit(item.route);
return item;
}
/// 选择指定搜索项
SpotlightItem? selectItem(int index) {
if (index < 0 || index >= state.results.length) return null;
final item = state.results[index];
_addRecentSearch(item.name);
_addRecentVisit(item.route);
return item;
}
// ---- 最近搜索 ----
/// 添加最近搜索关键词
void _addRecentSearch(String keyword) {
final updated = List<String>.from(state.recentSearches)
..remove(keyword)
..insert(0, keyword);
if (updated.length > _maxRecentCount) {
updated.removeRange(_maxRecentCount, updated.length);
}
_saveRecentSearches(updated);
state = state.copyWith(recentSearches: updated);
}
/// 删除最近搜索
void removeRecentSearch(String keyword) {
final updated = List<String>.from(state.recentSearches)..remove(keyword);
_saveRecentSearches(updated);
state = state.copyWith(recentSearches: updated);
}
/// 清空最近搜索
void clearRecentSearches() {
_saveRecentSearches([]);
state = state.copyWith(recentSearches: []);
}
// ---- 最近访问记录 ----
/// 添加最近访问记录
void _addRecentVisit(String route) {
final visits = _loadRecentVisits();
visits[route] = DateTime.now().millisecondsSinceEpoch;
_saveRecentVisits(visits);
}
/// 搜索结果排序(最近访问优先)
List<SpotlightItem> _sortByRecentVisit(List<SpotlightItem> items) {
final visits = _loadRecentVisits();
final sorted = List<SpotlightItem>.from(items);
sorted.sort((a, b) {
final aTime = visits[a.route] ?? 0;
final bTime = visits[b.route] ?? 0;
return bTime.compareTo(aTime); // 最近访问排前面
});
return sorted;
}
// ---- 持久化 ----
/// 从本地存储加载最近搜索
List<String> _loadRecentSearches() {
try {
final raw = KvStorage.getString(_kRecentSearches);
if (raw != null && raw.isNotEmpty) {
final decoded = jsonDecode(raw) as List<dynamic>;
return decoded.map((e) => e.toString()).toList();
}
} catch (e) {
Log.w('Spotlight搜索: 读取最近搜索失败 $e');
}
return [];
}
/// 保存最近搜索到本地存储
void _saveRecentSearches(List<String> searches) {
try {
KvStorage.setString(_kRecentSearches, jsonEncode(searches));
} catch (e) {
Log.w('Spotlight搜索: 保存最近搜索失败 $e');
}
}
/// 从本地存储加载最近访问记录
Map<String, int> _loadRecentVisits() {
try {
final raw = KvStorage.getString(_kRecentVisits);
if (raw != null && raw.isNotEmpty) {
final decoded = jsonDecode(raw) as Map<String, dynamic>;
return decoded.map((k, v) => MapEntry(k, SafeJson.parseInt(v)));
}
} catch (e) {
Log.w('Spotlight搜索: 读取最近访问记录失败 $e');
}
return {};
}
/// 保存最近访问记录到本地存储
void _saveRecentVisits(Map<String, int> visits) {
try {
KvStorage.setString(_kRecentVisits, jsonEncode(visits));
} catch (e) {
Log.w('Spotlight搜索: 保存最近访问记录失败 $e');
}
}
}
// ============================================================
// Provider
// ============================================================
/// Spotlight搜索Provider
final spotlightSearchProvider =
NotifierProvider<SpotlightSearchNotifier, SpotlightSearchState>(
SpotlightSearchNotifier.new,
);