新增笔记功能 图标更新
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
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<NoteListItem> 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<NoteListItem>? 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<NoteListState> {
|
||||
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<void> _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<void> refresh() async {
|
||||
state = state.copyWith(loading: true, error: null);
|
||||
await _fetchFirstPage(
|
||||
keyword: state.keyword,
|
||||
tag: state.tag,
|
||||
sort: state.sort,
|
||||
location: state.location,
|
||||
);
|
||||
}
|
||||
|
||||
/// 上拉加载下一页
|
||||
Future<void> 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<NoteListItem> _mergeUnique(List<NoteListItem> old, List<NoteListItem> 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, NoteListState>(NoteListNotifier.new);
|
||||
|
||||
/// 笔记详情
|
||||
final noteDetailProvider =
|
||||
FutureProvider.autoDispose.family<NoteDetail, int>((ref, id) {
|
||||
return ref.watch(noteApiProvider).detail(id);
|
||||
});
|
||||
|
||||
/// 标签列表
|
||||
final noteTagsProvider = FutureProvider.autoDispose<List<String>>((ref) {
|
||||
return ref.watch(noteApiProvider).tags();
|
||||
});
|
||||
|
||||
/// 目录树
|
||||
final noteFolderTreeProvider = FutureProvider.autoDispose<FolderTree>((ref) {
|
||||
return ref.watch(noteApiProvider).folderTree();
|
||||
});
|
||||
Reference in New Issue
Block a user