feat(theater): 剧场分类列表服务端分页

This commit is contained in:
Zhou Weidong committed 2026-10-21 18:24:14 +08:00
1 parent f8769f2e1d
commit 13a14f3e85
16 files changed
+769 -21

No files matched your search

+77 -11
View File
@@ -8,9 +8,11 @@ import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../../core/data/models/drama.dart';
import '../../core/data/repositories/catalog_repository.dart';
import '../../core/data/repositories/search_repository.dart';
import '../../core/data/services/catalog_retry.dart';
import '../../core/data/services/category_controller.dart';
import '../../core/data/services/theater_feed_controller.dart';
import '../../core/formatting/error_text.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_dimens.dart';
@@ -70,6 +72,7 @@ class DiscoverPage extends StatefulWidget {
class _DiscoverPageState extends State<DiscoverPage> {
late final DiscoverLogic logic;
late final TheaterFeedController theater;
final ValueNotifier<bool> _headerPinned = ValueNotifier<bool>(false);
final ScrollController _categoryScroll = ScrollController();
final Map<String, GlobalKey> _categoryAnchors = {};
@@ -82,13 +85,25 @@ class _DiscoverPageState extends State<DiscoverPage> {
void initState() {
super.initState();
logic = DiscoverBinding().create(search: context.read<SearchRepository>());
theater = TheaterFeedController(context.read<CatalogRepository>());
logic.scroll.addListener(_onScroll);
theater.addListener(_onTheaterChanged);
unawaited(theater.select(null));
if (widget.focusSearch) _focusOnFirstFrame();
}
void _onScroll() {
final pinned = logic.scroll.hasClients && logic.scroll.offset > 0;
if (pinned != _headerPinned.value) _headerPinned.value = pinned;
if (logic.scroll.hasClients &&
!logic.hasSearchQuery &&
logic.scroll.position.extentAfter < 400) {
unawaited(theater.loadMore());
}
}
void _onTheaterChanged() {
if (mounted) setState(() {});
}
void _focusOnFirstFrame() {
@@ -111,6 +126,8 @@ class _DiscoverPageState extends State<DiscoverPage> {
@override
void dispose() {
logic.scroll.removeListener(_onScroll);
theater.removeListener(_onTheaterChanged);
theater.dispose();
_categoryScroll.dispose();
_headerPinned.dispose();
logic.dispose();
@@ -169,7 +186,8 @@ class _DiscoverPageState extends State<DiscoverPage> {
child: AppPullToRefresh(
key: const Key('discover-refresh'),
edgeOffset: _stickyHeaderHeight + 8,
onRefresh: categories.load,
onRefresh: () =>
Future.wait([categories.load(), theater.refreshCurrent()]),
child: ListView(
controller: logic.scroll,
physics: AppPullToRefresh.physics,
@@ -195,7 +213,7 @@ class _DiscoverPageState extends State<DiscoverPage> {
bool _showStickyHeader(CategoryController categories) {
if (logic.hasSearchQuery || categories.error != null) return false;
return !(categories.loading && categories.feed.sections.isEmpty);
return !(categories.loading && categories.categories.isEmpty);
}
Widget _stickyHeader(AppLocalizations l10n, CategoryController categories) {
@@ -244,7 +262,7 @@ class _DiscoverPageState extends State<DiscoverPage> {
),
];
}
if (categories.loading && categories.feed.sections.isEmpty) {
if (categories.loading && categories.categories.isEmpty) {
return [_block(const CircularProgressIndicator(strokeWidth: 2))];
}
final error = categories.error;
@@ -269,16 +287,35 @@ class _DiscoverPageState extends State<DiscoverPage> {
),
];
}
return _theaterBlocks(l10n, categories);
if (theater.loadingFirstPage) {
return [_block(const CircularProgressIndicator(strokeWidth: 2))];
}
final theaterError = theater.lastError;
if (theaterError != null && theater.dramas.isEmpty) {
return [
_block(
EmptyState(
key: const Key('home-categories-error'),
icon: Icons.cloud_off_rounded,
title: errorText(l10n, theaterError, l10n.catalogError),
subtitle: l10n.emptyHint,
actionLabel: l10n.retry,
onAction: () => unawaited(_retryTheater()),
footer: kDebugMode
? CatalogDebugInfo(
error: theaterError,
onEditEndpoint: () => unawaited(_editTheaterEndpoint()),
)
: null,
),
),
];
}
return _theaterBlocks(l10n);
}
List<Widget> _theaterBlocks(
AppLocalizations l10n,
CategoryController categories,
) {
final dramas = logic.selectedCategoryId == null
? categories.feed.dramas
: categories.dramasOf(logic.selectedCategoryId!);
List<Widget> _theaterBlocks(AppLocalizations l10n) {
final dramas = theater.dramas;
return [
_toolbar(l10n),
if (dramas.isEmpty)
@@ -293,6 +330,17 @@ class _DiscoverPageState extends State<DiscoverPage> {
)
else
DramaGrid(l10n: l10n, dramas: dramas, onOpen: _openDrama),
if (theater.loadingMore)
const Padding(
padding: EdgeInsets.symmetric(vertical: 20),
child: Center(
child: SizedBox.square(
key: Key('theater-load-more'),
dimension: 22,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
),
];
}
@@ -386,6 +434,7 @@ class _DiscoverPageState extends State<DiscoverPage> {
void _selectCategory(String? categoryId) {
logic.selectCategory(categoryId);
unawaited(theater.select(categoryId));
final id = categoryId ?? 'all';
_ensuredCategoryId = id;
WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -523,6 +572,23 @@ class _DiscoverPageState extends State<DiscoverPage> {
return retryCatalogLoad(reload: categories.load);
}
Future<void> _retryTheater() async {
await retryCatalogLoad(
reload: () async {
await theater.refreshCurrent();
},
);
}
Future<void> _editTheaterEndpoint() {
return showCatalogEndpointEditor(
context,
reload: () async {
await theater.refreshCurrent();
},
);
}
Future<void> _editCategoriesEndpoint(CategoryController categories) {
return showCatalogEndpointEditor(context, reload: categories.load);
}