import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:image/image.dart' as img; import '../../config/app_config.dart'; import '../documents/pending_documents_provider.dart'; import 'blur_detector.dart'; class DottedVerticalLine extends StatelessWidget { final double height; final Color color; const DottedVerticalLine({super.key, required this.height, this.color = Colors.black45}); @override Widget build(BuildContext context) { return Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: List.generate((height / 4).floor(), (index) => Container( width: 1.5, height: 2, color: color, )), ); } } class BlurScoreBar extends StatelessWidget { final double score; final double threshold = 80.0; final double maxScore = 100.0; const BlurScoreBar({super.key, required this.score}); @override Widget build(BuildContext context) { final double displayScore = score; const double displayThreshold = 80.0; const double maxDisplayScore = 100.0; final double normalizedScore = displayScore / maxDisplayScore; final double thresholdRatio = displayThreshold / maxDisplayScore; final bool isSharp = displayScore >= displayThreshold; final Color barColor = isSharp ? AppConfig.successColor : AppConfig.errorColor; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('Ketajaman Foto', style: AppConfig.bodyMedium.copyWith(fontWeight: FontWeight.bold)), Text( '${displayScore.toStringAsFixed(1)} / ${maxDisplayScore.toInt()}', style: AppConfig.bodyMedium.copyWith(color: barColor, fontWeight: FontWeight.bold), ), ], ), const SizedBox(height: 8), LayoutBuilder( builder: (context, constraints) { final double totalWidth = constraints.maxWidth; final double thresholdPosition = totalWidth * thresholdRatio; return Stack( clipBehavior: Clip.none, children: [ // 1. Progress Bar Track Container( height: 20, width: double.infinity, decoration: BoxDecoration( color: Colors.grey.shade200, borderRadius: BorderRadius.circular(10), ), ), // 2. Active Filled Bar FractionallySizedBox( widthFactor: normalizedScore, child: Container( height: 20, decoration: BoxDecoration( color: barColor, borderRadius: BorderRadius.circular(10), ), ), ), // 3. Vertical Dotted Threshold Line Positioned( left: thresholdPosition, top: 0, bottom: 0, child: const DottedVerticalLine(height: 20, color: Colors.black45), ), // 4. Threshold Label Positioned( left: thresholdPosition - 40, top: 22, child: Text( 'Batas Min (${displayThreshold.toInt()})', style: AppConfig.bodySmall.copyWith( color: Colors.grey.shade600, fontWeight: FontWeight.bold, fontSize: 9, ), ), ), ], ); }, ), const SizedBox(height: 20), ], ); } } class ImagePreviewScreen extends ConsumerStatefulWidget { final String imagePath; final double? latitude; final double? longitude; const ImagePreviewScreen({ super.key, required this.imagePath, this.latitude, this.longitude, }); @override ConsumerState createState() => _ImagePreviewScreenState(); } class _ImagePreviewScreenState extends ConsumerState { bool _analyzing = true; BlurResult? _blurResult; late String _currentImagePath; @override void initState() { super.initState(); _currentImagePath = widget.imagePath; _detectBlur(); } Future _detectBlur() async { try { final result = await BlurDetector.checkBlur(_currentImagePath); if (mounted) { setState(() { _blurResult = result; _analyzing = false; }); } } catch (e) { if (mounted) { setState(() { _analyzing = false; }); } } } Future _rotateImage() async { showDialog( context: context, barrierDismissible: false, builder: (context) => const Center( child: CircularProgressIndicator(color: AppConfig.primaryColor), ), ); try { final bytes = await File(_currentImagePath).readAsBytes(); final decoded = img.decodeImage(bytes); if (decoded != null) { // Rotate 90 degrees clockwise final rotated = img.copyRotate(decoded, angle: 90); final encoded = img.encodeJpg(rotated); final dir = Directory(_currentImagePath).parent.path; final name = _currentImagePath.split('/').last.split('\\').last; final newPath = '$dir/rotated_${DateTime.now().millisecondsSinceEpoch}_$name'; await File(newPath).writeAsBytes(encoded); // Delete old rotated files to prevent memory leaks/leftovers if (_currentImagePath.contains('rotated_')) { try { await File(_currentImagePath).delete(); } catch (_) {} } if (mounted) { setState(() { _currentImagePath = newPath; _analyzing = true; }); Navigator.pop(context); // Close loading dialog _detectBlur(); // Re-run blur analysis } } else { if (mounted) Navigator.pop(context); } } catch (e) { if (mounted) Navigator.pop(context); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppConfig.backgroundColor, appBar: AppBar( title: const Text('Konfirmasi Foto'), actions: [ IconButton( icon: const Icon(Icons.rotate_right), onPressed: _rotateImage, tooltip: 'Putar 90°', ), ], ), body: SafeArea( child: Column( children: [ // Interactive photo view (with pinch-to-zoom support) Expanded( child: Stack( children: [ Container( margin: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, spreadRadius: 2, ) ], border: Border.all(color: Colors.grey.shade200), ), child: ClipRRect( borderRadius: BorderRadius.circular(16), child: InteractiveViewer( maxScale: 4.0, child: Center( child: Hero( tag: 'doc_image_preview', child: Image.file( File(_currentImagePath), fit: BoxFit.contain, ), ), ), ), ), ), Positioned( bottom: 24, right: 24, child: FloatingActionButton.small( heroTag: 'rotate_button', backgroundColor: Colors.white.withOpacity(0.9), foregroundColor: AppConfig.primaryColor, onPressed: _rotateImage, child: const Icon(Icons.rotate_right), ), ), ], ), ), // Bottom confirmation box Container( padding: const EdgeInsets.all(24.0), decoration: BoxDecoration( color: Colors.white, borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 15, offset: const Offset(0, -5), ) ], ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( 'Periksa Kualitas Foto', style: AppConfig.h2.copyWith(fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), const SizedBox(height: 16), // Blur status area if (_analyzing) Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: AppConfig.primaryColor), ), const SizedBox(width: 12), Text( 'Menganalisis ketajaman foto...', style: AppConfig.bodyMedium.copyWith(color: AppConfig.textSecondary), ), ], ) else if (_blurResult != null) ...[ BlurScoreBar(score: _blurResult!.score), _buildBlurBadge(_blurResult!) ] else Text( 'Pastikan seluruh area dokumen masuk ke dalam frame, tulisan dapat terbaca dengan jelas, dan hasil foto tidak blur.', style: AppConfig.bodyMedium.copyWith(color: AppConfig.textSecondary), textAlign: TextAlign.center, ), const SizedBox(height: 24), Row( children: [ // Retake Button Expanded( child: OutlinedButton( onPressed: () => Navigator.pop(context), style: OutlinedButton.styleFrom( foregroundColor: AppConfig.textPrimary, side: BorderSide(color: Colors.grey.shade400), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), padding: const EdgeInsets.symmetric(vertical: 14), ), child: const Text('Ambil Ulang', style: TextStyle(fontWeight: FontWeight.bold)), ), ), const SizedBox(width: 16), // Confirm & Sync Button Expanded( child: ElevatedButton( onPressed: (!_analyzing && _blurResult != null && !_blurResult!.isBlur) ? () { ref.read(pendingDocumentsProvider.notifier).addDocument( _currentImagePath, latitude: widget.latitude, longitude: widget.longitude, ); Navigator.pop(context); context.go('/documents'); } : null, style: ElevatedButton.styleFrom( backgroundColor: AppConfig.primaryColor, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), padding: const EdgeInsets.symmetric(vertical: 14), ), child: const Text('Unggah Dokumen', style: TextStyle(fontWeight: FontWeight.bold)), ), ), ], ), ], ), ), ], ), ), ); } Widget _buildBlurBadge(BlurResult result) { if (result.isBlur) { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppConfig.errorColor.withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: AppConfig.errorColor.withOpacity(0.3)), ), child: Row( children: [ const Icon(Icons.warning_amber_rounded, color: AppConfig.errorColor, size: 24), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Foto Terdeteksi Blur!', style: AppConfig.bodyMedium.copyWith(color: AppConfig.errorColor, fontWeight: FontWeight.bold), ), const SizedBox(height: 2), Text( 'Diharuskan untuk mengambil ulang gambar.', style: AppConfig.bodySmall.copyWith(color: AppConfig.textSecondary), ), ], ), ), ], ), ); } else { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppConfig.successColor.withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: AppConfig.successColor.withOpacity(0.3)), ), child: Row( children: [ const Icon(Icons.check_circle_outline, color: AppConfig.successColor, size: 24), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Kualitas Foto Baik', style: AppConfig.bodyMedium.copyWith(color: AppConfig.successColor, fontWeight: FontWeight.bold), ), const SizedBox(height: 2), Text( 'Fokus dan ketajaman gambar optimal.', style: AppConfig.bodySmall.copyWith(color: AppConfig.textSecondary), ), ], ), ), ], ), ); } } }