import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../api/note_api.dart'; import '../core/network/api_exception.dart'; import '../models/note.dart'; /// 笔记列表页状态 class NoteListState { final List items; final bool loading; // 首屏 / 刷新 / 切筛选条件 final bool loadingMore; // 上拉加载下一页 final String? error; final int page; // 已加载到第几页,0 表示还没加载 final bool hasMore; final int total; final String keyword; final String tag; final String sort; // updated | created | title final NoteLocation location; const NoteListState({ this.items = const [], this.loading = false, this.loadingMore = false, this.error, this.page = 0, this.hasMore = false, this.total = 0, this.keyword = '', this.tag = '', this.sort = 'updated', this.location = NoteLocation.all, }); /// 真正的一条都没有(不是加载中、也不是出错) bool get isEmpty => items.isEmpty && !loading && error == null; /// 是否处于筛选态——决定空状态该显示"还没有笔记"还是"没有匹配结果" bool get hasFilter => keyword.isNotEmpty || tag.isNotEmpty; NoteListState copyWith({ List? items, bool? loading, bool? loadingMore, Object? error = _sentinel, int? page, bool? hasMore, int? total, String? keyword, String? tag, String? sort, NoteLocation? location, }) { return NoteListState( items: items ?? this.items, loading: loading ?? this.loading, loadingMore: loadingMore ?? this.loadingMore, error: error == _sentinel ? this.error : error as String?, page: page ?? this.page, hasMore: hasMore ?? this.hasMore, total: total ?? this.total, keyword: keyword ?? this.keyword, tag: tag ?? this.tag, sort: sort ?? this.sort, location: location ?? this.location, ); } static const _sentinel = Object(); } class NoteListNotifier extends Notifier { static const _pageSize = 20; Timer? _searchDebounce; @override NoteListState build() { ref.onDispose(() => _searchDebounce?.cancel()); // build() 里不能同步读写 state,所以丢到微任务里再发起首次加载 Future.microtask(() => _fetchFirstPage( keyword: '', tag: '', sort: 'updated', location: NoteLocation.all, )); return const NoteListState(loading: true); } /// 取第一页。参数由调用方传入而不是读 state, /// 是为了能在 build() 阶段安全调用。 Future _fetchFirstPage({ required String keyword, required String tag, required String sort, required NoteLocation location, }) async { try { final result = await ref.read(noteApiProvider).list( page: 1, size: _pageSize, scope: location.apiScope, folderId: location.folderId, sort: sort, keyword: keyword, tag: tag, ); if (!ref.mounted) return; state = state.copyWith( items: result.items, loading: false, error: null, page: 1, hasMore: result.hasMore, total: result.total, ); } on ApiException catch (e) { if (!ref.mounted) return; state = state.copyWith(loading: false, error: e.message); } catch (_) { if (!ref.mounted) return; state = state.copyWith(loading: false, error: '笔记加载失败'); } } /// 下拉刷新 / 详情页改动后刷新。保留当前筛选条件。 Future refresh() async { state = state.copyWith(loading: true, error: null); await _fetchFirstPage( keyword: state.keyword, tag: state.tag, sort: state.sort, location: state.location, ); } /// 上拉加载下一页 Future loadMore() async { if (state.loadingMore || !state.hasMore || state.loading) return; state = state.copyWith(loadingMore: true); try { final next = state.page + 1; final result = await ref.read(noteApiProvider).list( page: next, size: _pageSize, scope: state.location.apiScope, folderId: state.location.folderId, sort: state.sort, keyword: state.keyword, tag: state.tag, ); if (!ref.mounted) return; state = state.copyWith( // 用 id 去重:加载过程中如果有笔记被置顶/更新, // 它可能同时出现在两页里,直接拼接会出现重复卡片 items: _mergeUnique(state.items, result.items), loadingMore: false, page: next, hasMore: result.hasMore, total: result.total, ); } catch (_) { if (!ref.mounted) return; state = state.copyWith(loadingMore: false); } } List _mergeUnique(List old, List added) { final seen = old.map((e) => e.id).toSet(); return [...old, ...added.where((e) => !seen.contains(e.id))]; } /// 搜索关键词。带防抖,避免每敲一个字都打一次接口。 void setKeyword(String keyword) { if (keyword == state.keyword) return; state = state.copyWith(keyword: keyword); _searchDebounce?.cancel(); _searchDebounce = Timer(const Duration(milliseconds: 350), () { if (!ref.mounted) return; refresh(); }); } /// 标签筛选 void setTag(String tag) { if (tag == state.tag) return; state = state.copyWith(tag: tag); refresh(); } /// 切换浏览的目录 void setLocation(NoteLocation location) { if (location.scope == state.location.scope && location.folderId == state.location.folderId) { return; } state = state.copyWith(location: location); refresh(); } /// 切换排序方式 void setSort(String sort) { if (sort == state.sort) return; state = state.copyWith(sort: sort); refresh(); } } /// 列表用普通 NotifierProvider(不加 autoDispose): /// 从详情页返回时搜索关键词、标签筛选和所在目录都要还在,不能被回收重建。 final noteListProvider = NotifierProvider(NoteListNotifier.new); /// 笔记详情 final noteDetailProvider = FutureProvider.autoDispose.family((ref, id) { return ref.watch(noteApiProvider).detail(id); }); /// 标签列表 final noteTagsProvider = FutureProvider.autoDispose>((ref) { return ref.watch(noteApiProvider).tags(); }); /// 目录树 final noteFolderTreeProvider = FutureProvider.autoDispose((ref) { return ref.watch(noteApiProvider).folderTree(); });