/// 照片管理占位页(P3 计划 Task 4):基础管理列表(可见性/处理状态 /// 标签 + 游标加载更多);完整筛选与操作由 Task 9 队列 UI 替换。 library; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../core/api_client.dart'; import '../core/app_exception.dart'; import '../core/error_codes.dart'; import '../core/error_l10n.dart'; import '../features/albums/album_format.dart'; import '../features/albums/models.dart'; import '../features/photos/models.dart'; import '../l10n/app_localizations.dart'; /// 照片管理占位(从详情页携带 [album] 进入,时间按相册时区展示)。 class AlbumPhotosPage extends ConsumerStatefulWidget { const AlbumPhotosPage({required this.album, super.key}); final Album album; @override ConsumerState createState() => _AlbumPhotosPageState(); } class _AlbumPhotosPageState extends ConsumerState { static const int _pageSize = 20; List _items = const []; String? _nextCursor; bool _hasMore = false; bool _loading = true; bool _loadingMore = false; String? _errorCode; @override void initState() { super.initState(); _reload(); } Future _reload() async { setState(() { _loading = true; _errorCode = null; }); try { final page = await ref.read(apiClientProvider).listAlbumPhotos( widget.album.id, limit: _pageSize, ); if (mounted) { setState(() { _items = page.items; _nextCursor = page.nextCursor; _hasMore = page.hasMore; _loading = false; }); } } on AppException catch (e) { if (mounted) { setState(() { _errorCode = e.code; _loading = false; }); } } on Object { if (mounted) { setState(() { _errorCode = ErrorCodes.internalError; _loading = false; }); } } } Future _loadMore() async { if (_loading || _loadingMore || !_hasMore) return; setState(() => _loadingMore = true); try { final page = await ref.read(apiClientProvider).listAlbumPhotos( widget.album.id, cursor: _nextCursor, limit: _pageSize, ); if (mounted) { setState(() { _items = [..._items, ...page.items]; _nextCursor = page.nextCursor; _hasMore = page.hasMore; _loadingMore = false; }); } } on AppException { if (mounted) { setState(() => _loadingMore = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( AppLocalizations.of(context).messageForErrorCode( ErrorCodes.networkError, ), ), ), ); } } on Object { if (mounted) { setState(() => _loadingMore = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(AppLocalizations.of(context).errorGeneric), ), ); } } } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final theme = Theme.of(context); return Scaffold( appBar: AppBar(title: Text(l10n.albumPhotos)), body: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Task 9 占位横幅:完整筛选/操作届时替换本页。 Card( margin: const EdgeInsets.fromLTRB(12, 12, 12, 0), child: Padding( padding: const EdgeInsets.all(12), child: Row( children: [ Icon( Icons.construction_outlined, size: 18, color: theme.colorScheme.onSurfaceVariant, ), const SizedBox(width: 8), Expanded( child: Text( l10n.photosTask9Placeholder, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ), ], ), ), ), Expanded( child: _loading ? const Center(child: CircularProgressIndicator()) : _items.isEmpty && _errorCode != null ? Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.cloud_off, size: 48), const SizedBox(height: 12), Text(l10n.messageForErrorCode(_errorCode!)), const SizedBox(height: 12), FilledButton.icon( onPressed: _reload, icon: const Icon(Icons.refresh), label: Text(l10n.retry), ), ], ), ) : ListView.builder( itemCount: _items.length + (_items.isEmpty || _hasMore || _loadingMore ? 1 : 0), itemBuilder: (context, index) { if (_items.isEmpty) { return Padding( padding: const EdgeInsets.all(32), child: Text( l10n.albumPhotosEmpty, textAlign: TextAlign.center, style: theme.textTheme.bodySmall, ), ); } if (index >= _items.length) { return const Padding( padding: EdgeInsets.symmetric(vertical: 16), child: Center( child: SizedBox( height: 24, width: 24, child: CircularProgressIndicator(strokeWidth: 2), ), ), ); } if (index >= _items.length - 3 && _hasMore && !_loadingMore) { WidgetsBinding.instance.addPostFrameCallback( (_) => _loadMore(), ); } return _photoTile(l10n, theme, _items[index]); }, ), ), ], ), ); } Widget _photoTile( AppLocalizations l10n, ThemeData theme, AdminPhoto photo, ) { return ListTile( leading: Container( width: 44, height: 44, decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(8), ), child: const Icon(Icons.image_outlined), ), title: Text( '${photo.photoId.substring(0, 8)} · ${(photo.bytes / 1024).round()} KB', style: theme.textTheme.bodyMedium?.copyWith( fontFeatures: const [FontFeature.tabularFigures()], ), ), subtitle: Text( formatAlbumDateTime( photo.uploadedAt, widget.album.timezone, l10n.localeName, ), ), trailing: Wrap( spacing: 6, runSpacing: 4, crossAxisAlignment: WrapCrossAlignment.center, children: [ _PhotoTag( label: _visibilityLabel(l10n, photo.visibility), highlight: photo.visibility == PhotoVisibility.published, ), _PhotoTag( label: _processingLabel(l10n, photo.processingState), highlight: photo.processingState == PhotoProcessingState.ready, ), ], ), ); } static String _visibilityLabel(AppLocalizations l10n, PhotoVisibility v) { switch (v) { case PhotoVisibility.published: return l10n.visibilityPublished; case PhotoVisibility.unpublished: return l10n.visibilityUnpublished; case PhotoVisibility.hidden: return l10n.visibilityHidden; case PhotoVisibility.deleted: return l10n.visibilityDeleted; } } static String _processingLabel( AppLocalizations l10n, PhotoProcessingState s, ) { switch (s) { case PhotoProcessingState.pendingUpload: return l10n.procPendingUpload; case PhotoProcessingState.uploaded: return l10n.procUploaded; case PhotoProcessingState.processing: return l10n.procProcessing; case PhotoProcessingState.ready: return l10n.procReady; case PhotoProcessingState.processingFailed: return l10n.procProcessingFailed; } } } /// 照片标签(可见性/处理状态)。 class _PhotoTag extends StatelessWidget { const _PhotoTag({required this.label, required this.highlight}); final String label; final bool highlight; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: highlight ? theme.colorScheme.secondaryContainer : theme.colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(999), ), child: Text( label, style: theme.textTheme.labelSmall?.copyWith( color: highlight ? theme.colorScheme.onSecondaryContainer : theme.colorScheme.onSurfaceVariant, ), ), ); } }