1. 新增工作台三栏布局模式,适配宽屏设备 2. 添加跨平台系统托盘支持,新增托盘图标资源 3. 修复工作台模式下导航返回异常问题 4. 统一JSON类型安全解析,替换硬类型转换 5. 增加macOS深度链接支持,统一渠道分发信息 6. 优化部分页面生命周期和状态加载逻辑 7. 移除废弃的nearby_connections依赖
319 lines
9.1 KiB
Dart
319 lines
9.1 KiB
Dart
/// ============================================================
|
||
/// 闲言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,
|
||
);
|