feat: Enhance search functionality with initial query parameters and infinite scrolling
This commit is contained in:
@@ -23,11 +23,40 @@ class BrowseParams {
|
||||
this.page = 1,
|
||||
});
|
||||
|
||||
String? _normalizedStatus() {
|
||||
final raw = status?.trim();
|
||||
if (raw == null || raw.isEmpty) return null;
|
||||
switch (raw.toLowerCase()) {
|
||||
case 'ongoing':
|
||||
return 'Đang ra';
|
||||
case 'completed':
|
||||
return 'Hoàn thành';
|
||||
case 'hiatus':
|
||||
return 'Tạm ngưng';
|
||||
default:
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
String _normalizedSort() {
|
||||
final raw = sort.trim();
|
||||
if (raw.isEmpty) return 'latest';
|
||||
switch (raw.toLowerCase()) {
|
||||
case 'latest':
|
||||
case 'popular':
|
||||
case 'rating':
|
||||
case 'name':
|
||||
return raw.toLowerCase();
|
||||
default:
|
||||
return 'latest';
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toQueryParams() => {
|
||||
if (query != null && query!.isNotEmpty) 'q': query,
|
||||
if (genre != null) 'genre': genre,
|
||||
if (status != null) 'status': status,
|
||||
'sort': sort,
|
||||
if (_normalizedStatus() != null) 'status': _normalizedStatus(),
|
||||
'sort': _normalizedSort(),
|
||||
'page': page.toString(),
|
||||
'limit': '20',
|
||||
};
|
||||
@@ -56,18 +85,39 @@ class BrowseResult {
|
||||
final int totalCount;
|
||||
final int totalPages;
|
||||
final int currentPage;
|
||||
final bool isLoadingMore;
|
||||
|
||||
const BrowseResult({
|
||||
required this.items,
|
||||
required this.totalCount,
|
||||
required this.totalPages,
|
||||
required this.currentPage,
|
||||
this.isLoadingMore = false,
|
||||
});
|
||||
|
||||
bool get hasMore => currentPage < totalPages;
|
||||
|
||||
BrowseResult copyWith({
|
||||
List<NovelModel>? items,
|
||||
int? totalCount,
|
||||
int? totalPages,
|
||||
int? currentPage,
|
||||
bool? isLoadingMore,
|
||||
}) {
|
||||
return BrowseResult(
|
||||
items: items ?? this.items,
|
||||
totalCount: totalCount ?? this.totalCount,
|
||||
totalPages: totalPages ?? this.totalPages,
|
||||
currentPage: currentPage ?? this.currentPage,
|
||||
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NovelsNotifier extends StateNotifier<AsyncValue<BrowseResult>> {
|
||||
final Ref _ref;
|
||||
BrowseParams _params = const BrowseParams();
|
||||
bool _isLoadingMore = false;
|
||||
|
||||
NovelsNotifier(this._ref) : super(const AsyncValue.loading()) {
|
||||
fetch();
|
||||
@@ -75,25 +125,53 @@ class NovelsNotifier extends StateNotifier<AsyncValue<BrowseResult>> {
|
||||
|
||||
BrowseParams get params => _params;
|
||||
|
||||
Future<BrowseResult> _fetchPage(BrowseParams params) async {
|
||||
final client = _ref.read(apiClientProvider);
|
||||
final res = await client.dio.get('/api/novels/browse', queryParameters: params.toQueryParams());
|
||||
final data = res.data as Map<String, dynamic>;
|
||||
return BrowseResult(
|
||||
items: (data['items'] as List).map((e) => NovelModel.fromJson(e as Map<String, dynamic>)).toList(),
|
||||
totalCount: (data['totalCount'] as num?)?.toInt() ?? 0,
|
||||
totalPages: (data['totalPages'] as num?)?.toInt() ?? 1,
|
||||
currentPage: (data['currentPage'] as num?)?.toInt() ?? params.page,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> fetch({BrowseParams? params}) async {
|
||||
if (params != null) _params = params;
|
||||
state = const AsyncValue.loading();
|
||||
try {
|
||||
final client = _ref.read(apiClientProvider);
|
||||
final res = await client.dio.get('/api/novels/browse', queryParameters: _params.toQueryParams());
|
||||
final data = res.data as Map<String, dynamic>;
|
||||
state = AsyncValue.data(BrowseResult(
|
||||
items: (data['items'] as List).map((e) => NovelModel.fromJson(e as Map<String, dynamic>)).toList(),
|
||||
totalCount: data['totalCount'] as int,
|
||||
totalPages: data['totalPages'] as int,
|
||||
currentPage: data['currentPage'] as int,
|
||||
));
|
||||
final firstPageParams = _params.copyWith(page: 1);
|
||||
final result = await _fetchPage(firstPageParams);
|
||||
_params = firstPageParams;
|
||||
state = AsyncValue.data(result);
|
||||
} catch (e, st) {
|
||||
state = AsyncValue.error(e, st);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateParams(BrowseParams params) => fetch(params: params);
|
||||
|
||||
Future<void> loadNextPage() async {
|
||||
final current = state.valueOrNull;
|
||||
if (current == null || !current.hasMore || _isLoadingMore) return;
|
||||
|
||||
_isLoadingMore = true;
|
||||
state = AsyncValue.data(current.copyWith(isLoadingMore: true));
|
||||
|
||||
try {
|
||||
final nextParams = _params.copyWith(page: current.currentPage + 1);
|
||||
final nextPage = await _fetchPage(nextParams);
|
||||
_params = nextParams;
|
||||
|
||||
final merged = [...current.items, ...nextPage.items];
|
||||
state = AsyncValue.data(nextPage.copyWith(items: merged, isLoadingMore: false));
|
||||
} catch (e, st) {
|
||||
state = AsyncValue.error(e, st);
|
||||
} finally {
|
||||
_isLoadingMore = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final novelsProvider = StateNotifierProvider<NovelsNotifier, AsyncValue<BrowseResult>>((ref) {
|
||||
|
||||
Reference in New Issue
Block a user