TIF_NGANJUK_E41212177/lib/dashboard/dashboard_page.dart

909 lines
31 KiB
Dart

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<DashboardScreen> createState() => _DashboardScreenState();
}
class _DashboardScreenState extends State<DashboardScreen> {
late DashboardController controller;
@override
void initState() {
super.initState();
// Pastikan controller sudah diinisialisasi
if (!Get.isRegistered<DashboardController>()) {
Get.put(DashboardController());
}
controller = Get.find<DashboardController>();
}
@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,
),
);
},
);
}
}