feat(library): 收藏与观看历史支持分页
This commit is contained in:
1 parent
63d3b0f7ca
commit
235b50598a
24 files changed
+1015
-105
No files matched your search
@@ -10,13 +10,22 @@ import '../repositories/favorite_repository.dart';
|
||||
/// 点击收藏先乐观更新(立刻变色),失败再回滚并给出提示 —— 网络抖动时不至于
|
||||
/// "点了没反应"或"看起来收藏了其实没有"。
|
||||
class FavoriteController extends ChangeNotifier {
|
||||
FavoriteController(this._repository);
|
||||
FavoriteController(this._repository, {this.pageSize = defaultPageSize});
|
||||
|
||||
static const int defaultPageSize = 20;
|
||||
|
||||
final FavoriteRepository _repository;
|
||||
final int pageSize;
|
||||
final Set<String> _saved = {};
|
||||
List<Drama> cards = const [];
|
||||
int total = 0;
|
||||
bool loading = false;
|
||||
bool loadingMore = false;
|
||||
bool hasMore = false;
|
||||
String? error;
|
||||
String? loadMoreError;
|
||||
int _pageIndex = 0;
|
||||
int _requestSeq = 0;
|
||||
bool _disposed = false;
|
||||
|
||||
Set<String> get savedIds => Set.unmodifiable(_saved);
|
||||
@@ -29,20 +38,66 @@ class FavoriteController extends ChangeNotifier {
|
||||
|
||||
/// 拉收藏列表:列表既用于"我的"页展示,也用于校准 [savedIds]。
|
||||
Future<void> load() async {
|
||||
final request = ++_requestSeq;
|
||||
loading = true;
|
||||
loadingMore = false;
|
||||
error = null;
|
||||
loadMoreError = null;
|
||||
notify();
|
||||
try {
|
||||
final list = await _repository.list();
|
||||
final page = await _repository.page(pageIndex: 1, pageSize: pageSize);
|
||||
if (_disposed || request != _requestSeq) return;
|
||||
final list = _uniqueCards(page.list);
|
||||
cards = list;
|
||||
_saved
|
||||
..clear()
|
||||
..addAll(list.map((drama) => drama.id));
|
||||
total = page.total;
|
||||
_pageIndex = 1;
|
||||
hasMore = page.list.isNotEmpty && cards.length < total;
|
||||
} catch (_) {
|
||||
error = 'favoritesError';
|
||||
if (!_disposed && request == _requestSeq) {
|
||||
error = 'favoritesError';
|
||||
}
|
||||
} finally {
|
||||
loading = false;
|
||||
notify();
|
||||
if (!_disposed && request == _requestSeq) {
|
||||
loading = false;
|
||||
notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 追加下一页;正在刷新、已在加载或无更多时直接忽略。
|
||||
Future<void> loadMore() async {
|
||||
if (loading || loadingMore || !hasMore) return;
|
||||
final request = ++_requestSeq;
|
||||
loadingMore = true;
|
||||
loadMoreError = null;
|
||||
notify();
|
||||
try {
|
||||
final page = await _repository.page(
|
||||
pageIndex: _pageIndex + 1,
|
||||
pageSize: pageSize,
|
||||
);
|
||||
if (_disposed || request != _requestSeq) return;
|
||||
final known = cards.map((drama) => drama.id).toSet();
|
||||
final appended = _uniqueCards(
|
||||
page.list,
|
||||
).where((drama) => known.add(drama.id)).toList(growable: false);
|
||||
cards = [...cards, ...appended];
|
||||
_saved.addAll(appended.map((drama) => drama.id));
|
||||
total = page.total;
|
||||
_pageIndex += 1;
|
||||
hasMore = page.list.isNotEmpty && cards.length < total;
|
||||
} catch (_) {
|
||||
if (!_disposed && request == _requestSeq) {
|
||||
loadMoreError = 'favoritesError';
|
||||
}
|
||||
} finally {
|
||||
if (!_disposed && request == _requestSeq) {
|
||||
loadingMore = false;
|
||||
notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,10 +107,17 @@ class FavoriteController extends ChangeNotifier {
|
||||
/// 这样"收藏完打开我的收藏"能立刻看到,不必等下一次整表刷新。
|
||||
Future<bool> toggle(String dramaId, {Drama? card}) async {
|
||||
final wasSaved = _saved.contains(dramaId);
|
||||
final previousCards = cards;
|
||||
final previousTotal = total;
|
||||
final previousHasMore = hasMore;
|
||||
// 乐观更新
|
||||
if (wasSaved) {
|
||||
_saved.remove(dramaId);
|
||||
cards = cards.where((drama) => drama.id != dramaId).toList();
|
||||
if (cards.any((drama) => drama.id == dramaId)) {
|
||||
cards = cards.where((drama) => drama.id != dramaId).toList();
|
||||
total = total > 0 ? total - 1 : 0;
|
||||
hasMore = cards.length < total;
|
||||
}
|
||||
} else {
|
||||
_saved.add(dramaId);
|
||||
}
|
||||
@@ -65,17 +127,30 @@ class FavoriteController extends ChangeNotifier {
|
||||
// 以服务端返回的状态为准(可能与本地猜测不同,例如并发操作)
|
||||
if (saved) {
|
||||
_saved.add(dramaId);
|
||||
if (card != null && !cards.any((drama) => drama.id == dramaId)) {
|
||||
if (wasSaved) {
|
||||
cards = previousCards;
|
||||
total = previousTotal;
|
||||
hasMore = previousHasMore;
|
||||
} else if (card != null && !cards.any((drama) => drama.id == dramaId)) {
|
||||
cards = [card, ...cards];
|
||||
total += 1;
|
||||
hasMore = cards.length < total;
|
||||
}
|
||||
} else {
|
||||
_saved.remove(dramaId);
|
||||
cards = cards.where((drama) => drama.id != dramaId).toList();
|
||||
if (cards.any((drama) => drama.id == dramaId)) {
|
||||
cards = cards.where((drama) => drama.id != dramaId).toList();
|
||||
total = total > 0 ? total - 1 : 0;
|
||||
hasMore = cards.length < total;
|
||||
}
|
||||
}
|
||||
notify();
|
||||
return true;
|
||||
} catch (_) {
|
||||
// 回滚
|
||||
cards = previousCards;
|
||||
total = previousTotal;
|
||||
hasMore = previousHasMore;
|
||||
if (wasSaved) {
|
||||
_saved.add(dramaId);
|
||||
} else {
|
||||
@@ -94,6 +169,8 @@ class FavoriteController extends ChangeNotifier {
|
||||
_saved.remove(dramaId);
|
||||
final next = [...cards]..removeAt(index);
|
||||
cards = next;
|
||||
total = total > 0 ? total - 1 : 0;
|
||||
hasMore = cards.length < total;
|
||||
notify();
|
||||
try {
|
||||
await _repository.remove(dramaId);
|
||||
@@ -106,6 +183,8 @@ class FavoriteController extends ChangeNotifier {
|
||||
);
|
||||
cards = restored;
|
||||
_saved.add(dramaId);
|
||||
total += 1;
|
||||
hasMore = cards.length < total;
|
||||
notify();
|
||||
return false;
|
||||
}
|
||||
@@ -113,12 +192,27 @@ class FavoriteController extends ChangeNotifier {
|
||||
|
||||
/// 退出登录时清空:收藏状态属于账号,不能跨账号串联。
|
||||
void clear() {
|
||||
_requestSeq += 1;
|
||||
_saved.clear();
|
||||
cards = const [];
|
||||
total = 0;
|
||||
loading = false;
|
||||
hasMore = false;
|
||||
loadingMore = false;
|
||||
loadMoreError = null;
|
||||
error = null;
|
||||
_pageIndex = 0;
|
||||
notify();
|
||||
}
|
||||
|
||||
List<Drama> _uniqueCards(List<Drama> source) {
|
||||
final seen = <String>{};
|
||||
return [
|
||||
for (final drama in source)
|
||||
if (seen.add(drama.id)) drama,
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_disposed = true;
|
||||
|
||||
@@ -11,41 +11,53 @@ import '../repositories/watch_history_repository.dart';
|
||||
|
||||
/// 独立观看记录页状态。
|
||||
///
|
||||
/// 最近记录只下发 dramaId;详情按 dramaId 去重后补取,并以 4 个并发为上限。
|
||||
/// 分页接口提供标题/封面/集号;详情按 dramaId 去重后补取,供点击播放使用。
|
||||
class WatchRecordsController extends ChangeNotifier {
|
||||
factory WatchRecordsController({
|
||||
required WatchHistoryRepository history,
|
||||
required CatalogRepository catalog,
|
||||
int limit = defaultLimit,
|
||||
}) => WatchRecordsController._(history, catalog, limit);
|
||||
int pageSize = defaultPageSize,
|
||||
}) => WatchRecordsController._(history, catalog, pageSize);
|
||||
|
||||
WatchRecordsController._(this._history, this._catalog, this.limit);
|
||||
WatchRecordsController._(this._history, this._catalog, this.pageSize);
|
||||
|
||||
static const int defaultLimit = 20;
|
||||
static const int defaultPageSize = 20;
|
||||
static const int detailConcurrency = 4;
|
||||
|
||||
final WatchHistoryRepository _history;
|
||||
final CatalogRepository _catalog;
|
||||
final int limit;
|
||||
final int pageSize;
|
||||
final Map<String, Drama> _detailCache = {};
|
||||
|
||||
List<WatchRecord> records = const [];
|
||||
int total = 0;
|
||||
bool loading = false;
|
||||
bool loadingMore = false;
|
||||
bool hasMore = false;
|
||||
bool clearing = false;
|
||||
String? error;
|
||||
String? loadMoreError;
|
||||
String? actionError;
|
||||
int _pageIndex = 0;
|
||||
int _requestSeq = 0;
|
||||
bool _disposed = false;
|
||||
|
||||
/// 加载最近记录并补齐详情;单条详情失败不拖垮整页。
|
||||
Future<void> load() async {
|
||||
final request = ++_requestSeq;
|
||||
loading = true;
|
||||
loadingMore = false;
|
||||
error = null;
|
||||
loadMoreError = null;
|
||||
actionError = null;
|
||||
_notify();
|
||||
try {
|
||||
final recent = await _history.recent(limit: limit);
|
||||
if (_disposed) return;
|
||||
records = _uniqueRecords(recent);
|
||||
final page = await _history.recent(pageIndex: 1, pageSize: pageSize);
|
||||
if (_disposed || request != _requestSeq) return;
|
||||
records = _uniqueRecords(page.list);
|
||||
total = page.total;
|
||||
_pageIndex = 1;
|
||||
hasMore = page.list.isNotEmpty && records.length < total;
|
||||
_notify();
|
||||
await _hydrate(records.map((record) => record.dramaId).toList());
|
||||
} catch (failure, stackTrace) {
|
||||
@@ -57,17 +69,54 @@ class WatchRecordsController extends ChangeNotifier {
|
||||
stackTrace: stackTrace,
|
||||
label: '[WatchRecordsController] recent failed',
|
||||
);
|
||||
if (!_disposed) {
|
||||
if (!_disposed && request == _requestSeq) {
|
||||
error = failure is ApiException ? failure.message : 'progressError';
|
||||
}
|
||||
} finally {
|
||||
if (!_disposed) {
|
||||
if (!_disposed && request == _requestSeq) {
|
||||
loading = false;
|
||||
_notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 追加下一页;刷新、加载中或无更多时忽略。
|
||||
Future<void> loadMore() async {
|
||||
if (loading || loadingMore || !hasMore) return;
|
||||
final request = ++_requestSeq;
|
||||
loadingMore = true;
|
||||
loadMoreError = null;
|
||||
_notify();
|
||||
try {
|
||||
final page = await _history.recent(
|
||||
pageIndex: _pageIndex + 1,
|
||||
pageSize: pageSize,
|
||||
);
|
||||
if (_disposed || request != _requestSeq) return;
|
||||
final known = records.map((record) => record.dramaId).toSet();
|
||||
final appended = _uniqueRecords(
|
||||
page.list,
|
||||
).where((record) => known.add(record.dramaId)).toList(growable: false);
|
||||
records = [...records, ...appended];
|
||||
total = page.total;
|
||||
_pageIndex += 1;
|
||||
hasMore = page.list.isNotEmpty && records.length < total;
|
||||
_notify();
|
||||
await _hydrate(appended.map((record) => record.dramaId).toList());
|
||||
} catch (failure) {
|
||||
if (!_disposed && request == _requestSeq) {
|
||||
loadMoreError = failure is ApiException
|
||||
? failure.message
|
||||
: 'progressError';
|
||||
}
|
||||
} finally {
|
||||
if (!_disposed && request == _requestSeq) {
|
||||
loadingMore = false;
|
||||
_notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 只重试一条详情失败记录。
|
||||
Future<void> retryDetail(String dramaId) async {
|
||||
final index = _recordIndex(dramaId);
|
||||
@@ -87,6 +136,8 @@ class WatchRecordsController extends ChangeNotifier {
|
||||
if (index < 0) return true;
|
||||
final removed = records[index];
|
||||
records = [...records]..removeAt(index);
|
||||
total = total > 0 ? total - 1 : 0;
|
||||
hasMore = records.length < total;
|
||||
actionError = null;
|
||||
_notify();
|
||||
try {
|
||||
@@ -96,6 +147,8 @@ class WatchRecordsController extends ChangeNotifier {
|
||||
final next = [...records];
|
||||
next.insert(index > next.length ? next.length : index, removed);
|
||||
records = next;
|
||||
total += 1;
|
||||
hasMore = records.length < total;
|
||||
actionError = 'recordDeleteFailed';
|
||||
return false;
|
||||
} finally {
|
||||
@@ -106,8 +159,15 @@ class WatchRecordsController extends ChangeNotifier {
|
||||
/// 清空全部记录,先更新 UI,失败恢复原列表。
|
||||
Future<bool> clear() async {
|
||||
if (records.isEmpty) return true;
|
||||
_requestSeq += 1;
|
||||
final previous = records;
|
||||
final previousTotal = total;
|
||||
final previousPageIndex = _pageIndex;
|
||||
records = const [];
|
||||
total = 0;
|
||||
hasMore = false;
|
||||
_pageIndex = 0;
|
||||
loadingMore = false;
|
||||
clearing = true;
|
||||
actionError = null;
|
||||
_notify();
|
||||
@@ -116,6 +176,9 @@ class WatchRecordsController extends ChangeNotifier {
|
||||
return true;
|
||||
} catch (_) {
|
||||
records = previous;
|
||||
total = previousTotal;
|
||||
_pageIndex = previousPageIndex;
|
||||
hasMore = records.length < total;
|
||||
actionError = 'recordClearFailed';
|
||||
return false;
|
||||
} finally {
|
||||
|
||||
Reference in new issue
Block a user