import 'dart:typed_data'; import 'package:flutter/material.dart'; import '../../core/service_locator.dart'; /// Photo gallery screen for viewing recipe images in full screen. /// Supports swiping between images and pinch-to-zoom. class PhotoGalleryScreen extends StatefulWidget { final List imageUrls; final int initialIndex; const PhotoGalleryScreen({ super.key, required this.imageUrls, this.initialIndex = 0, }); @override State createState() => _PhotoGalleryScreenState(); } class _PhotoGalleryScreenState extends State { late PageController _pageController; late int _currentIndex; @override void initState() { super.initState(); _currentIndex = widget.initialIndex; _pageController = PageController(initialPage: widget.initialIndex); } @override void dispose() { _pageController.dispose(); super.dispose(); } void _goToPrevious() { if (_currentIndex > 0) { _pageController.previousPage( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, ); } } void _goToNext() { if (_currentIndex < widget.imageUrls.length - 1) { _pageController.nextPage( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, ); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, appBar: AppBar( backgroundColor: Colors.transparent, elevation: 0, leading: Container( margin: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.black.withOpacity(0.5), shape: BoxShape.circle, ), child: IconButton( icon: const Icon(Icons.close, color: Colors.white), onPressed: () => Navigator.pop(context), padding: EdgeInsets.zero, ), ), title: Text( '${_currentIndex + 1} / ${widget.imageUrls.length}', style: const TextStyle(color: Colors.white), ), centerTitle: true, ), body: Stack( children: [ // Photo viewer PageView.builder( controller: _pageController, itemCount: widget.imageUrls.length, onPageChanged: (index) { setState(() { _currentIndex = index; }); }, itemBuilder: (context, index) { return Center( child: InteractiveViewer( minScale: 0.5, maxScale: 3.0, child: _buildImage(widget.imageUrls[index]), ), ); }, ), // Navigation arrows if (widget.imageUrls.length > 1) ...[ // Previous arrow (left) if (_currentIndex > 0) Positioned( left: 16, top: 0, bottom: 0, child: Center( child: Container( decoration: BoxDecoration( color: Colors.black.withOpacity(0.5), shape: BoxShape.circle, ), child: IconButton( icon: const Icon( Icons.chevron_left, color: Colors.white, size: 32, ), onPressed: _goToPrevious, padding: const EdgeInsets.all(16), ), ), ), ), // Next arrow (right) if (_currentIndex < widget.imageUrls.length - 1) Positioned( right: 16, top: 0, bottom: 0, child: Center( child: Container( decoration: BoxDecoration( color: Colors.black.withOpacity(0.5), shape: BoxShape.circle, ), child: IconButton( icon: const Icon( Icons.chevron_right, color: Colors.white, size: 32, ), onPressed: _goToNext, padding: const EdgeInsets.all(16), ), ), ), ), ], ], ), ); } /// Builds an image widget using ImmichService for authenticated access. Widget _buildImage(String imageUrl) { final immichService = ServiceLocator.instance.immichService; // Try to extract asset ID from URL (format: .../api/assets/{id}/original) final assetIdMatch = RegExp(r'/api/assets/([^/]+)/').firstMatch(imageUrl); if (assetIdMatch != null && immichService != null) { final assetId = assetIdMatch.group(1); if (assetId != null) { // Use ImmichService to fetch full image (not thumbnail) for gallery view return FutureBuilder( future: immichService.fetchImageBytes(assetId, isThumbnail: false), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center( child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(Colors.white), ), ); } if (snapshot.hasError || !snapshot.hasData || snapshot.data == null) { return const Center( child: Icon( Icons.image_not_supported, size: 64, color: Colors.white54, ), ); } return Image.memory( snapshot.data!, fit: BoxFit.contain, ); }, ); } } // Fallback to direct network image if not an Immich URL or service unavailable return Image.network( imageUrl, fit: BoxFit.contain, errorBuilder: (context, error, stackTrace) { return const Center( child: Icon( Icons.image_not_supported, size: 64, color: Colors.white54, ), ); }, loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; return Center( child: CircularProgressIndicator( value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null, valueColor: const AlwaysStoppedAnimation(Colors.white), ), ); }, ); } }