import 'dart:io'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import '../../widgets/color.dart'; import '../../services/classifier_service.dart'; import '../../models/prediction_result.dart'; class KlasifikasiScreen extends StatefulWidget { const KlasifikasiScreen({super.key}); @override State createState() => _KlasifikasiScreenState(); } class _KlasifikasiScreenState extends State { File? _image; bool _isProcessing = false; PredictionResult? _result; final ClassifierService _classifierService = ClassifierService(); @override void initState() { super.initState(); _initModel(); } Future _initModel() async { await _classifierService.loadModel(); } // ini hasil foto asli tanpa preprocessing Future _pickImage(ImageSource source) async { final picker = ImagePicker(); final pickedFile = await picker.pickImage( source: source, imageQuality: 100, ); if (pickedFile != null) { setState(() { _isProcessing = true; _result = null; }); try { File original = File(pickedFile.path); // 1. Preprocessing untuk UI (Zoom 80% & Square Crop) // Gambar ini yang akan disimpan di variabel _image untuk ditampilkan di widget File processed = await _classifierService.preprocessImageForDisplay( original, ); setState(() { _image = processed; }); // 2. Klasifikasi menggunakan file yang sudah di-crop/resize final result = await _classifierService.classify(processed); setState(() { _result = result; _isProcessing = false; }); } catch (e) { print("Error during image processing: $e"); setState(() => _isProcessing = false); } } } // ini kalau mau preprocessing gambar untuk ditampilkan di UI, biar sama dengan yang dianalisis model // Future _pickImage(ImageSource source) async { // final picker = ImagePicker(); // final pickedFile = await picker.pickImage( // source: source, // imageQuality: 100, // ); // if (pickedFile != null) { // File original = File(pickedFile.path); // /// preprocessing supaya gambar yang tampil = gambar yang dianalisis model // File processed = await _classifierService.preprocessImageForDisplay( // original, // ); // setState(() { // _image = processed; // _isProcessing = true; // _result = null; // }); // final result = await _classifierService.classify(processed); // setState(() { // _result = result; // _isProcessing = false; // }); // } // } // fungsi untuk menampilkan dialog instruksi sebelum membuka kamera // void _showCameraInstructions(BuildContext context) { // showDialog( // context: context, // builder: (BuildContext context) { // return Dialog( // shape: RoundedRectangleBorder( // borderRadius: BorderRadius.circular(25), // ), // child: Container( // padding: const EdgeInsets.all(20), // decoration: BoxDecoration( // color: Colors.white, // borderRadius: BorderRadius.circular(25), // ), // child: Column( // mainAxisSize: MainAxisSize.min, // children: [ // // Icon Header yang menarik // Container( // padding: const EdgeInsets.all(15), // decoration: BoxDecoration( // color: AppColors.heroCokelat.withOpacity(0.2), // shape: BoxShape.circle, // ), // child: const Icon( // Icons.center_focus_strong_rounded, // color: AppColors.brownMain, // size: 40, // ), // ), // const SizedBox(height: 20), // Text( // "Instruksi Pengambilan", // style: GoogleFonts.montserrat( // fontSize: 18, // fontWeight: FontWeight.bold, // color: AppColors.brownMain, // ), // ), // const SizedBox(height: 15), // // Point-point instruksi // _buildInstructionRow( // Icons.center_focus_weak, // "Pastikan biji kopi berada tepat di tengah bingkai.", // ), // const SizedBox(height: 10), // _buildInstructionRow( // Icons.lightbulb_outline, // "Gunakan pencahayaan yang cukup agar tekstur terlihat jelas.", // ), // const SizedBox(height: 10), // _buildInstructionRow( // Icons.stay_primary_portrait, // "Pegang ponsel dengan stabil agar foto tidak buram (blur).", // ), // const SizedBox(height: 25), // // Tombol Mengerti // SizedBox( // width: double.infinity, // child: ElevatedButton( // onPressed: () { // Navigator.pop(context); // Tutup dialog // _pickImage(ImageSource.camera); // Lanjut buka kamera // }, // style: ElevatedButton.styleFrom( // backgroundColor: AppColors.brownMain, // shape: RoundedRectangleBorder( // borderRadius: BorderRadius.circular(15), // ), // padding: const EdgeInsets.symmetric(vertical: 12), // ), // child: Text( // "Saya Mengerti", // style: GoogleFonts.montserrat( // color: Colors.white, // fontWeight: FontWeight.bold, // ), // ), // ), // ), // ], // ), // ), // ); // }, // ); // } // // Widget pendukung untuk baris teks instruksi // Widget _buildInstructionRow(IconData icon, String text) { // return Row( // children: [ // Icon(icon, size: 20, color: AppColors.greenMain), // const SizedBox(width: 12), // Expanded( // child: Text( // text, // style: GoogleFonts.montserrat( // fontSize: 12, // color: AppColors.textDark, // height: 1.4, // ), // ), // ), // ], // ); // } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.background, appBar: _buildAppBar(), body: SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.only( left: 16, right: 16, top: 16, bottom: 50, ), child: Column( children: [ _buildInputCard(), const SizedBox(height: 25), if (_isProcessing) const Center( child: CircularProgressIndicator(color: AppColors.brownMain), ), if (!_isProcessing && _result != null) _result!.isValid ? _buildResultSection() : _buildInvalidCard(), ], ), ), ), ); } PreferredSizeWidget _buildAppBar() { return AppBar( backgroundColor: AppColors.cardWhite, elevation: 0, title: Row( children: [ Image.asset( 'assets/images/Logo_Coffee_Scan.png', width: 30, errorBuilder: (c, e, s) => const Icon(Icons.coffee, color: AppColors.brownMain), ), const SizedBox(width: 10), RichText( text: TextSpan( style: const TextStyle( fontFamily: 'Montserrat', fontSize: 20, fontWeight: FontWeight.w900, ), children: const [ TextSpan( text: 'Coffee', style: TextStyle(color: AppColors.brownMain), ), TextSpan( text: 'Scan', style: TextStyle(color: AppColors.greenMain), ), ], ), ), ], ), ); } Widget _buildInputCard() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.heroCokelat, borderRadius: BorderRadius.circular(25), ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Stack( alignment: Alignment.center, children: [ Container( width: 135, height: 135, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), ), child: _image == null ? const Icon( Icons.image_search, size: 50, color: Colors.grey, ) : ClipRRect( borderRadius: BorderRadius.circular(20), child: Image.file(_image!, fit: BoxFit.cover), ), ), if (_image == null) Container( width: 100, height: 100, decoration: BoxDecoration( border: Border.all( color: Colors.white.withOpacity(0.5), width: 2, ), borderRadius: BorderRadius.circular(15), ), ), ], ), const SizedBox(width: 16), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Posisikan biji kopi di tengah bingkai kotak agar hasil lebih akurat", style: const TextStyle( fontFamily: 'Montserrat', fontSize: 8, fontWeight: FontWeight.w500, color: Colors.white, height: 1.2, ), ), const SizedBox(height: 10), _buildActionButton( Icons.camera_alt_rounded, "Buka Kamera", () => _pickImage(ImageSource.camera), ), // _buildActionButton( // Icons.camera_alt_rounded, // "Buka Kamera", // () => _showCameraInstructions( // context, // ), // ), const SizedBox(height: 8), _buildActionButton( Icons.photo_library_rounded, "Unggah File", () => _pickImage(ImageSource.gallery), ), ], ), ), ], ), ); } Widget _buildActionButton(IconData icon, String label, VoidCallback onTap) { return ElevatedButton.icon( onPressed: onTap, icon: Icon(icon, size: 18, color: Colors.black), label: Text( label, style: const TextStyle( fontFamily: 'Montserrat', fontSize: 12, fontWeight: FontWeight.w700, color: Colors.black, ), ), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFF5E6CA), elevation: 0, minimumSize: const Size(double.infinity, 38), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), ); } Widget _buildInvalidCard() { return Container( padding: const EdgeInsets.all(25), decoration: BoxDecoration( color: Colors.red[50], borderRadius: BorderRadius.circular(25), border: Border.all(color: Colors.red.shade200), ), child: Column( children: [ const Icon(Icons.block_flipped, color: Colors.red, size: 60), const SizedBox(height: 15), Text( "Objek Tidak Dikenali", style: TextStyle( fontFamily: 'Montserrat', fontWeight: FontWeight.w700, fontSize: 18, color: Colors.red[900], ), ), const SizedBox(height: 10), Text( "Pastikan objek berada di tengah kotak dan memiliki pencahayaan yang cukup.", textAlign: TextAlign.center, style: TextStyle( fontFamily: 'Montserrat', fontSize: 13, color: Colors.red[700], ), ), ], ), ); } Widget _buildResultSection() { return Column( children: [ const Icon(Icons.check_circle, color: AppColors.greenSuccess, size: 70), const SizedBox(height: 8), Text( "Hasil Klasifikasi", style: const TextStyle( fontFamily: 'Montserrat', fontSize: 20, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 25), Row( children: [ _buildDetailBox( "Jenis Biji Kopi", _result!.jenisKopi, Icons.coffee_rounded, ), const SizedBox(width: 12), _buildDetailBox( "Tingkat Roasting", _result!.tingkatRoasting, Icons.local_fire_department_rounded, ), ], ), const SizedBox(height: 20), _buildAccuracyMainCard(), const SizedBox(height: 20), _buildTop3AnalisisCard(), ], ); } Widget _buildAccuracyMainCard() { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColors.greenLight, borderRadius: BorderRadius.circular(20), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Akurasi Prediksi", style: const TextStyle( fontFamily: 'Montserrat', fontWeight: FontWeight.w700, color: AppColors.greenSuccess, ), ), Text( "${_result!.akurasi.toStringAsFixed(2)}%", style: const TextStyle( fontFamily: 'Montserrat', fontSize: 32, fontWeight: FontWeight.w700, color: AppColors.greenSuccess, ), ), ], ), const Icon( Icons.analytics_rounded, color: AppColors.greenSuccess, size: 50, ), ], ), ); } Widget _buildTop3AnalisisCard() { List> filtered = _result!.top3Predictions .where((pred) => (pred['score'] * 100) >= 0.1) .toList(); double s1 = filtered.isNotEmpty ? filtered[0]['score'] : 0.0; double s2 = filtered.length > 1 ? filtered[1]['score'] : 0.0; double s3 = filtered.length > 2 ? filtered[2]['score'] : 0.0; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(25), boxShadow: [ BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 15), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Analisis Kemungkinan", style: const TextStyle( fontFamily: 'Montserrat', fontWeight: FontWeight.w700, fontSize: 14, ), ), const SizedBox(height: 25), Row( children: [ SizedBox( width: 120, height: 120, child: Stack( alignment: Alignment.center, children: [ SizedBox( width: 120, height: 120, child: CircularProgressIndicator( value: 1.0, strokeWidth: 12, color: Colors.grey[100], ), ), if (filtered.length > 2) SizedBox( width: 120, height: 120, child: CircularProgressIndicator( value: s1 + s2 + s3, strokeWidth: 12, strokeCap: StrokeCap.round, color: AppColors.rank3, ), ), if (filtered.length > 1) SizedBox( width: 120, height: 120, child: CircularProgressIndicator( value: s1 + s2, strokeWidth: 12, strokeCap: StrokeCap.round, color: AppColors.rank2, ), ), SizedBox( width: 120, height: 120, child: CircularProgressIndicator( value: s1, strokeWidth: 12, strokeCap: StrokeCap.round, color: AppColors.rank1, ), ), Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( "${(s1 * 100).toStringAsFixed(2)}%", style: const TextStyle( fontFamily: 'Montserrat', fontWeight: FontWeight.w700, fontSize: 18, color: AppColors.rank1, ), ), ], ), ], ), ), const SizedBox(width: 15), Expanded( child: Column( children: List.generate(filtered.length, (index) { Color c = index == 0 ? AppColors.rank1 : (index == 1 ? AppColors.rank2 : AppColors.rank3); return _buildTop3Item(filtered[index], c, index == 0); }), ), ), ], ), ], ), ); } Widget _buildTop3Item(Map pred, Color color, bool isTop) { return Padding( padding: const EdgeInsets.symmetric(vertical: 5), child: Row( children: [ Container( width: 8, height: 8, decoration: BoxDecoration(color: color, shape: BoxShape.circle), ), const SizedBox(width: 8), Expanded( child: Text( pred['label'], maxLines: 1, style: TextStyle( fontFamily: 'Montserrat', fontSize: 11, color: isTop ? AppColors.textDark : AppColors.textGrey, fontWeight: isTop ? FontWeight.w700 : FontWeight.w500, ), ), ), const SizedBox(width: 4), Text( "${(pred['score'] * 100).toStringAsFixed(2)}%", style: TextStyle( fontFamily: 'Montserrat', fontSize: 11, fontWeight: FontWeight.w700, color: color, ), ), ], ), ); } Widget _buildDetailBox(String t, String v, IconData i) { return Expanded( child: Container( padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 10), decoration: BoxDecoration( color: AppColors.buttonCream.withOpacity(0.4), borderRadius: BorderRadius.circular(20), ), child: Column( children: [ Icon(i, color: AppColors.brownMain, size: 28), const SizedBox(height: 8), Text( t, style: const TextStyle( fontFamily: 'Montserrat', fontSize: 10, color: AppColors.textGrey, fontWeight: FontWeight.w600, ), ), Text( v, textAlign: TextAlign.center, style: const TextStyle( fontFamily: 'Montserrat', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.brownMain, ), ), ], ), ), ); } }