import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; import '../../core/service_locator.dart'; /// Media gallery screen for viewing recipe images and videos in full screen. /// Supports swiping between media and pinch-to-zoom for images. class PhotoGalleryScreen extends StatefulWidget { final List imageUrls; final List videoUrls; final int initialIndex; const PhotoGalleryScreen({ super.key, required this.imageUrls, this.videoUrls = const [], this.initialIndex = 0, }); @override State createState() => _PhotoGalleryScreenState(); } class _PhotoGalleryScreenState extends State { late PageController _pageController; late int _currentIndex; final Map _videoControllers = {}; List get _allMediaUrls { return [...widget.imageUrls, ...widget.videoUrls]; } bool _isVideo(int index) { return index >= widget.imageUrls.length; } @override void initState() { super.initState(); _currentIndex = widget.initialIndex; _pageController = PageController(initialPage: widget.initialIndex); _initializeVideo(_currentIndex); } @override void dispose() { _pageController.dispose(); for (final controller in _videoControllers.values) { controller.dispose(); } super.dispose(); } void _initializeVideo(int index) { if (_isVideo(index)) { final videoIndex = index - widget.imageUrls.length; if (videoIndex >= 0 && videoIndex < widget.videoUrls.length) { final videoUrl = widget.videoUrls[videoIndex]; if (!_videoControllers.containsKey(index)) { final controller = VideoPlayerController.networkUrl(Uri.parse(videoUrl)); controller.initialize().then((_) { if (mounted) { setState(() {}); controller.play(); } }); _videoControllers[index] = controller; } } } } void _disposeVideo(int index) { final controller = _videoControllers.remove(index); controller?.dispose(); } void _goToPrevious() { if (_currentIndex > 0) { _disposeVideo(_currentIndex); _currentIndex--; _initializeVideo(_currentIndex); _pageController.previousPage( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, ); } } void _goToNext() { if (_currentIndex < _allMediaUrls.length - 1) { _disposeVideo(_currentIndex); _currentIndex++; _initializeVideo(_currentIndex); _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.withValues(alpha: 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} / ${_allMediaUrls.length}', style: const TextStyle(color: Colors.white), ), centerTitle: true, ), body: Stack( children: [ // Media viewer PageView.builder( controller: _pageController, itemCount: _allMediaUrls.length, onPageChanged: (index) { setState(() { _disposeVideo(_currentIndex); _currentIndex = index; _initializeVideo(_currentIndex); }); }, itemBuilder: (context, index) { if (_isVideo(index)) { return _buildVideo(index); } else { return Center( child: InteractiveViewer( minScale: 0.5, maxScale: 3.0, child: _buildImage(widget.imageUrls[index]), ), ); } }, ), // Navigation arrows if (_allMediaUrls.length > 1) ...[ // Previous arrow (left) if (_currentIndex > 0) Positioned( left: 16, top: 0, bottom: 0, child: Center( child: Container( decoration: BoxDecoration( color: Colors.black.withValues(alpha: 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 < _allMediaUrls.length - 1) Positioned( right: 16, top: 0, bottom: 0, child: Center( child: Container( decoration: BoxDecoration( color: Colors.black.withValues(alpha: 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 MediaService for authenticated access. Widget _buildImage(String imageUrl) { final mediaService = ServiceLocator.instance.mediaService; if (mediaService == null) { return Image.network(imageUrl, fit: BoxFit.contain); } // Try to extract asset ID from Immich URL (format: .../api/assets/{id}/original) final assetIdMatch = RegExp(r'/api/assets/([^/]+)/').firstMatch(imageUrl); String? assetId; if (assetIdMatch != null) { assetId = assetIdMatch.group(1); } else { // For Blossom URLs, use the full URL assetId = imageUrl; } if (assetId != null) { // Use MediaService to fetch full image (not thumbnail) for gallery view return FutureBuilder( future: mediaService.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), ), ); }, ); } /// Builds a video player widget. Widget _buildVideo(int index) { final controller = _videoControllers[index]; if (controller == null || !controller.value.isInitialized) { return const Center( child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(Colors.white), ), ); } return Center( child: AspectRatio( aspectRatio: controller.value.aspectRatio, child: VideoPlayer(controller), ), ); } }