import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:harvest_guard_app/components/scan_history_card.dart'; import 'package:harvest_guard_app/dashboard/dashboard_controller.dart'; // Fungsi helper yang digunakan di seluruh widget double getSafeAnimationValue( AnimationController controller, double start, double end) { // Pastikan nilai controller dibatasi antara 0.0 dan 1.0 double safeValue = controller.value.clamp(0.0, 1.0); // Jika nilai lebih kecil dari start, kembalikan 0.0 if (safeValue < start) return 0.0; // Jika nilai lebih besar dari end, kembalikan 1.0 if (safeValue > end) return 1.0; // Normalisasi nilai ke rentang 0.0 - 1.0 berdasarkan start dan end return (safeValue - start) / (end - start); } class DashboardScreen extends StatefulWidget { const DashboardScreen({super.key}); @override State createState() => _DashboardScreenState(); } class _DashboardScreenState extends State { late DashboardController controller; @override void initState() { super.initState(); // Pastikan controller sudah diinisialisasi if (!Get.isRegistered()) { Get.put(DashboardController()); } controller = Get.find(); } @override Widget build(BuildContext context) { return Scaffold( body: Stack( children: [ // Animated Background _buildAnimatedBackground(), // Main Content SafeArea( child: Padding( padding: const EdgeInsets.all(20.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header dengan greeting dan avatar _buildHeader(), const SizedBox(height: 30), // Welcome Banner with Animation _buildWelcomeBanner(), const SizedBox(height: 30), // Main Action Card _buildMainActionCard(), const SizedBox(height: 25), // Riwayat kesehatan section _buildHistoryHeader(), const SizedBox(height: 16), // Riwayat scan list _buildHistoryList(), ], ), ), ), ], ), floatingActionButton: _buildFloatingActionButton(), ); } // Animated Background yang aman Widget _buildAnimatedBackground() { return Stack( children: [ // Base gradient background Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topRight, end: Alignment.bottomLeft, colors: [ Colors.white, Colors.green.shade50, Colors.green.shade100, ], ), ), ), // Reactive color based on scan history Obx(() { final historyCount = controller.modelController.scanHistoryList.length; final baseColor = historyCount > 0 ? Color.fromARGB( 255, 0, 120 + (historyCount * 10).clamp(0, 100), 0) : Colors.green.shade400; return Stack( children: [ // Top blob with animation Positioned( top: -100, right: -50, child: AnimatedBuilder( animation: controller.animationController, builder: (context, child) { // Menggunakan nilai yang aman untuk animasi final sinValue = math.sin( controller.animationController.value.clamp(0.0, 1.0) * math.pi * 2); return Transform.rotate( angle: sinValue * 0.05, child: Container( height: 250, width: 250, decoration: BoxDecoration( gradient: RadialGradient( colors: [ baseColor.withOpacity(0.3), baseColor.withOpacity(0.1), baseColor.withOpacity(0.0), ], stops: const [0.2, 0.6, 1.0], ), borderRadius: BorderRadius.circular(150), ), ), ); }, ), ), // Decorative floating elements (shortened for brevity) for (int i = 0; i < 3; i++) Positioned( top: 100 + (i * 150), left: (i % 2 == 0) ? 20 : null, right: (i % 2 == 0) ? null : 20, child: AnimatedBuilder( animation: controller.animationController, builder: (context, child) { final safeValue = controller.animationController.value.clamp(0.0, 1.0); final phase = i * 0.2; final animValue = (safeValue + phase) % 1.0; return Opacity( opacity: 0.2, child: Transform.translate( offset: Offset(math.sin(animValue * math.pi * 2) * 10, math.cos(animValue * math.pi * 2) * 10), child: Icon( Icons.eco, size: 20.0, color: baseColor.withOpacity(0.3), ), ), ); }, ), ), ], ); }), ], ); } // Header dengan animasi yang aman Widget _buildHeader() { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ // Greeting text dengan nama user Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ AnimatedBuilder( animation: controller.animationController, builder: (context, child) { // Gunakan getSafeAnimationValue untuk nilai yang aman final progress = getSafeAnimationValue( controller.animationController, 0.0, 0.3); final value = Curves.easeOut.transform(progress); return Opacity( opacity: value, child: Transform.translate( offset: Offset(-20 * (1 - value), 0), child: Obx(() => Text( 'Hallo, ${controller.userName.value}', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, foreground: Paint() ..shader = LinearGradient( colors: [ Colors.green.shade700, Colors.green.shade500, ], ).createShader( const Rect.fromLTWH(0.0, 0.0, 200.0, 70.0)), ), )), ), ); }, ), AnimatedBuilder( animation: controller.animationController, builder: (context, child) { // Gunakan getSafeAnimationValue untuk nilai yang aman final progress = getSafeAnimationValue( controller.animationController, 0.05, 0.35); final value = Curves.easeOut.transform(progress); return Opacity( opacity: value, child: Transform.translate( offset: Offset(-20 * (1 - value), 0), child: const Text( 'Semoga panen melimpah!', style: TextStyle( fontSize: 14, color: Colors.black54, ), ), ), ); }, ), ], ), // Animated Avatar AnimatedBuilder( animation: controller.animationController, builder: (context, child) { // Gunakan getSafeAnimationValue untuk nilai yang aman final progress = getSafeAnimationValue(controller.animationController, 0.0, 0.4); // Elastic animation dihitung dengan aman double elasticValue = 0.0; if (progress > 0.0) { try { elasticValue = Curves.elasticOut.transform(progress); } catch (_) { // Fallback jika masih ada error elasticValue = progress; } } return Transform.scale( scale: elasticValue.clamp(0.0, 1.0), child: GestureDetector( onTap: controller.switchUserProfile, child: Container( width: 60, height: 60, decoration: BoxDecoration( shape: BoxShape.circle, color: const Color(0xFFFFD966), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 10, spreadRadius: 2, offset: const Offset(0, 5), ), ], ), child: ClipOval( child: Center( child: Icon( Icons.person, size: 40, color: Colors.brown.shade800, ), ), ), ), ), ); }, ), ], ); } // Welcome Banner dengan animasi yang aman Widget _buildWelcomeBanner() { return AnimatedBuilder( animation: controller.animationController, builder: (context, child) { // Gunakan getSafeAnimationValue untuk nilai yang aman final progress = getSafeAnimationValue(controller.animationController, 0.1, 0.5); final value = Curves.easeOut.transform(progress); return Opacity( opacity: value, child: Transform.translate( offset: Offset(0, 30 * (1 - value)), child: Container( width: double.infinity, padding: const EdgeInsets.all(20), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ Colors.green.shade300, Colors.green.shade500, ], ), borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: Colors.green.withOpacity(0.3), blurRadius: 15, spreadRadius: 5, offset: const Offset(0, 8), ), ], ), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Periksa kesehatan\npadimu sekarang!', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white, height: 1.3, ), ), const SizedBox(height: 10), Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6), decoration: BoxDecoration( color: Colors.white.withOpacity(0.3), borderRadius: BorderRadius.circular(20), ), child: const Text( 'Deteksi Dini Penyakit', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: Colors.white, ), ), ), ], ), ), const SizedBox(width: 10), Container( width: 60, height: 60, decoration: const BoxDecoration( color: Colors.white24, shape: BoxShape.circle, ), child: const Icon( Icons.search, color: Colors.white, size: 30, ), ), ], ), ), ), ); }, ); } // Main Action Card dengan animasi yang aman Widget _buildMainActionCard() { return AnimatedBuilder( animation: controller.animationController, builder: (context, child) { // Gunakan getSafeAnimationValue untuk nilai yang aman final progress = getSafeAnimationValue(controller.animationController, 0.2, 0.6); final value = Curves.easeOut.transform(progress); return Opacity( opacity: value, child: Transform.translate( offset: Offset(0, 40 * (1 - value)), child: GestureDetector( onTap: controller.startScanning, child: Container( width: double.infinity, height: 180, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ Colors.white, Colors.green.shade50, ], ), borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, 5), ), ], border: Border.all( color: Colors.green.shade200, width: 1.5, ), ), child: Stack( children: [ // Background decoration Positioned( bottom: -20, right: -20, child: Icon( Icons.eco, size: 100, color: Colors.green.withOpacity(0.1), ), ), // Content Padding( padding: const EdgeInsets.all(20.0), child: Row( children: [ // Icon Container( width: 100, height: 100, decoration: BoxDecoration( color: Colors.green.shade100, shape: BoxShape.circle, ), child: Center( child: Icon( Icons.camera_alt_rounded, size: 50, color: Colors.green.shade700, ), ), ), const SizedBox(width: 20), // Text content Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ const Text( 'Scan Tanaman', style: TextStyle( fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87, ), ), const SizedBox(height: 10), Text( 'Arahkan kamera ke tanaman padi untuk deteksi penyakit', style: TextStyle( fontSize: 14, color: Colors.grey.shade700, ), ), const SizedBox(height: 15), Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 8), decoration: BoxDecoration( color: Colors.green.shade500, borderRadius: BorderRadius.circular(20), ), child: const Text( 'Mulai Scan', style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ], ), ), ], ), ), ], ), ), ), ), ); }, ); } // History header dengan animasi yang aman Widget _buildHistoryHeader() { return AnimatedBuilder( animation: controller.animationController, builder: (context, child) { // Gunakan getSafeAnimationValue untuk nilai yang aman final progress = getSafeAnimationValue(controller.animationController, 0.3, 0.7); final value = Curves.easeOut.transform(progress); return Opacity( opacity: value, child: Transform.translate( offset: Offset(0, 30 * (1 - value)), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 5, spreadRadius: 1, ), ], ), child: Row( children: [ Icon( Icons.history, size: 18, color: Colors.green.shade700, ), const SizedBox(width: 8), const Text( 'Riwayat Pemeriksaan', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black87, ), ), ], ), ), // Selengkapnya button InkWell( onTap: controller.navigateToScanHistoryDetail, borderRadius: BorderRadius.circular(20), child: Padding( padding: const EdgeInsets.all(8.0), child: Row( children: [ Text( 'Selengkapnya', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: Colors.green.shade700, ), ), const SizedBox(width: 2), Icon( Icons.arrow_forward_ios, size: 12, color: Colors.green.shade700, ), ], ), ), ), ], ), ), ); }, ); } // History list dengan animasi yang aman Widget _buildHistoryList() { return Expanded( child: AnimatedBuilder( animation: controller.animationController, builder: (context, _) { return Obx(() { final scanHistory = controller.modelController.scanHistoryList; if (scanHistory.isEmpty) { // Empty state dengan animasi aman final progress = getSafeAnimationValue( controller.animationController, 0.4, 0.8); final value = Curves.easeOut.transform(progress); return Opacity( opacity: value, child: Transform.translate( offset: Offset(0, 30 * (1 - value)), child: Container( margin: const EdgeInsets.only(top: 30), padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), border: Border.all( color: Colors.grey.shade200, width: 1, ), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.search_off_rounded, size: 60, color: Colors.grey.shade400, ), const SizedBox(height: 15), const Text( 'Belum ada riwayat pemeriksaan', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Colors.grey, ), ), const SizedBox(height: 5), Text( 'Mulai scan tanaman padi untuk melihat hasilnya disini', textAlign: TextAlign.center, style: TextStyle( fontSize: 14, color: Colors.grey.shade500, ), ), ], ), ), ), ); } // Tampilkan 3 history terbaru saja final recentHistory = scanHistory.length > 3 ? scanHistory.sublist(0, 3) : scanHistory; // Gunakan ListView dengan staggered effect aman return ListView.builder( itemCount: recentHistory.length, physics: const BouncingScrollPhysics(), itemBuilder: (context, index) { final item = recentHistory[index]; // Gunakan persen offset berbeda untuk setiap item secara bertahap final startValue = 0.45 + (index * 0.1); // 0.45, 0.55, 0.65 final endValue = startValue + 0.2; // 0.65, 0.75, 0.85 // Hitung animasi dengan nilai aman final progress = getSafeAnimationValue( controller.animationController, startValue, endValue); final opacity = Curves.easeOut.transform(progress); if (opacity <= 0.01) { return const SizedBox .shrink(); // Sembunyikan sampai animasi dimulai } return Opacity( opacity: opacity, child: Transform.translate( offset: Offset(0, 50 * (1 - opacity)), child: Padding( padding: const EdgeInsets.only(bottom: 15), child: _buildEnhancedHistoryCard(item), ), ), ); }, ); }); }, ), ); } // Enhanced history card Widget _buildEnhancedHistoryCard(dynamic item) { // Ini adalah versi yang lebih bagus dari ScanHistoryCard // Anda perlu menyesuaikan dengan properties yang ada di model Anda Color statusColor; IconData statusIcon; // Tentukan warna dan icon berdasarkan hasil diagnosis if (item.confidence > 80) { statusColor = Colors.red.shade400; statusIcon = Icons.warning_rounded; } else if (item.confidence > 50) { statusColor = Colors.orange.shade400; statusIcon = Icons.warning_amber_rounded; } else { statusColor = Colors.green.shade400; statusIcon = Icons.check_circle_rounded; } return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), offset: const Offset(0, 3), blurRadius: 10, ), ], ), child: Material( color: Colors.transparent, borderRadius: BorderRadius.circular(20), child: InkWell( borderRadius: BorderRadius.circular(20), onTap: () { // Navigasi ke detail item controller.openHistoryDetail(item); }, child: Padding( padding: const EdgeInsets.all(12), child: Row( children: [ // Status indicator Container( width: 50, height: 50, decoration: BoxDecoration( color: statusColor.withOpacity(0.1), shape: BoxShape.circle, ), child: Center( child: Icon( statusIcon, color: statusColor, size: 26, ), ), ), const SizedBox(width: 15), // Content Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item.diseaseResult, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black87, ), ), const SizedBox(height: 5), Row( children: [ Icon( Icons.calendar_today, size: 12, color: Colors.grey.shade500, ), const SizedBox(width: 4), Text( item.timestamp.toString().substring(0, 10), style: TextStyle( fontSize: 12, color: Colors.grey.shade600, ), ), const SizedBox(width: 10), Icon( Icons.bar_chart_rounded, size: 12, color: Colors.grey.shade500, ), const SizedBox(width: 4), Text( '${item.confidence.toStringAsFixed(1)}%', style: TextStyle( fontSize: 12, color: Colors.grey.shade600, ), ), ], ), ], ), ), // Arrow const Icon( Icons.arrow_forward_ios, size: 16, color: Colors.grey, ), ], ), ), ), ), ); } // Floating Action Button dengan animasi yang aman Widget _buildFloatingActionButton() { return AnimatedBuilder( animation: controller.animationController, builder: (context, child) { // Gunakan getSafeAnimationValue untuk nilai yang aman final progress = getSafeAnimationValue(controller.animationController, 0.6, 1.0); // Elastic animation dihitung dengan aman double elasticValue = 0.0; if (progress > 0) { try { elasticValue = Curves.elasticOut.transform(progress); } catch (_) { // Fallback jika masih ada error elasticValue = progress; } } // Batasi nilai antara 0.0 dan 1.0 elasticValue = elasticValue.clamp(0.0, 1.0); return Transform.scale( scale: elasticValue, child: FloatingActionButton.extended( onPressed: controller.startScanning, backgroundColor: Colors.green.shade600, icon: const Icon( Icons.camera_alt_rounded, color: Colors.white, ), label: const Text( 'Scan Baru', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, ), ), elevation: 5, ), ); }, ); } }