import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; 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; }); } } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppConfig.backgroundColor, appBar: AppBar( title: const Text('Konfirmasi Foto'), ), 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, ), ), ), ), ), ), ], ), ), // 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 ? () { 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), ), ], ), ), ], ), ); } } }