import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/contracts/library.dart'; import '../../providers/di_providers.dart'; import '../../providers/session_provider.dart'; import '../../shared/mappers/media_image_url.dart'; import '../../shared/utils/media_nav.dart'; import '../../shared/utils/time_bucket.dart'; import '../../shared/widgets/app_sliver_header.dart'; import '../../shared/widgets/app_loading_ring.dart'; import '../../shared/widgets/empty_state.dart'; import 'aggregated_grouping.dart'; import 'widgets/aggregated_grouped_grid.dart'; import 'widgets/aggregated_media_card.dart'; import 'widgets/server_filter_bar.dart'; class AggregatedRecentPage extends ConsumerStatefulWidget { final bool embedded; const AggregatedRecentPage({super.key, this.embedded = false}); @override ConsumerState createState() => _AggregatedRecentPageState(); } class _AggregatedRecentPageState extends ConsumerState { StreamSubscription? _subscription; List _serverResults = []; bool _loading = false; bool _switching = false; final Set _collapsed = {}; String? _serverFilter; @override void initState() { super.initState(); _load(); } @override void dispose() { _subscription?.cancel(); super.dispose(); } Future _load() async { _subscription?.cancel(); _subscription = null; setState(() { _loading = true; _serverResults = []; }); try { final uc = await ref.read(aggregatedRecentUseCaseProvider.future); final stream = uc.executeStream(); _subscription = stream.listen( (result) { if (mounted) { setState(() => _serverResults = [..._serverResults, result]); } }, onDone: () { if (mounted) setState(() => _loading = false); }, onError: (_) { if (mounted) setState(() => _loading = false); }, ); } catch (_) { if (mounted) setState(() => _loading = false); } } Future _onItemTap( BuildContext context, GlobalSearchServerResult serverResult, EmbyRawItem item, ) async { if (_switching) return; setState(() => _switching = true); try { await goMediaDetailOnServer( context, ref, serverId: serverResult.serverId, item: item, ); } finally { if (mounted) setState(() => _switching = false); } } void _toggle(String key) { setState(() { if (!_collapsed.remove(key)) _collapsed.add(key); }); } @override Widget build(BuildContext context) { ref.listen(sessionProvider, (previous, next) { final prevIds = previous?.value?.sessions.map((s) => s.serverId).toSet() ?? {}; final nextIds = next.value?.sessions.map((s) => s.serverId).toSet() ?? {}; if (prevIds.length == nextIds.length && prevIds.containsAll(nextIds)) { return; } if (_serverFilter != null && !nextIds.contains(_serverFilter)) { setState(() => _serverFilter = null); } _load(); }); if (widget.embedded) { return Stack( fit: StackFit.expand, children: [ CustomScrollView( slivers: [ SliverOverlapInjector( handle: NestedScrollView.sliverOverlapAbsorberHandleFor( context, ), ), ..._buildContentSlivers(context), ], ), if (_switching) _switchingOverlay(), ], ); } return Stack( fit: StackFit.expand, children: [ Scaffold( extendBodyBehindAppBar: true, body: CustomScrollView( slivers: [ AppSliverHeader( title: '最近添加', actions: [ IconButton( onPressed: _loading ? null : _load, icon: _loading ? const AppLoadingRing(size: 16) : const Icon(Icons.refresh, size: 18), tooltip: '刷新', ), ], ), ..._buildContentSlivers(context), ], ), ), if (_switching) _switchingOverlay(), ], ); } Widget _switchingOverlay() => const Positioned.fill( child: ColoredBox( color: Color(0x44000000), child: Center(child: AppLoadingRing()), ), ); List _buildContentSlivers(BuildContext context) { if (_serverResults.isEmpty && !_loading) { return const [ SliverFillRemaining( hasScrollBody: false, child: EmptyState( icon: Icons.new_releases_outlined, title: '暂无最近添加内容', ), ), ]; } if (_serverResults.isEmpty && _loading) { return const [ SliverFillRemaining( hasScrollBody: false, child: Center(child: AppLoadingRing()), ), ]; } final now = DateTime.now(); final entries = flatten( _serverResults, serverFilter: _serverFilter, deduplicateAcrossServers: true, ); final groups = groupEntries(entries, GroupAxis.addedTime, now); final servers = _serverResults .map((sr) => (id: sr.serverId, name: sr.serverName)) .toList(growable: false); return [ SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.only(top: 8), child: ServerFilterBar( servers: servers, selected: _serverFilter, onSelect: (v) => setState(() => _serverFilter = v), ), ), ), ...buildAggregatedGridSlivers( groups: groups, collapsed: _collapsed, onToggle: _toggle, cardBuilder: (entry) => _card(context, entry, now), ), ]; } Widget _card(BuildContext context, AggregatedEntry entry, DateTime now) { final sr = entry.source; final item = entry.item; final subtitle = item.ProductionYear != null ? '${item.ProductionYear}' : null; return AggregatedMediaCard( item: item, imageUrl: EmbyImageUrl.landscapeCard( baseUrl: sr.serverUrl, token: sr.token, item: item, maxWidth: 400, ), imageHeaders: buildEmbyImageHeaders( ref, token: sr.token, userId: sr.userId, ), title: item.Name, subtitle: subtitle, serverName: sr.serverName, timeLabel: relativeTimeLabel(entry.dateFor(GroupAxis.addedTime), now), showProgress: false, onTap: () => _onItemTap(context, sr, item), ); } }