import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; import '../../core/api/api_config.dart'; import '../../core/api/services/image_api.dart'; import '../../core/auth/auth_service.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_spacing.dart'; import '../../shared/widgets/top_nav_bar.dart'; import 'models/gallery_task_item.dart'; import 'video_thumbnail_cache.dart'; /// Gallery screen - matches Pencil hpwBg class GalleryScreen extends StatefulWidget { const GalleryScreen({super.key, required this.isActive}); final bool isActive; @override State createState() => _GalleryScreenState(); } class _GalleryScreenState extends State { List _tasks = []; bool _loading = true; bool _loadingMore = false; String? _error; int _currentPage = 1; bool _hasNext = false; bool _hasLoadedOnce = false; final ScrollController _scrollController = ScrollController(); static const int _pageSize = 20; @override void initState() { super.initState(); _scrollController.addListener(_onScroll); if (widget.isActive) _loadTasks(refresh: true); } @override void didUpdateWidget(covariant GalleryScreen oldWidget) { super.didUpdateWidget(oldWidget); if (widget.isActive && !oldWidget.isActive && !_hasLoadedOnce) { _loadTasks(refresh: true); } } @override void dispose() { _scrollController.dispose(); super.dispose(); } void _onScroll() { if (_loadingMore || !_hasNext || _tasks.isEmpty) return; final pos = _scrollController.position; if (pos.pixels >= pos.maxScrollExtent - 200) { _loadTasks(refresh: false); } } Future _loadTasks({bool refresh = true}) async { if (refresh) { setState(() { _loading = true; _error = null; _currentPage = 1; }); } else { if (_loadingMore || !_hasNext) return; setState(() => _loadingMore = true); } try { await AuthService.loginComplete; final page = refresh ? 1 : _currentPage; final res = await ImageApi.getMyTasks( sentinel: ApiConfig.appId, trophy: page.toString(), heatmap: _pageSize.toString(), ); if (!mounted) return; if (res.isSuccess && res.data != null) { final data = res.data as Map?; final intensify = data?['intensify'] as List? ?? []; final list = intensify .whereType>() .map((e) => GalleryTaskItem.fromJson(e)) .toList(); final hasNext = data?['manifest'] as bool? ?? false; setState(() { if (refresh) { _tasks = list; } else { _tasks = [..._tasks, ...list]; } _currentPage = page + 1; _hasNext = hasNext; _loading = false; _loadingMore = false; _hasLoadedOnce = true; }); } else { setState(() { if (refresh) _tasks = []; _loading = false; _loadingMore = false; _error = res.msg.isNotEmpty ? res.msg : 'Failed to load'; }); } } catch (e) { if (mounted) { setState(() { if (refresh) _tasks = []; _loading = false; _loadingMore = false; _error = e.toString(); }); } } } List get _gridItems { final items = []; for (final task in _tasks) { items.addAll(task.mediaItems); } return items; } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.background, appBar: const PreferredSize( preferredSize: Size.fromHeight(56), child: TopNavBar( title: 'My Gallery', ), ), body: _loading ? const Center(child: CircularProgressIndicator()) : _error != null ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( _error!, textAlign: TextAlign.center, style: const TextStyle(color: AppColors.textSecondary), ), const SizedBox(height: AppSpacing.lg), TextButton( onPressed: _loadTasks, child: const Text('Retry'), ), ], ), ) : LayoutBuilder( builder: (context, constraints) { return Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 390), child: RefreshIndicator( onRefresh: () => _loadTasks(refresh: true), child: _gridItems.isEmpty && !_loading ? SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), child: SizedBox( height: constraints.maxHeight - 100, child: const Center( child: Text( 'No images yet', style: TextStyle( color: AppColors.textSecondary, ), ), ), ), ) : GridView.builder( physics: const AlwaysScrollableScrollPhysics(), controller: _scrollController, padding: EdgeInsets.fromLTRB( AppSpacing.screenPadding, AppSpacing.xl, AppSpacing.screenPadding, AppSpacing.screenPaddingLarge + (_loadingMore ? 48.0 : 0), ), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, childAspectRatio: 165 / 248, mainAxisSpacing: AppSpacing.xl, crossAxisSpacing: AppSpacing.xl, ), itemCount: _gridItems.length + (_loadingMore ? 1 : 0), itemBuilder: (context, index) { if (index >= _gridItems.length) { return const Center( child: Padding( padding: EdgeInsets.all(16), child: SizedBox( width: 24, height: 24, child: CircularProgressIndicator( strokeWidth: 2, ), ), ), ); } return _GalleryCard( mediaItem: _gridItems[index], onTap: () { Navigator.of(context).pushNamed( '/result', arguments: _gridItems[index], ); }, ); }, ), ), ), ); }, ), ); } } class _GalleryCard extends StatelessWidget { const _GalleryCard({ required this.mediaItem, required this.onTap, }); final GalleryMediaItem mediaItem; final VoidCallback onTap; @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: LayoutBuilder( builder: (context, constraints) { return Container( width: constraints.maxWidth, height: constraints.maxHeight, decoration: BoxDecoration( color: AppColors.surfaceAlt, borderRadius: BorderRadius.circular(24), border: Border.all(color: AppColors.border, width: 1), boxShadow: const [ BoxShadow( color: AppColors.shadowMedium, blurRadius: 12, offset: Offset(0, 4), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(24), child: mediaItem.isVideo ? _VideoThumbnailCover(videoUrl: mediaItem.videoUrl!) : (mediaItem.imageUrl != null && mediaItem.imageUrl!.isNotEmpty) ? CachedNetworkImage( imageUrl: mediaItem.imageUrl!, fit: BoxFit.cover, placeholder: (_, __) => Container( color: AppColors.surfaceAlt, ), errorWidget: (_, __, ___) => Container( color: AppColors.surfaceAlt, ), ) : Container(color: AppColors.surfaceAlt), ), ); }, ), ); } } class _VideoThumbnailCover extends StatelessWidget { const _VideoThumbnailCover({required this.videoUrl}); final String videoUrl; @override Widget build(BuildContext context) { return FutureBuilder( future: VideoThumbnailCache.instance.getThumbnail(videoUrl), builder: (context, snapshot) { if (snapshot.hasData && snapshot.data != null) { return Image.memory( snapshot.data!, fit: BoxFit.cover, ); } return Container( color: AppColors.surfaceAlt, child: snapshot.connectionState == ConnectionState.waiting ? const Center( child: SizedBox( width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2), ), ) : const SizedBox.shrink(), ); }, ); } }