import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../core/contracts/library.dart'; import '../../providers/di_providers.dart'; import '../../providers/emby_headers_provider.dart'; import '../../providers/library_overview_provider.dart'; import '../../providers/session_provider.dart'; import '../../providers/shell_backdrop_provider.dart'; import '../../shared/constants/layout_constants.dart'; import '../../shared/mappers/media_image_url.dart'; import '../../shared/utils/user_error_formatter.dart'; import '../../shared/widgets/app_loading_ring.dart'; import '../../shared/widgets/app_error_state.dart'; import '../../shared/utils/media_nav.dart'; import '../../shared/widgets/app_sliver_header.dart'; import '../../shared/widgets/empty_state.dart'; import '../../shared/widgets/media_poster_card.dart'; const _kPageSize = 20; const _kScrollThreshold = 200.0; const _kFields = 'BasicSyncInfo,CommunityRating,ProductionYear,ChildCount,Container,' 'Overview,UserDataLastPlayedDate,OfficialRating,Genres,Status,' 'People,Studios,ProviderIds'; const _kImageTypes = 'Primary,Backdrop,Thumb,Logo'; const _kIncludeTypes = 'Series,Movie,Video,MusicVideo'; const _kCollectionIncludeTypes = 'BoxSet,Folder,Series,Movie,Video,MusicVideo'; const _kSortBy = 'DateLastContentAdded,DateCreated,SortName'; class LibraryListPage extends ConsumerStatefulWidget { final String libraryId; final String? title; final bool isCollection; const LibraryListPage({ super.key, required this.libraryId, this.title, this.isCollection = false, }); @override ConsumerState createState() => _LibraryListPageState(); } class _LibraryListPageState extends ConsumerState { final List _items = []; int _startIndex = 0; bool _hasMore = true; bool _isFirstLoad = true; bool _isLoadingMore = false; String? _loadError; final _scrollController = ScrollController(); @override void initState() { super.initState(); _fetchPage(); _scrollController.addListener(_onScroll); } @override void didUpdateWidget(LibraryListPage oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.libraryId != widget.libraryId) { _resetAndFetch(); } } @override void dispose() { _scrollController.dispose(); super.dispose(); } void _onScroll() { if (!_scrollController.hasClients) return; final remaining = _scrollController.position.maxScrollExtent - _scrollController.position.pixels; if (remaining <= _kScrollThreshold && _hasMore && !_isLoadingMore) { _fetchPage(); } } void _resetAndFetch() { setState(() { _items.clear(); _startIndex = 0; _hasMore = true; _isFirstLoad = true; _isLoadingMore = false; _loadError = null; }); _fetchPage(); } Future _refresh() async { setState(() { _items.clear(); _startIndex = 0; _hasMore = true; _isFirstLoad = true; _isLoadingMore = false; _loadError = null; }); await _fetchPage(); } Future _fetchPage() async { if (_isLoadingMore || !_hasMore) return; setState(() { _isLoadingMore = true; _loadError = null; }); try { final uc = await ref.read(libraryItemsUseCaseProvider.future); final res = await uc.execute( LibraryItemsReq( parentId: widget.libraryId, includeItemTypes: widget.isCollection ? _kCollectionIncludeTypes : _kIncludeTypes, sortBy: _kSortBy, sortOrder: SortOrder.descending, fields: _kFields, enableImageTypes: _kImageTypes, startIndex: _startIndex, limit: _kPageSize, recursive: !widget.isCollection, ), ); if (!mounted) return; final fetched = res.items; setState(() { _items.addAll(fetched); _startIndex += fetched.length; _hasMore = fetched.length == _kPageSize; _isFirstLoad = false; _isLoadingMore = false; }); } catch (e) { if (!mounted) return; setState(() { _loadError = e.toString(); _isFirstLoad = false; _isLoadingMore = false; }); } } @override Widget build(BuildContext context) { final session = ref.watch(activeSessionProvider); if (session == null) return const SizedBox.shrink(); final imageHeaders = ref.watch(embyImageHeadersProvider).value; final hasBackdrop = ref.watch( shellBackdropProvider.select((state) => state != null), ); final immersiveTextColor = hasBackdrop ? Colors.white : null; final libraryName = ref .watch(libraryOverviewProvider) .value ?.libraries .where((l) => l.Id == widget.libraryId) .firstOrNull ?.Name ?? widget.title ?? ''; return Scaffold( backgroundColor: Colors.transparent, extendBodyBehindAppBar: true, body: RefreshIndicator( onRefresh: _refresh, child: CustomScrollView( controller: _scrollController, physics: const AlwaysScrollableScrollPhysics( parent: BouncingScrollPhysics(), ), slivers: [ AppSliverHeader( title: libraryName, leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () => context.pop(), ), ), ..._buildContentSlivers( session, imageHeaders, immersiveTextColor: immersiveTextColor, ), ], ), ), ); } List _buildContentSlivers( dynamic session, Map? imageHeaders, { required Color? immersiveTextColor, }) { if (_isFirstLoad) { if (_loadError != null) { return [ SliverFillRemaining( hasScrollBody: false, child: AppErrorState( message: formatUserError(_loadError, fallback: '媒体列表加载失败'), foregroundColor: immersiveTextColor, onRetry: _resetAndFetch, ), ), ]; } return const [ SliverFillRemaining( hasScrollBody: false, child: Center(child: AppLoadingRing()), ), ]; } if (_items.isEmpty && !_isLoadingMore && _loadError != null) { return [ SliverFillRemaining( hasScrollBody: false, child: AppErrorState( message: formatUserError(_loadError, fallback: '媒体列表加载失败'), foregroundColor: immersiveTextColor, onRetry: _resetAndFetch, ), ), ]; } if (_items.isEmpty && !_isLoadingMore) { return [ SliverFillRemaining( hasScrollBody: false, child: EmptyState( icon: Icons.inbox_outlined, title: '暂无媒体内容', foregroundColor: immersiveTextColor, ), ), ]; } return [ SliverPadding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), sliver: SliverGrid( delegate: SliverChildBuilderDelegate( (_, i) => MediaPosterCard( item: _items[i], width: double.infinity, textColor: immersiveTextColor, imageUrl: EmbyImageUrl.primary( baseUrl: session.serverUrl, token: session.token, item: _items[i], maxHeight: 360, ), preloadImageUrl: EmbyImageUrl.detailBanner( baseUrl: session.serverUrl, token: session.token, item: _items[i], ), imageHeaders: imageHeaders, onTap: () => goMediaDetail(context, _items[i]), ), childCount: _items.length, ), gridDelegate: mediaGridDelegate, ), ), SliverToBoxAdapter( child: _buildFooter(immersiveTextColor: immersiveTextColor), ), ]; } Widget _buildFooter({required Color? immersiveTextColor}) { if (_isLoadingMore) { return const Padding( padding: EdgeInsets.symmetric(vertical: 24), child: Center(child: AppLoadingRing()), ); } if (!_hasMore && _items.isNotEmpty) { return Padding( padding: const EdgeInsets.symmetric(vertical: 24), child: Center( child: Text( '没有更多了', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: immersiveTextColor?.withValues(alpha: 0.55), ), ), ), ); } return const SizedBox.shrink(); } }