/// 相册列表页签(P3 计划 Task 4):游标加载更多、下拉刷新、卡片标签行 /// (状态 + 四开关)、时区感知的时间展示、公开令牌短显、FAB 新建; /// 加载/空/错误态齐备。详情/编辑由卡片与列表级刷新联动。 library; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../core/error_l10n.dart'; import '../features/albums/album_format.dart'; import '../features/albums/models.dart'; import '../features/albums/providers.dart'; import '../l10n/app_localizations.dart'; import 'album_detail_page.dart'; import 'album_form_page.dart'; /// 首页「相册」页签。 class AlbumsTab extends ConsumerWidget { const AlbumsTab({super.key}); Future _openCreate(BuildContext context, WidgetRef ref) async { final changed = await Navigator.of(context).push( MaterialPageRoute(builder: (_) => const AlbumFormPage()), ); if (changed == true) { ref.read(albumListProvider.notifier).refresh(); } } @override Widget build(BuildContext context, WidgetRef ref) { final l10n = AppLocalizations.of(context); final state = ref.watch(albumListProvider); return Scaffold( appBar: AppBar(title: Text(l10n.albums)), floatingActionButton: FloatingActionButton.extended( onPressed: () => _openCreate(context, ref), icon: const Icon(Icons.add), label: Text(l10n.albumCreate), ), body: AlbumListBody(state: state), ); } } /// 列表主体:按状态分发 加载/错误/空/列表(含加载更多页脚)。 class AlbumListBody extends ConsumerWidget { const AlbumListBody({required this.state, super.key}); final AlbumListState state; @override Widget build(BuildContext context, WidgetRef ref) { final l10n = AppLocalizations.of(context); final notifier = ref.read(albumListProvider.notifier); if (state.loading) { return const Center(child: CircularProgressIndicator()); } if (state.items.isEmpty) { if (state.errorCode != null) { return _ErrorView( message: l10n.messageForErrorCode(state.errorCode!), onRetry: notifier.refresh, ); } return _EmptyView(message: l10n.albumsEmpty); } final hasFooter = state.hasMore || state.loadMoreFailed; return RefreshIndicator( onRefresh: notifier.refresh, child: ListView.builder( physics: const AlwaysScrollableScrollPhysics(), itemCount: state.items.length + (hasFooter ? 1 : 0), itemBuilder: (context, index) { if (index >= state.items.length) { if (state.loadMoreFailed) { return ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 16), leading: const Icon(Icons.error_outline), title: Text(l10n.loadMoreFailed), onTap: notifier.loadMore, ); } return const Padding( padding: EdgeInsets.symmetric(vertical: 16), child: Center( child: SizedBox( height: 24, width: 24, child: CircularProgressIndicator(strokeWidth: 2), ), ), ); } // 接近底部时预取下一页(loadMore 自带并发/末页守卫)。 if (index >= state.items.length - 3 && state.hasMore && !state.loadingMore) { WidgetsBinding.instance.addPostFrameCallback( (_) => notifier.loadMore(), ); } return AlbumCard(album: state.items[index]); }, ), ); } } /// 首屏错误视图(重试按钮触发整页刷新)。 class _ErrorView extends StatelessWidget { const _ErrorView({required this.message, required this.onRetry}); final String message; final Future Function() onRetry; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.cloud_off, size: 48), const SizedBox(height: 12), Text(message, textAlign: TextAlign.center), const SizedBox(height: 12), FilledButton.icon( onPressed: onRetry, icon: const Icon(Icons.refresh), label: Text(l10n.retry), ), ], ), ); } } /// 空列表视图。 class _EmptyView extends StatelessWidget { const _EmptyView({required this.message}); final String message; @override Widget build(BuildContext context) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.photo_library_outlined, size: 48), const SizedBox(height: 12), Text(message, textAlign: TextAlign.center), ], ), ); } } /// 相册卡片:默认语言标题、状态 + 四开关标签行、相册时区的时间区间、 /// 公开令牌短显。 class AlbumCard extends StatelessWidget { const AlbumCard({required this.album, super.key}); final AlbumSummary album; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final theme = Theme.of(context); final time = _timeRange(l10n); return Card( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), child: InkWell( borderRadius: BorderRadius.circular(12), onTap: () => Navigator.of(context).push( MaterialPageRoute( builder: (_) => AlbumDetailPage(albumId: album.id), ), ), child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( _title, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.titleMedium ?.copyWith(fontWeight: FontWeight.w600), ), ), const SizedBox(width: 8), _StatusChip(status: album.status), ], ), const SizedBox(height: 8), Wrap( spacing: 6, runSpacing: 4, children: [ _SwitchChip(label: l10n.tagUpload, on: album.uploadEnabled), _SwitchChip(label: l10n.tagPublic, on: album.publicEnabled), _SwitchChip( label: l10n.tagDownload, on: album.downloadEnabled, ), _SwitchChip( label: l10n.tagOriginal, on: album.originalDownloadEnabled, ), ], ), const SizedBox(height: 8), Row( children: [ Icon( Icons.schedule, size: 14, color: theme.colorScheme.onSurfaceVariant, ), const SizedBox(width: 4), Expanded( child: Text( time, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ), ], ), const SizedBox(height: 2), Text( album.publicToken.length > 10 ? '${album.publicToken.substring(0, 10)}…' : album.publicToken, style: theme.textTheme.bodySmall?.copyWith( fontFeatures: const [FontFeature.tabularFigures()], color: theme.colorScheme.onSurfaceVariant, ), ), ], ), ), ), ); } /// 默认语言标题(缺失时回退首个条目;无翻译时显示 ID 兜底)。 String get _title { for (final t in album.translations) { if (t.locale == album.defaultLocale) return t.title; } if (album.translations.isNotEmpty) return album.translations.first.title; return album.id; } /// 相册时区的时间区间(结束未定显示「未定」占位)。 String _timeRange(AppLocalizations l10n) { final locale = l10n.localeName; final start = formatAlbumDateTime(album.startAt, album.timezone, locale); final end = album.endAt == null ? l10n.notSet : formatAlbumDateTime(album.endAt!, album.timezone, locale); return '$start ~ $end'; } } /// 状态标签:active 正常 / offline 下线。 class _StatusChip extends StatelessWidget { const _StatusChip({required this.status}); final AlbumStatus status; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final theme = Theme.of(context); final active = status == AlbumStatus.active; return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: active ? theme.colorScheme.secondaryContainer : theme.colorScheme.errorContainer, borderRadius: BorderRadius.circular(999), ), child: Text( active ? l10n.albumStatusActive : l10n.albumStatusOffline, style: theme.textTheme.labelSmall?.copyWith( color: active ? theme.colorScheme.onSecondaryContainer : theme.colorScheme.onErrorContainer, ), ), ); } } /// 开关标签:色调区分开/关。 class _SwitchChip extends StatelessWidget { const _SwitchChip({required this.label, required this.on}); final String label; final bool on; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: on ? theme.colorScheme.primaryContainer : theme.colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(999), ), child: Text( label, style: theme.textTheme.labelSmall?.copyWith( color: on ? theme.colorScheme.onPrimaryContainer : theme.colorScheme.onSurfaceVariant, ), ), ); } }