import 'dart:io'; import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'camera_screen.dart'; import '../../services/firebase_service.dart'; import '../../models/recommendation_model.dart'; import '../recommendation/recommendation_screen.dart'; /// Layar pemilihan foto menu — UX seperti upload gambar di AI. /// User bisa tambah/hapus foto (maks 3) sebelum scan. class PhotoPickerScreen extends StatefulWidget { final List initialPaths; const PhotoPickerScreen({super.key, this.initialPaths = const []}); @override State createState() => _PhotoPickerScreenState(); } class _PhotoPickerScreenState extends State { final List _selectedPaths = []; bool _isLoading = false; List _history = []; bool _isLoadingHistory = true; static const int _maxPhotos = 3; static const Color _brown = Color(0xFF8B5E3C); static const Color _darkBrown = Color(0xFF3C2415); static const Color _bgColor = Color(0xFFFAF7F4); @override void initState() { super.initState(); _selectedPaths.addAll(widget.initialPaths); _loadHistory(); } Future _loadHistory() async { try { final user = FirebaseService().currentUser; if (user != null) { final allHistory = await FirebaseService().getUserRecommendations(user.uid, limit: 30); final Map uniqueMenus = {}; final now = DateTime.now(); for (var rec in allHistory) { // Otomatis hapus draf yang lebih dari 24 jam dan belum dipilih if (now.difference(rec.createdAt).inHours > 24) { if (rec.selectedMenuName == null) { FirebaseService().deleteRecommendation(rec.id); } continue; } final key = rec.originalMenuText.length > 50 ? rec.originalMenuText.substring(0, 50) : rec.originalMenuText; if (!uniqueMenus.containsKey(key)) { uniqueMenus[key] = rec; } } if (mounted) { setState(() { _history = uniqueMenus.values.take(3).toList(); _isLoadingHistory = false; }); } } else { if (mounted) setState(() => _isLoadingHistory = false); } } catch (e) { if (mounted) setState(() => _isLoadingHistory = false); } } // ── Actions ────────────────────────────────────────────────────────────── Future _pickFromGallery() async { if (_selectedPaths.length >= _maxPhotos) { _showMaxReached(); return; } try { final remaining = _maxPhotos - _selectedPaths.length; final images = await ImagePicker().pickMultiImage( imageQuality: 80, maxWidth: 1920, maxHeight: 1920, limit: remaining, ); if (images.isEmpty) return; setState( () => _selectedPaths.addAll(images.take(remaining).map((f) => f.path)), ); } catch (_) { _showError('Gagal membuka galeri'); } } Future _pickFromCamera() async { if (_selectedPaths.length >= _maxPhotos) { _showMaxReached(); return; } try { final image = await ImagePicker().pickImage( source: ImageSource.camera, imageQuality: 80, maxWidth: 1920, maxHeight: 1920, ); if (image == null) return; if (await File(image.path).exists()) setState(() => _selectedPaths.add(image.path)); } catch (_) { _showError('Gagal membuka kamera'); } } void _removePhoto(int index) => setState(() => _selectedPaths.removeAt(index)); void _showMaxReached() { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text( 'Maksimal 3 foto dapat dipilih', style: TextStyle(fontFamily: 'Poppins'), ), backgroundColor: _brown, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), duration: const Duration(seconds: 2), ), ); } void _showError(String msg) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(msg, style: const TextStyle(fontFamily: 'Poppins')), backgroundColor: Colors.red.shade600, behavior: SnackBarBehavior.floating, ), ); } Future _deleteHistory(RecommendationModel rec) async { final confirm = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Hapus Draf?', style: TextStyle(fontFamily: 'Poppins', fontWeight: FontWeight.bold, color: _darkBrown)), content: const Text('Draf menu ini akan dihapus dari riwayat scan Anda.'), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('Batal', style: TextStyle(color: Colors.grey)), ), ElevatedButton( onPressed: () => Navigator.pop(ctx, true), style: ElevatedButton.styleFrom(backgroundColor: Colors.red.shade600), child: const Text('Hapus', style: TextStyle(color: Colors.white)), ), ], ), ); if (confirm == true) { setState(() { _history.remove(rec); }); // Hanya hapus dari database jika belum pernah dipilih ke riwayat utama if (rec.selectedMenuName == null) { FirebaseService().deleteRecommendation(rec.id); } } } Future _proceedToScan() async { if (_selectedPaths.isEmpty) return; setState(() => _isLoading = true); await Future.delayed(const Duration(milliseconds: 80)); if (!mounted) return; await Navigator.of(context).push( PageRouteBuilder( pageBuilder: (_, a, __) => CameraScreen(imagePaths: List.from(_selectedPaths)), transitionsBuilder: (_, a, __, child) => SlideTransition( position: Tween( begin: const Offset(0, 1), end: Offset.zero, ).chain(CurveTween(curve: Curves.easeOutCubic)).animate(a), child: child, ), transitionDuration: const Duration(milliseconds: 350), ), ); if (mounted) setState(() => _isLoading = false); } Future _proceedWithHistory(RecommendationModel scan) async { setState(() => _isLoading = true); await Future.delayed(const Duration(milliseconds: 80)); if (!mounted) return; await Navigator.of(context).push( PageRouteBuilder( pageBuilder: (_, a, __) => RecommendationScreen(recommendation: scan, ocrResult: null), transitionsBuilder: (_, a, __, child) => SlideTransition( position: Tween( begin: const Offset(0, 1), end: Offset.zero, ).chain(CurveTween(curve: Curves.easeOutCubic)).animate(a), child: child, ), transitionDuration: const Duration(milliseconds: 350), ), ); if (mounted) setState(() => _isLoading = false); } void _showAddPhotoMenu() { showModalBottomSheet( context: context, backgroundColor: Colors.transparent, builder: (ctx) => Container( decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), padding: const EdgeInsets.fromLTRB(24, 16, 24, 32), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 40, height: 4, decoration: BoxDecoration( color: const Color(0xFFD4CFC8), borderRadius: BorderRadius.circular(2), ), ), const SizedBox(height: 20), const Text( 'Tambah Foto Menu', style: TextStyle( fontFamily: 'Poppins', fontWeight: FontWeight.w700, fontSize: 17, color: _darkBrown, ), ), const SizedBox(height: 4), Text( '${_selectedPaths.length}/$_maxPhotos foto dipilih', style: TextStyle( fontFamily: 'Poppins', fontSize: 12, color: _brown.withOpacity(0.65), ), ), const SizedBox(height: 20), Row( children: [ Expanded( child: _buildSheetOption( icon: Icons.photo_library_rounded, label: 'Galeri', subtitle: 'Pilih beberapa', color: _brown, onTap: () { Navigator.pop(ctx); _pickFromGallery(); }, ), ), const SizedBox(width: 12), Expanded( child: _buildSheetOption( icon: Icons.camera_alt_rounded, label: 'Kamera', subtitle: 'Ambil foto', color: const Color(0xFF6B3A2A), onTap: () { Navigator.pop(ctx); _pickFromCamera(); }, ), ), ], ), ], ), ), ); } // ── Build ───────────────────────────────────────────────────────────────── @override Widget build(BuildContext context) { return Scaffold( backgroundColor: _bgColor, appBar: _buildAppBar(), body: SafeArea( child: Column( children: [ Expanded(child: _buildBody()), _buildBottomBar(), ], ), ), ); } PreferredSizeWidget _buildAppBar() => AppBar( backgroundColor: Colors.white, elevation: 0, leading: IconButton( icon: const Icon( Icons.arrow_back_ios_rounded, color: _darkBrown, size: 20, ), onPressed: () => Navigator.pop(context), ), title: const Text( 'Pilih Foto Menu', style: TextStyle( fontFamily: 'Poppins', fontWeight: FontWeight.w700, fontSize: 18, color: _darkBrown, ), ), centerTitle: true, bottom: PreferredSize( preferredSize: const Size.fromHeight(1), child: Container(height: 1, color: const Color(0xFFF0EBE4)), ), ); Widget _buildBody() => SingleChildScrollView( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildInfoHeader(), const SizedBox(height: 20), _buildPhotoGrid(), const SizedBox(height: 24), _buildHistorySection(), const SizedBox(height: 24), _buildTips(), ], ), ); Widget _buildInfoHeader() { final count = _selectedPaths.length; return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( gradient: LinearGradient( colors: [_brown.withOpacity(0.08), _brown.withOpacity(0.03)], ), borderRadius: BorderRadius.circular(16), border: Border.all(color: _brown.withOpacity(0.15)), ), child: Row( children: [ Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: _brown.withOpacity(0.12), borderRadius: BorderRadius.circular(12), ), child: const Icon( Icons.document_scanner_rounded, color: _brown, size: 22, ), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( count == 0 ? 'Belum ada foto dipilih' : '$count foto dipilih', style: const TextStyle( fontFamily: 'Poppins', fontWeight: FontWeight.w700, fontSize: 14, color: _darkBrown, ), ), Text( 'Maksimal $_maxPhotos foto menu · ketuk slot untuk menambah', style: TextStyle( fontFamily: 'Poppins', fontSize: 11, color: _brown.withOpacity(0.7), ), ), ], ), ), const SizedBox(width: 10), _buildDotProgress(), ], ), ); } Widget _buildDotProgress() => Row( mainAxisSize: MainAxisSize.min, children: List.generate(_maxPhotos, (i) { final filled = i < _selectedPaths.length; return AnimatedContainer( duration: const Duration(milliseconds: 300), margin: const EdgeInsets.only(left: 5), width: filled ? 10 : 8, height: filled ? 10 : 8, decoration: BoxDecoration( shape: BoxShape.circle, color: filled ? _brown : _brown.withOpacity(0.2), border: Border.all( color: filled ? _brown : _brown.withOpacity(0.3), width: 1.5, ), ), ); }), ); Widget _buildPhotoGrid() { final count = _selectedPaths.length; final showAdd = count < _maxPhotos; final totalCells = count + (showAdd ? 1 : 0); return GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, crossAxisSpacing: 10, mainAxisSpacing: 10, childAspectRatio: 0.85, ), itemCount: totalCells, itemBuilder: (ctx, i) => i < count ? _buildPhotoThumbnail(i) : _buildAddSlot(), ); } Widget _buildPhotoThumbnail(int index) => Stack( children: [ ClipRRect( borderRadius: BorderRadius.circular(14), child: Image.file( File(_selectedPaths[index]), fit: BoxFit.cover, width: double.infinity, height: double.infinity, errorBuilder: (_, __, ___) => Container( color: const Color(0xFFE8E0D8), child: const Icon( Icons.broken_image_rounded, color: Colors.white54, size: 32, ), ), ), ), // Gradient overlay atas Positioned( top: 0, left: 0, right: 0, child: Container( height: 36, decoration: BoxDecoration( borderRadius: const BorderRadius.vertical(top: Radius.circular(14)), gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Colors.black.withOpacity(0.35), Colors.transparent], ), ), ), ), // Nomor badge Positioned( top: 6, left: 8, child: Container( padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2), decoration: BoxDecoration( color: _brown, borderRadius: BorderRadius.circular(6), ), child: Text( '${index + 1}', style: const TextStyle( fontFamily: 'Poppins', color: Colors.white, fontSize: 10, fontWeight: FontWeight.w700, ), ), ), ), // Tombol hapus Positioned( top: 4, right: 4, child: GestureDetector( onTap: () => _removePhoto(index), child: Container( width: 26, height: 26, decoration: BoxDecoration( color: Colors.red.shade600, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.25), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: const Icon( Icons.close_rounded, color: Colors.white, size: 14, ), ), ), ), ], ); Widget _buildHistorySection() { if (_isLoadingHistory) { return const Center(child: CircularProgressIndicator(color: _brown)); } if (_history.isEmpty) { return const SizedBox.shrink(); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Gunakan Menu Sebelumnya', style: TextStyle( fontFamily: 'Poppins', fontWeight: FontWeight.w700, fontSize: 16, color: _darkBrown, ), ), const SizedBox(height: 12), SizedBox( height: 140, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: _history.length, separatorBuilder: (context, index) => const SizedBox(width: 12), itemBuilder: (context, index) { final rec = _history[index]; // Buat judul simulasi dari menu rekomendasi pertama final title = rec.recommendations.isNotEmpty ? rec.recommendations.first.menuName : 'Menu Cafe'; return Stack( children: [ GestureDetector( onTap: () => _proceedWithHistory(rec), child: Container( width: 130, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: _brown.withOpacity(0.15)), boxShadow: [ BoxShadow( color: _brown.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), child: _buildHistoryImage(rec), ), Padding( padding: const EdgeInsets.all(10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( fontFamily: 'Poppins', fontWeight: FontWeight.w600, fontSize: 12, color: _darkBrown, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 2), Text( '${rec.recommendations.length} menu', style: TextStyle( fontFamily: 'Poppins', fontSize: 10, color: _brown.withOpacity(0.8), ), ), ], ), ), ], ), ), ), Positioned( top: 6, right: 6, child: GestureDetector( onTap: () => _deleteHistory(rec), child: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: Colors.red.shade600, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.2), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: const Icon( Icons.close_rounded, size: 14, color: Colors.white, ), ), ), ), ], ); }, ), ), ], ); } Widget _buildHistoryImage(RecommendationModel rec) { if (rec.menuImageUrl != null && rec.menuImageUrl!.startsWith('http')) { return Image.network( rec.menuImageUrl!, height: 70, width: double.infinity, fit: BoxFit.cover, errorBuilder: (_, __, ___) => _fallbackImage(), ); } else if (rec.menuImagePaths != null && rec.menuImagePaths!.isNotEmpty) { final file = File(rec.menuImagePaths!.first); if (file.existsSync()) { return Image.file( file, height: 70, width: double.infinity, fit: BoxFit.cover, ); } } return _fallbackImage(); } Widget _fallbackImage() => Container( height: 70, width: double.infinity, color: _brown.withOpacity(0.1), child: Icon(Icons.restaurant_menu_rounded, color: _brown.withOpacity(0.5)), ); /// Slot kosong bertitik dashed — mengindikasikan "bisa tambah lebih" tanpa tombol eksplisit Widget _buildAddSlot() => GestureDetector( onTap: _showAddPhotoMenu, child: CustomPaint( painter: _DashedBorderPainter(color: _brown.withOpacity(0.3), radius: 14), child: Container( decoration: BoxDecoration( color: _brown.withOpacity(0.025), borderRadius: BorderRadius.circular(14), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Dua ikon mini berdampingan sebagai hint sumber Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _miniIcon(Icons.photo_library_rounded), Container( margin: const EdgeInsets.symmetric(horizontal: 6), width: 1, height: 16, color: _brown.withOpacity(0.2), ), _miniIcon(Icons.camera_alt_rounded), ], ), const SizedBox(height: 8), // Lingkaran "+" kecil Container( width: 20, height: 20, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all(color: _brown.withOpacity(0.45), width: 1.5), ), child: Icon( Icons.add_rounded, size: 13, color: _brown.withOpacity(0.6), ), ), const SizedBox(height: 6), Text( '${_maxPhotos - _selectedPaths.length} slot tersisa', textAlign: TextAlign.center, style: TextStyle( fontFamily: 'Poppins', fontSize: 10, fontWeight: FontWeight.w500, color: _brown.withOpacity(0.5), height: 1.3, ), ), ], ), ), ), ); Widget _miniIcon(IconData icon) => Container( padding: const EdgeInsets.all(5), decoration: BoxDecoration( color: _brown.withOpacity(0.08), borderRadius: BorderRadius.circular(7), ), child: Icon(icon, size: 14, color: _brown.withOpacity(0.6)), ); Widget _buildTips() => Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFFE8DFD4)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Row( children: [ Icon( Icons.lightbulb_outline_rounded, size: 15, color: Color(0xFFE5A100), ), SizedBox(width: 6), Text( 'Tips Scan Menu', style: TextStyle( fontFamily: 'Poppins', fontWeight: FontWeight.w700, fontSize: 12, color: _darkBrown, ), ), ], ), const SizedBox(height: 8), ...[ 'Foto menu dengan pencahayaan yang baik', 'Jika menu terdiri dari beberapa halaman, tambah semua', 'Pastikan tulisan harga terbaca jelas', ].map( (tip) => Padding( padding: const EdgeInsets.only(top: 5), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '• ', style: TextStyle( color: _brown, fontWeight: FontWeight.bold, fontSize: 11, ), ), Expanded( child: Text( tip, style: const TextStyle( fontFamily: 'Poppins', fontSize: 11, color: Color(0xFF6B5B4E), height: 1.4, ), ), ), ], ), ), ), ], ), ); Widget _buildBottomBar() => Container( padding: const EdgeInsets.fromLTRB(20, 12, 20, 20), decoration: BoxDecoration( color: Colors.white, border: const Border(top: BorderSide(color: Color(0xFFF0EBE4))), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.04), blurRadius: 12, offset: const Offset(0, -4), ), ], ), child: SafeArea( top: false, child: AnimatedSwitcher( duration: const Duration(milliseconds: 300), child: _selectedPaths.isNotEmpty ? _buildScanButton() : _buildEmptyPrompt(), ), ), ); Widget _buildScanButton() => SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _isLoading ? null : _proceedToScan, style: ElevatedButton.styleFrom( backgroundColor: _brown, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), elevation: 4, shadowColor: _brown.withOpacity(0.4), ), child: _isLoading ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator( color: Colors.white, strokeWidth: 2, ), ) : Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.document_scanner_rounded, size: 20), const SizedBox(width: 10), Text( 'Scan ${_selectedPaths.length} Foto Menu', style: const TextStyle( fontFamily: 'Poppins', fontSize: 15, fontWeight: FontWeight.w700, ), ), ], ), ), ); Widget _buildEmptyPrompt() => Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.touch_app_rounded, size: 16, color: _brown.withOpacity(0.5)), const SizedBox(width: 8), Flexible( child: Text( 'Ketuk slot bergaris putus untuk menambah foto', textAlign: TextAlign.center, style: TextStyle( fontFamily: 'Poppins', fontSize: 12, color: _brown.withOpacity(0.6), ), ), ), ], ); Widget _buildSheetOption({ required IconData icon, required String label, required String subtitle, required Color color, required VoidCallback onTap, }) => GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(vertical: 18), decoration: BoxDecoration( gradient: LinearGradient( colors: [color, color.withOpacity(0.8)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(16), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, color: Colors.white, size: 28), const SizedBox(height: 8), Text( label, style: const TextStyle( fontFamily: 'Poppins', color: Colors.white, fontWeight: FontWeight.w700, fontSize: 14, ), ), Text( subtitle, style: TextStyle( fontFamily: 'Poppins', color: Colors.white.withOpacity(0.8), fontSize: 11, ), ), ], ), ), ); } /// CustomPainter untuk border dashed pada slot tambah foto class _DashedBorderPainter extends CustomPainter { final Color color; final double radius; final double dashWidth; final double dashGap; const _DashedBorderPainter({ required this.color, this.radius = 12, this.dashWidth = 6, this.dashGap = 4, }); @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = color ..strokeWidth = 1.5 ..style = PaintingStyle.stroke; final path = Path() ..addRRect( RRect.fromRectAndRadius( Rect.fromLTWH(1, 1, size.width - 2, size.height - 2), Radius.circular(radius), ), ); final metrics = path.computeMetrics(); for (final metric in metrics) { double distance = 0; while (distance < metric.length) { final len = math.min(dashWidth, metric.length - distance); canvas.drawPath(metric.extractPath(distance, distance + len), paint); distance += dashWidth + dashGap; } } } @override bool shouldRepaint(_DashedBorderPainter old) => old.color != color || old.radius != radius; }