import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:camera/camera.dart'; import 'package:file_picker/file_picker.dart'; import 'package:file_selector/file_selector.dart' as fsel; import 'package:votianlt_app/services/developer.dart' as developer; import '../l10n/app_localizations.dart'; import '../models/tasks/photo_task.dart'; import '../widgets/offline_banner.dart'; class PhotoCaptureScreen extends StatefulWidget { final PhotoTask task; final Function(List) onPhotosCompleted; const PhotoCaptureScreen({ super.key, required this.task, required this.onPhotosCompleted, }); @override State createState() => _PhotoCaptureScreenState(); } class _PhotoCaptureScreenState extends State { CameraController? _cameraController; // Android/iOS/Web List? _cameras; final List _capturedPhotos = []; final PageController _pageController = PageController(); int _currentPhotoIndex = 0; bool _isCameraInitialized = false; bool _isCameraSupportedOnThisPlatform = false; bool _useFilePickerMode = false; // desktop fallback @override void initState() { super.initState(); _detectPlatformSupportAndInit(); } @override void dispose() { _cameraController?.dispose(); _pageController.dispose(); super.dispose(); } void _detectPlatformSupportAndInit() { // Requirement: Desktop (macOS/Windows/Linux) uses file picker; Android/iOS uses camera. if (kIsWeb) { // Keep web behavior using camera if available _isCameraSupportedOnThisPlatform = true; _initializeCamera(); return; } switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.iOS: _isCameraSupportedOnThisPlatform = true; // enable capture button _useFilePickerMode = false; _initializeCamera(); return; case TargetPlatform.macOS: case TargetPlatform.windows: case TargetPlatform.linux: // Desktop → use file picker instead of camera _isCameraSupportedOnThisPlatform = true; // enable button _useFilePickerMode = true; return; default: _isCameraSupportedOnThisPlatform = false; } } Future _initializeCamera() async { try { // Android/iOS/Web camera initialization _cameras = await availableCameras(); if (_cameras != null && _cameras!.isNotEmpty) { _cameraController = CameraController( _cameras![0], ResolutionPreset.medium, ); await _cameraController!.initialize(); if (mounted) { setState(() { _isCameraInitialized = true; }); } } } catch (e, stackTrace) { developer.log('Error initializing camera: $e', name: 'PhotoCaptureScreen'); developer.log('Stack trace: $stackTrace', name: 'PhotoCaptureScreen'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('${AppLocalizations.of(context).cameraError}: $e')), ); } } } Future _capturePhoto() async { try { // Camera plugin path (Android/iOS/Web) if (_cameraController != null && _isCameraInitialized) { final XFile photo = await _cameraController!.takePicture(); final Uint8List photoBytes = await photo.readAsBytes(); setState(() { _capturedPhotos.add(photoBytes); }); // Navigate to the newly added photo (even if it's the first one). If the // PageView is not attached yet, _showPhotoAt will schedule a post-frame jump. _showPhotoAt(_capturedPhotos.length - 1); } else { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(AppLocalizations.of(context).cameraNotReady)), ); } } } catch (e, stackTrace) { developer.log('Error capturing photo: $e', name: 'PhotoCaptureScreen'); developer.log('Stack trace: $stackTrace', name: 'PhotoCaptureScreen'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('${AppLocalizations.of(context).photoError}: $e')), ); } } } Future _pickPhotoFromFile() async { try { // Use file_selector for desktop and web for robust platform support final bool useFileSelector = kIsWeb || defaultTargetPlatform == TargetPlatform.macOS || defaultTargetPlatform == TargetPlatform.windows || defaultTargetPlatform == TargetPlatform.linux; if (useFileSelector) { final typeGroup = fsel.XTypeGroup( label: 'images', extensions: ['jpg', 'jpeg', 'png', 'heic', 'bmp', 'gif', 'webp'], ); final fsel.XFile? picked = await fsel.openFile(acceptedTypeGroups: [typeGroup]); if (picked != null) { final data = await picked.readAsBytes(); setState(() { _capturedPhotos.add(data); }); _showPhotoAt(_capturedPhotos.length - 1); } } else { // On Android/iOS, use file_picker which integrates with platform pickers final result = await FilePicker.platform.pickFiles( allowMultiple: false, type: FileType.image, withData: true, ); if (result != null && result.files.isNotEmpty) { final file = result.files.first; final bytes = file.bytes; if (bytes != null) { setState(() { _capturedPhotos.add(bytes); }); _showPhotoAt(_capturedPhotos.length - 1); } else { // On some platforms, bytes may be null if withData was false; try path final path = file.path; if (path != null) { final data = await XFile(path).readAsBytes(); setState(() { _capturedPhotos.add(data); }); if (_capturedPhotos.length > 1) { _showPhotoAt(_capturedPhotos.length - 1); } } } } } } catch (e, stackTrace) { developer.log('Error picking photo from file: $e', name: 'PhotoCaptureScreen'); developer.log('Stack trace: $stackTrace', name: 'PhotoCaptureScreen'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('${AppLocalizations.of(context).photoError}: $e')), ); } } } void _deletePhoto(int index) { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text(AppLocalizations.of(context).deletePhoto), content: Text(AppLocalizations.of(context).deletePhotoConfirm), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: Text(AppLocalizations.of(context).cancel), ), ElevatedButton( onPressed: () { Navigator.of(context).pop(); // Remove and navigate to a valid photo if any remain int targetIndex = _currentPhotoIndex; setState(() { _capturedPhotos.removeAt(index); if (_capturedPhotos.isEmpty) { _currentPhotoIndex = 0; } else { if (targetIndex >= _capturedPhotos.length) { targetIndex = _capturedPhotos.length - 1; } _currentPhotoIndex = targetIndex; } }); if (_capturedPhotos.isNotEmpty) { _showPhotoAt(_currentPhotoIndex); } }, style: ElevatedButton.styleFrom(backgroundColor: Colors.red), child: Text(AppLocalizations.of(context).delete, style: const TextStyle(color: Colors.white)), ), ], ); }, ); } bool get _canComplete { return _capturedPhotos.length >= widget.task.minPhotoCount && _capturedPhotos.length <= widget.task.maxPhotoCount; } bool get _canTakeMore { return _capturedPhotos.length < widget.task.maxPhotoCount; } bool get _isDesktopPlatform { if (kIsWeb) return false; switch (defaultTargetPlatform) { case TargetPlatform.macOS: case TargetPlatform.windows: case TargetPlatform.linux: return true; default: return false; } } void _showPhotoAt(int index) { if (_capturedPhotos.isEmpty) return; final int clamped = index.clamp(0, _capturedPhotos.length - 1); if (!mounted) return; // Always schedule navigation after the current frame so that // PageView has rebuilt with the latest itemCount before we jump/animate. WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; if (_pageController.hasClients) { try { _pageController.animateToPage( clamped, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut, ); } catch (e, stackTrace) { developer.log('Error animating to page: $e', name: 'PhotoCaptureScreen'); developer.log('Stack trace: $stackTrace', name: 'PhotoCaptureScreen'); _pageController.jumpToPage(clamped); } setState(() { _currentPhotoIndex = clamped; }); } }); } void _goToPreviousPhoto() { if (_currentPhotoIndex > 0) { _showPhotoAt(_currentPhotoIndex - 1); } } void _goToNextPhoto() { if (_currentPhotoIndex < _capturedPhotos.length - 1) { _showPhotoAt(_currentPhotoIndex + 1); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context).photoCapture), backgroundColor: Colors.blue, foregroundColor: Colors.white, actions: [ if (_canComplete) TextButton( onPressed: () { widget.onPhotosCompleted(_capturedPhotos); Navigator.of(context).pop(); }, child: Text( AppLocalizations.of(context).finish, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), ), ), ], ), body: Column( children: [ OfflineBanner(), // Task info header Container( width: double.infinity, padding: EdgeInsets.all(16), color: Colors.grey[100], child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${AppLocalizations.of(context).requiredPhotos}: ${widget.task.minPhotoCount}-${widget.task.maxPhotoCount}', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), Text( '${AppLocalizations.of(context).photosTaken}: ${_capturedPhotos.length}', style: TextStyle(fontSize: 14, color: Colors.grey[600]), ), ], ), ), // Camera preview, photo gallery or empty state Expanded( child: _capturedPhotos.isEmpty ? _buildCameraOrEmptyState() : _buildPhotoGallery(), ), // Bottom controls Container( padding: EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, boxShadow: [ BoxShadow( color: Colors.grey.withValues(alpha: 0.3), spreadRadius: 1, blurRadius: 5, offset: Offset(0, -3), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ // Camera or file select button Expanded( child: ElevatedButton.icon( onPressed: _canTakeMore && _isCameraSupportedOnThisPlatform ? (_useFilePickerMode ? _pickPhotoFromFile : (_isCameraInitialized ? _capturePhoto : null)) : null, icon: Icon(_useFilePickerMode ? Icons.photo_library : Icons.camera_alt), label: Text( !_isCameraSupportedOnThisPlatform ? AppLocalizations.of(context).cameraNotSupportedOnPlatform : (!_canTakeMore ? AppLocalizations.of(context).maxPhotosReached : (_useFilePickerMode ? AppLocalizations.of(context).selectPhoto : (_isCameraInitialized ? AppLocalizations.of(context).takePhoto : (defaultTargetPlatform == TargetPlatform.macOS ? AppLocalizations.of(context).cameraReadyNoPreview : AppLocalizations.of(context).cameraLoading)))), ), style: ElevatedButton.styleFrom( backgroundColor: _canTakeMore && (_useFilePickerMode || _isCameraInitialized) ? Colors.blue : Colors.grey, foregroundColor: Colors.white, padding: EdgeInsets.symmetric(vertical: 12), ), ), ), if (_capturedPhotos.isNotEmpty) ...[ SizedBox(width: 16), // Delete current photo button ElevatedButton.icon( onPressed: () => _deletePhoto(_currentPhotoIndex), icon: Icon(Icons.delete), label: Text(AppLocalizations.of(context).delete), style: ElevatedButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16), ), ), ], ], ), SizedBox(height: 12), // Bottom 'Fertig' button placed under the row SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _canComplete ? () { widget.onPhotosCompleted(_capturedPhotos); Navigator.of(context).pop(); } : null, style: ElevatedButton.styleFrom( backgroundColor: _canComplete ? Colors.green : Colors.grey, foregroundColor: Colors.white, padding: EdgeInsets.symmetric(vertical: 14), ), child: Text(AppLocalizations.of(context).finish, style: const TextStyle(fontWeight: FontWeight.bold)), ), ), ], ), ), ], ), ); } Widget _buildCameraOrEmptyState() { // If platform not supported, show informative message if (!_isCameraSupportedOnThisPlatform) { return Center( child: Padding( padding: EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.desktop_windows, size: 80, color: Colors.grey[400]), SizedBox(height: 16), Text( AppLocalizations.of(context).cameraNotAvailable, style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.grey[700]), ), SizedBox(height: 8), Text( AppLocalizations.of(context).cameraNotSupportedMessage, textAlign: TextAlign.center, style: TextStyle(fontSize: 14, color: Colors.grey[600]), ), ], ), ), ); } // macOS fallback: explain file selection if (_useFilePickerMode) { return Center( child: Padding( padding: EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.photo_library, size: 80, color: Colors.grey[400]), SizedBox(height: 16), Text( AppLocalizations.of(context).addPhotos, style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.grey[700]), ), SizedBox(height: 8), Text( AppLocalizations.of(context).addPhotosInstruction, textAlign: TextAlign.center, style: TextStyle(fontSize: 14, color: Colors.grey[600]), ), ], ), ), ); } // Show camera preview if available on any platform if (_isCameraInitialized && _cameraController != null) { return Container( margin: EdgeInsets.all(16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.grey.withValues(alpha: 0.3), spreadRadius: 2, blurRadius: 8, offset: Offset(0, 4), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(12), child: CameraPreview(_cameraController!), ), ); } // When camera is not available, show empty state return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.camera_alt, size: 80, color: Colors.grey[400], ), SizedBox(height: 16), Text( AppLocalizations.of(context).cameraInitializing, style: TextStyle( fontSize: 18, color: Colors.grey[600], fontWeight: FontWeight.w500, ), ), SizedBox(height: 8), Text( AppLocalizations.of(context).cameraLoadingMessage, style: TextStyle( fontSize: 14, color: Colors.grey[500], ), textAlign: TextAlign.center, ), ], ), ); } Widget _buildPhotoGallery() { return Column( children: [ // Photo counter (if more than one photo) if (_capturedPhotos.length > 1) Container( padding: EdgeInsets.symmetric(vertical: 8), child: Text( '${_currentPhotoIndex + 1} ${AppLocalizations.of(context).photoOf} ${_capturedPhotos.length}', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Colors.grey[700], ), ), ), // Photo viewer with swipe gestures and navigation arrows Expanded( child: Stack( children: [ PageView.builder( controller: _pageController, onPageChanged: (index) { setState(() { _currentPhotoIndex = index; }); }, itemCount: _capturedPhotos.length, itemBuilder: (context, index) { return Container( margin: EdgeInsets.all(16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.grey.withValues(alpha: 0.3), spreadRadius: 2, blurRadius: 8, offset: Offset(0, 4), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(12), child: Image.memory( _capturedPhotos[index], fit: BoxFit.contain, errorBuilder: (context, error, stackTrace) { return Container( color: Colors.grey[300], child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error, size: 50, color: Colors.grey[600]), SizedBox(height: 8), Text(AppLocalizations.of(context).photoError), ], ), ), ); }, ), ), ); }, ), if (_capturedPhotos.length > 1 && _isDesktopPlatform) Positioned( left: 8, top: 0, bottom: 0, child: Center( child: IconButton( onPressed: _currentPhotoIndex > 0 ? _goToPreviousPhoto : null, icon: Icon(Icons.chevron_left, size: 36), style: IconButton.styleFrom( backgroundColor: Colors.white.withValues(alpha: 0.7), ), ), ), ), if (_capturedPhotos.length > 1 && _isDesktopPlatform) Positioned( right: 8, top: 0, bottom: 0, child: Center( child: IconButton( onPressed: _currentPhotoIndex < _capturedPhotos.length - 1 ? _goToNextPhoto : null, icon: Icon(Icons.chevron_right, size: 36), style: IconButton.styleFrom( backgroundColor: Colors.white.withValues(alpha: 0.7), ), ), ), ), ], ), ), // Photo indicators (if more than one photo) if (_capturedPhotos.length > 1) Container( padding: EdgeInsets.symmetric(vertical: 16), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: _capturedPhotos.asMap().entries.map((entry) { return Container( width: 8, height: 8, margin: EdgeInsets.symmetric(horizontal: 4), decoration: BoxDecoration( shape: BoxShape.circle, color: _currentPhotoIndex == entry.key ? Colors.blue : Colors.grey[400], ), ); }).toList(), ), ), ], ); } }