1768 lines
62 KiB
Dart
1768 lines
62 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'dart:math';
|
|
import 'dart:typed_data';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:image_picker/image_picker.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
|
import 'package:tugas_akhir_supabase/core/theme/app_colors.dart';
|
|
import 'package:tugas_akhir_supabase/screens/community/models/message.dart';
|
|
import 'package:tugas_akhir_supabase/screens/community/services/message_service.dart';
|
|
import 'package:tugas_akhir_supabase/services/gemini_disease_service.dart';
|
|
import 'package:tugas_akhir_supabase/data/models/diagnosis_result_model.dart';
|
|
import 'package:tugas_akhir_supabase/utils/pdf_generator.dart';
|
|
import 'package:image/image.dart' as img;
|
|
|
|
// API key for Gemini (in a real app, should be stored securely)
|
|
const String GEMINI_API_KEY = 'AIzaSyDEH7z1IoTZ3J10EProCSTSetTMDqbBXn4';
|
|
|
|
// App colors
|
|
class PlantScannerColors {
|
|
static final primary = Color(0xFF0A8754);
|
|
static final secondary = Color(0xFF39B686);
|
|
static final accent = Color(0xFF2C7873);
|
|
static final background = Color(0xFFF5F9F6);
|
|
static final cardBackground = Colors.white;
|
|
static final error = Color(0xFFD83A3A);
|
|
static final warning = Color(0xFFFF9800);
|
|
static final success = Color(0xFF4CAF50);
|
|
static final lightGreen = Color(0xFFE8F5E9);
|
|
static final darkText = Color(0xFF2C3333);
|
|
static final lightText = Color(0xFF6B7280);
|
|
static final disabledText = Color(0xFFAEB0B6);
|
|
static final divider = Color(0xFFEAECF0);
|
|
}
|
|
|
|
// Application states
|
|
enum ScanState { empty, loading, result, error, notPlant }
|
|
|
|
class PlantScannerScreen extends StatefulWidget {
|
|
const PlantScannerScreen({super.key});
|
|
|
|
@override
|
|
State<PlantScannerScreen> createState() => _PlantScannerScreenState();
|
|
}
|
|
|
|
class _PlantScannerScreenState extends State<PlantScannerScreen>
|
|
with SingleTickerProviderStateMixin {
|
|
final _picker = ImagePicker();
|
|
File? _image;
|
|
Uint8List? _webImage;
|
|
ScanState _scanState = ScanState.empty;
|
|
String _errorMessage = '';
|
|
late AnimationController _animationController;
|
|
bool _isAnalyzing = false;
|
|
bool _isLoading = false;
|
|
|
|
// Gemini API service
|
|
late GeminiDiseaseDiagnosisService _geminiService;
|
|
DiagnosisResultModel? _diagnosisResult;
|
|
|
|
final MessageService _messageService = MessageService();
|
|
final List<Message> _messages = [];
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_animationController = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(milliseconds: 1500),
|
|
)..repeat();
|
|
|
|
// Initialize Gemini API service with error handling
|
|
try {
|
|
// Check if Supabase is initialized
|
|
_geminiService = GeminiDiseaseDiagnosisService(
|
|
apiKey: GEMINI_API_KEY,
|
|
supabaseClient: Supabase.instance.client,
|
|
);
|
|
|
|
// Log API key for debugging (remove in production)
|
|
debugPrint('Using Gemini API key: ${GEMINI_API_KEY.substring(0, 5)}...');
|
|
debugPrint('Gemini service initialized successfully');
|
|
} catch (e) {
|
|
debugPrint('Error initializing Gemini service: $e');
|
|
_errorMessage = 'Gagal menginisialisasi layanan. Silakan coba lagi.';
|
|
_scanState = ScanState.error;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_animationController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final screenWidth = MediaQuery.of(context).size.width;
|
|
final isSmallScreen = screenWidth < 360;
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
backgroundColor: AppColors.primary,
|
|
foregroundColor: Colors.white,
|
|
title: Text(
|
|
'Analisis Tanaman',
|
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
|
|
),
|
|
elevation: 0,
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.help_outline, size: 22),
|
|
onPressed: _showHelpDialog,
|
|
),
|
|
],
|
|
),
|
|
body: Container(
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [
|
|
AppColors.primary.withOpacity(0.9),
|
|
AppColors.scaffoldBackground,
|
|
],
|
|
stops: const [0.0, 0.2],
|
|
),
|
|
),
|
|
child: SafeArea(child: _getContentForState()),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Content based on state
|
|
Widget _getContentForState() {
|
|
switch (_scanState) {
|
|
case ScanState.empty:
|
|
return _buildEmptyState();
|
|
case ScanState.loading:
|
|
return _buildLoadingState();
|
|
case ScanState.result:
|
|
return _buildResultView();
|
|
case ScanState.error:
|
|
return _buildErrorState();
|
|
case ScanState.notPlant:
|
|
return _buildNotPlantState();
|
|
}
|
|
}
|
|
|
|
// UI for empty state
|
|
Widget _buildEmptyState() {
|
|
return Center(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
// Logo or illustration
|
|
Container(
|
|
width: 70,
|
|
height: 70,
|
|
decoration: BoxDecoration(
|
|
color: PlantScannerColors.lightGreen,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(
|
|
Icons.eco_rounded,
|
|
size: 44,
|
|
color: PlantScannerColors.primary,
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
// Middle container with border
|
|
Container(
|
|
width: double.infinity,
|
|
margin: const EdgeInsets.only(bottom: 20),
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(15),
|
|
border: Border.all(
|
|
color: PlantScannerColors.primary.withOpacity(0.3),
|
|
width: 1.5,
|
|
),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.eco_outlined,
|
|
color: PlantScannerColors.primary,
|
|
size: 22,
|
|
),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: Text(
|
|
'Ambil foto yang jelas dari bagian tanaman yang terlihat bermasalah',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w600,
|
|
fontSize: 14,
|
|
color: PlantScannerColors.darkText,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.wb_sunny_outlined,
|
|
color: PlantScannerColors.warning,
|
|
size: 22,
|
|
),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: Text(
|
|
'Pastikan pencahayaan cukup dan tidak berbayang',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w600,
|
|
fontSize: 14,
|
|
color: PlantScannerColors.darkText,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.zoom_in,
|
|
color: PlantScannerColors.accent,
|
|
size: 22,
|
|
),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: Text(
|
|
'Dekatkan kamera agar detail gejala terlihat jelas',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w600,
|
|
fontSize: 14,
|
|
color: PlantScannerColors.darkText,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Image upload options with improved UI
|
|
Container(
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 15,
|
|
offset: const Offset(0, 5),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Text(
|
|
'Unggah Foto Tanaman',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: PlantScannerColors.darkText,
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: _buildUploadOption(
|
|
icon: Icons.camera_alt_rounded,
|
|
label: 'Kamera',
|
|
description: 'Ambil foto baru',
|
|
onTap: () => _pickImage(ImageSource.camera),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: _buildUploadOption(
|
|
icon: Icons.photo_library_rounded,
|
|
label: 'Galeri',
|
|
description: 'Pilih dari galeri',
|
|
onTap: _pickImageFromGallery,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// UI for loading state
|
|
Widget _buildLoadingState() {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
width: 80,
|
|
height: 80,
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 15,
|
|
offset: const Offset(0, 5),
|
|
),
|
|
],
|
|
),
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 3,
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
PlantScannerColors.primary,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 32),
|
|
Text(
|
|
_isAnalyzing ? 'Menganalisis Tanaman' : 'Menghubungkan ke API',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: PlantScannerColors.darkText,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
_isAnalyzing
|
|
? 'Mencari pola penyakit dan kondisi kesehatan...'
|
|
: 'Memproses gambar tanaman...',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(fontSize: 15, color: PlantScannerColors.lightText),
|
|
),
|
|
const SizedBox(height: 40),
|
|
// Loading animation for indication of progress
|
|
SizedBox(
|
|
width: 240,
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: LinearProgressIndicator(
|
|
backgroundColor: PlantScannerColors.lightGreen,
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
PlantScannerColors.secondary,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// Result view - Shows diagnosis results
|
|
Widget _buildResultView() {
|
|
if (_diagnosisResult == null) {
|
|
return Center(child: Text('Tidak ada hasil diagnosis'));
|
|
}
|
|
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Image preview
|
|
if (_image != null)
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: Image.file(
|
|
_image!,
|
|
width: double.infinity,
|
|
height: 200,
|
|
fit: BoxFit.cover,
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Tanaman Terdeteksi section
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Tanaman Terdeteksi',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey[700],
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
_diagnosisResult!.plantSpecies,
|
|
style: TextStyle(
|
|
fontSize: 23,
|
|
fontWeight: FontWeight.bold,
|
|
color: PlantScannerColors.darkText,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Row(
|
|
children: [
|
|
Icon(Icons.eco, color: AppColors.primary, size: 20),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Fase: ${_diagnosisResult!.plantData['growthStage'] ?? 'Tidak dapat ditentukan dari gambar.'}',
|
|
style: TextStyle(fontSize: 11, color: Colors.black),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 4,
|
|
vertical: 2,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color:
|
|
_diagnosisResult!.isHealthy
|
|
? PlantScannerColors.success.withOpacity(0.2)
|
|
: PlantScannerColors.error.withOpacity(0.2),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Text(
|
|
_diagnosisResult!.isHealthy
|
|
? 'healthy_plant'
|
|
: 'unhealthy_plant',
|
|
style: TextStyle(
|
|
color:
|
|
_diagnosisResult!.isHealthy
|
|
? PlantScannerColors.success
|
|
: PlantScannerColors.error,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Tingkat Keparahan & Dampak
|
|
if (!_diagnosisResult!.isHealthy)
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Tingkat Keparahan & Dampak',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey[700],
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Keparahan',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
LinearProgressIndicator(
|
|
value: _diagnosisResult!.confidenceValue,
|
|
backgroundColor: Colors.red.withOpacity(0.2),
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
Colors.red,
|
|
),
|
|
minHeight: 10,
|
|
borderRadius: BorderRadius.circular(5),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'${(_diagnosisResult!.confidenceValue * 100).toInt()}%',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.red,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Area Terinfeksi',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
LinearProgressIndicator(
|
|
value: () {
|
|
final infectedAreaRaw =
|
|
_diagnosisResult!.plantData['infectedArea'];
|
|
if (infectedAreaRaw is num) {
|
|
return infectedAreaRaw.toDouble() / 100;
|
|
} else if (infectedAreaRaw is String) {
|
|
final infectedArea = num.tryParse(
|
|
infectedAreaRaw,
|
|
);
|
|
if (infectedArea != null) {
|
|
return infectedArea.toDouble() / 100;
|
|
}
|
|
}
|
|
return 0.0;
|
|
}(),
|
|
backgroundColor: Colors.orange.withOpacity(0.2),
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
Colors.orange,
|
|
),
|
|
minHeight: 10,
|
|
borderRadius: BorderRadius.circular(5),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'${(() {
|
|
final infectedAreaRaw = _diagnosisResult!.plantData['infectedArea'];
|
|
if (infectedAreaRaw is num) {
|
|
return infectedAreaRaw.toInt();
|
|
} else if (infectedAreaRaw is String) {
|
|
final infectedArea = num.tryParse(infectedAreaRaw);
|
|
if (infectedArea != null) {
|
|
return infectedArea.toInt();
|
|
}
|
|
}
|
|
return 0;
|
|
})()}%',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.orange,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Potensi Kerugian: ${_diagnosisResult!.economicImpact['estimatedLoss'] ?? 'Tidak dapat ditentukan dari gambar. Kerugian bergantung pada luas area yang terinfeksi.'}',
|
|
style: TextStyle(fontSize: 14, color: Colors.red[700]),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Informasi Penyakit
|
|
if (!_diagnosisResult!.isHealthy)
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(Icons.bug_report, color: Colors.red[700], size: 22),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Informasi Penyakit',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey[700],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.red[50],
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
_diagnosisResult!.diseaseName,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.red[700],
|
|
),
|
|
),
|
|
if (_diagnosisResult!.scientificName.isNotEmpty)
|
|
Text(
|
|
'*${_diagnosisResult!.scientificName}*',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontStyle: FontStyle.italic,
|
|
color: Colors.red[700],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
// Gejala
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.warning_amber_rounded,
|
|
color: Colors.amber,
|
|
size: 20,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Gejala',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: PlantScannerColors.darkText,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[100],
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
_diagnosisResult!.symptoms,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
height: 1.5,
|
|
color: PlantScannerColors.darkText,
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
// Penyebab
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.science_outlined,
|
|
color: Colors.blue,
|
|
size: 20,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Penyebab',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: PlantScannerColors.darkText,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[100],
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
_diagnosisResult!.causes,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
height: 1.5,
|
|
color: PlantScannerColors.darkText,
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
// Bagian yang Terpengaruh
|
|
Row(
|
|
children: [
|
|
Icon(Icons.eco_outlined, color: Colors.green, size: 20),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Bagian yang Terpengaruh',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: PlantScannerColors.darkText,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[100],
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
_diagnosisResult!.additionalInfo.affectedParts.isNotEmpty
|
|
? _diagnosisResult!.additionalInfo.affectedParts.join(
|
|
', ',
|
|
)
|
|
: 'Daun',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
height: 1.5,
|
|
color: PlantScannerColors.darkText,
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
// Kondisi Lingkungan
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.wb_sunny_outlined,
|
|
color: Colors.orange,
|
|
size: 20,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Kondisi Lingkungan',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: PlantScannerColors.darkText,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[100],
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
_diagnosisResult!.additionalInfo.environmentalConditions,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
height: 1.5,
|
|
color: PlantScannerColors.darkText,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Pengobatan & Pencegahan
|
|
if (!_diagnosisResult!.isHealthy)
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.medical_services_outlined,
|
|
color: Colors.green,
|
|
size: 22,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Pengobatan & Pencegahan',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey[700],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Pengobatan Organik
|
|
Row(
|
|
children: [
|
|
Icon(Icons.eco_outlined, color: Colors.green, size: 20),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Pengobatan Organik',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: PlantScannerColors.darkText,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[100],
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
_diagnosisResult!.organicTreatment,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
height: 1.5,
|
|
color: PlantScannerColors.darkText,
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
// Pengobatan Kimia
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.science_outlined,
|
|
color: Colors.blue,
|
|
size: 20,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Pengobatan Kimia',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: PlantScannerColors.darkText,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[100],
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
_diagnosisResult!.chemicalTreatment,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
height: 1.5,
|
|
color: PlantScannerColors.darkText,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Langkah Pencegahan
|
|
if (!_diagnosisResult!.isHealthy)
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.shield_outlined,
|
|
color: Colors.green[700],
|
|
size: 22,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Langkah Pencegahan',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey[700],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
..._diagnosisResult!.preventionMeasures.map(
|
|
(measure) => Padding(
|
|
padding: const EdgeInsets.only(bottom: 12),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Icon(
|
|
Icons.check_circle_outline,
|
|
color: Colors.green,
|
|
size: 20,
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Text(
|
|
measure,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
height: 1.5,
|
|
color: PlantScannerColors.darkText,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Jadwal Perawatan
|
|
if (!_diagnosisResult!.isHealthy &&
|
|
_diagnosisResult!.treatmentSchedule.isNotEmpty)
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Jadwal Perawatan',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey[700],
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Penyiraman
|
|
if (_diagnosisResult!.treatmentSchedule['wateringSchedule'] !=
|
|
null)
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.water_drop_outlined,
|
|
color: Colors.blue,
|
|
size: 20,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Penyiraman',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: PlantScannerColors.darkText,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
_diagnosisResult!
|
|
.treatmentSchedule['wateringSchedule']
|
|
.toString(),
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
],
|
|
),
|
|
|
|
// Pemupukan
|
|
if (_diagnosisResult!
|
|
.treatmentSchedule['fertilizingSchedule'] !=
|
|
null)
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.eco_outlined,
|
|
color: Colors.green,
|
|
size: 20,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Pemupukan',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: PlantScannerColors.darkText,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
_diagnosisResult!
|
|
.treatmentSchedule['fertilizingSchedule']
|
|
.toString(),
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
],
|
|
),
|
|
|
|
// Pestisida
|
|
if (_diagnosisResult!
|
|
.treatmentSchedule['pesticideSchedule'] !=
|
|
null)
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.pest_control_outlined,
|
|
color: Colors.orange,
|
|
size: 20,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Pestisida',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: PlantScannerColors.darkText,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
_diagnosisResult!
|
|
.treatmentSchedule['pesticideSchedule']
|
|
.toString(),
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Varietas Alternatif
|
|
if (!_diagnosisResult!.isHealthy &&
|
|
_diagnosisResult!.alternativeVarieties.isNotEmpty)
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Varietas Alternatif',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey[700],
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
Row(
|
|
children: [
|
|
Icon(Icons.eco_outlined, color: Colors.green, size: 20),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Varietas padi tahan penyakit bercak daun bakteri',
|
|
style: TextStyle(
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w500,
|
|
color: PlantScannerColors.darkText,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Cari informasi varietas padi yang direkomendasikan untuk daerah Anda yang memiliki tingkat ketahanan terhadap penyakit bercak daun bakteri.',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey[600],
|
|
height: 1.5,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
// Action buttons
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: OutlinedButton.icon(
|
|
onPressed: () => setState(() => _scanState = ScanState.empty),
|
|
style: OutlinedButton.styleFrom(
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
icon: const Icon(Icons.refresh),
|
|
label: const Text('Analisis Ulang'),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: ElevatedButton.icon(
|
|
onPressed: _generatePdfReport,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: PlantScannerColors.primary,
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
icon: const Icon(Icons.download_rounded),
|
|
label: const Text('Simpan Laporan'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// Generate PDF report
|
|
Future<void> _generatePdfReport() async {
|
|
if (_diagnosisResult == null || _image == null) return;
|
|
|
|
try {
|
|
setState(() => _isLoading = true);
|
|
|
|
// Convert image to bytes
|
|
final imageBytes = await _image!.readAsBytes();
|
|
|
|
// Decode image (apapun formatnya) lalu encode ke PNG
|
|
final decoded = img.decodeImage(imageBytes);
|
|
Uint8List safeBytes;
|
|
if (decoded != null) {
|
|
safeBytes = Uint8List.fromList(img.encodePng(decoded));
|
|
} else {
|
|
safeBytes = imageBytes; // fallback, meski kemungkinan error
|
|
}
|
|
|
|
final pdfGenerator = HarvestPdfGenerator();
|
|
final pdfFile = await pdfGenerator.generateDiagnosisReportPdf(
|
|
diagnosisResult: _diagnosisResult!,
|
|
imageBytes: safeBytes,
|
|
);
|
|
|
|
setState(() => _isLoading = false);
|
|
|
|
if (!mounted) return;
|
|
showDialog(
|
|
context: context,
|
|
builder:
|
|
(context) => AlertDialog(
|
|
title: const Text('PDF Berhasil Dibuat'),
|
|
content: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Laporan PDF diagnosis tanaman telah berhasil dibuat.',
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Lokasi: \\n${pdfFile.path}',
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
fontStyle: FontStyle.italic,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: const Text('Tutup'),
|
|
),
|
|
TextButton(
|
|
onPressed: () async {
|
|
Navigator.pop(context);
|
|
await pdfGenerator.sharePdf(pdfFile);
|
|
},
|
|
child: const Text('Bagikan'),
|
|
),
|
|
TextButton(
|
|
onPressed: () async {
|
|
Navigator.pop(context);
|
|
await pdfGenerator.openPdf(pdfFile);
|
|
},
|
|
child: const Text('Buka'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
} catch (e) {
|
|
setState(() => _isLoading = false);
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Gagal membuat laporan: \\n${e.toString()}'),
|
|
backgroundColor: PlantScannerColors.error,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// UI for error state
|
|
Widget _buildErrorState() {
|
|
return Center(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(24),
|
|
decoration: BoxDecoration(
|
|
color: PlantScannerColors.error.withOpacity(0.1),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(
|
|
Icons.error_outline_rounded,
|
|
size: 56,
|
|
color: PlantScannerColors.error,
|
|
),
|
|
),
|
|
const SizedBox(height: 32),
|
|
Text(
|
|
'Terjadi Kesalahan',
|
|
style: TextStyle(
|
|
fontSize: 22,
|
|
fontWeight: FontWeight.bold,
|
|
color: PlantScannerColors.darkText,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(
|
|
color: PlantScannerColors.error.withOpacity(0.3),
|
|
width: 1,
|
|
),
|
|
),
|
|
child: Text(
|
|
_errorMessage.isNotEmpty
|
|
? _errorMessage
|
|
: 'Terjadi kesalahan saat menganalisis gambar. Silakan coba lagi.',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
color: PlantScannerColors.darkText,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 32),
|
|
ElevatedButton.icon(
|
|
onPressed: () => setState(() => _scanState = ScanState.empty),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: PlantScannerColors.primary,
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 24,
|
|
vertical: 12,
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
elevation: 0,
|
|
),
|
|
icon: const Icon(Icons.refresh),
|
|
label: Text('Coba Lagi'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// UI for not plant state
|
|
Widget _buildNotPlantState() {
|
|
return Center(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(24),
|
|
decoration: BoxDecoration(
|
|
color: PlantScannerColors.warning.withOpacity(0.1),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(
|
|
Icons.warning_amber_rounded,
|
|
size: 56,
|
|
color: PlantScannerColors.warning,
|
|
),
|
|
),
|
|
const SizedBox(height: 32),
|
|
Text(
|
|
'Bukan Tanaman Terdeteksi',
|
|
style: TextStyle(
|
|
fontSize: 22,
|
|
fontWeight: FontWeight.bold,
|
|
color: PlantScannerColors.darkText,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(
|
|
color: PlantScannerColors.warning.withOpacity(0.3),
|
|
width: 1,
|
|
),
|
|
),
|
|
child: Text(
|
|
'Kami tidak dapat mendeteksi tanaman dalam gambar ini. Pastikan gambar yang Anda kirim menampilkan tanaman dengan jelas.',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
color: PlantScannerColors.darkText,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 32),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: OutlinedButton.icon(
|
|
onPressed:
|
|
() => setState(() => _scanState = ScanState.empty),
|
|
style: OutlinedButton.styleFrom(
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
side: BorderSide(color: PlantScannerColors.accent),
|
|
),
|
|
icon: const Icon(Icons.arrow_back),
|
|
label: Text('Kembali'),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: ElevatedButton.icon(
|
|
onPressed: _pickImageFromGallery,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: PlantScannerColors.primary,
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
icon: const Icon(Icons.photo_library),
|
|
label: Text('Unggah Ulang'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Help dialog
|
|
void _showHelpDialog() {
|
|
showDialog(
|
|
context: context,
|
|
builder:
|
|
(context) => AlertDialog(
|
|
title: Text(
|
|
'Bantuan Analisis Tanaman',
|
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
|
),
|
|
content: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
ListTile(
|
|
contentPadding: EdgeInsets.zero,
|
|
leading: Icon(
|
|
Icons.check,
|
|
color: AppColors.primary,
|
|
size: 18,
|
|
),
|
|
minLeadingWidth: 24,
|
|
title: Text(
|
|
'Ambil foto yang jelas dari bagian tanaman yang terlihat bermasalah',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: PlantScannerColors.darkText,
|
|
),
|
|
),
|
|
),
|
|
ListTile(
|
|
contentPadding: EdgeInsets.zero,
|
|
leading: Icon(
|
|
Icons.check,
|
|
color: AppColors.primary,
|
|
size: 18,
|
|
),
|
|
minLeadingWidth: 24,
|
|
title: Text(
|
|
'Fokuskan pada bagian yang terinfeksi (daun, batang, buah)',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: PlantScannerColors.darkText,
|
|
),
|
|
),
|
|
),
|
|
ListTile(
|
|
contentPadding: EdgeInsets.zero,
|
|
leading: Icon(
|
|
Icons.check,
|
|
color: AppColors.primary,
|
|
size: 18,
|
|
),
|
|
minLeadingWidth: 24,
|
|
title: Text(
|
|
'Pastikan pencahayaan cukup dan foto tidak buram',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: PlantScannerColors.darkText,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: Text(
|
|
'Mengerti',
|
|
style: TextStyle(color: AppColors.primary),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// Upload option item with improved UI
|
|
Widget _buildUploadOption({
|
|
required IconData icon,
|
|
required String label,
|
|
required String description,
|
|
required VoidCallback onTap,
|
|
}) {
|
|
return InkWell(
|
|
onTap: onTap,
|
|
borderRadius: BorderRadius.circular(16),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: PlantScannerColors.background,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: PlantScannerColors.divider, width: 1),
|
|
),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
shape: BoxShape.circle,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: PlantScannerColors.primary.withOpacity(0.2),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Icon(icon, color: PlantScannerColors.primary, size: 24),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 16,
|
|
color: PlantScannerColors.darkText,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
description,
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: PlantScannerColors.lightText,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Camera image picking
|
|
Future<void> _pickImage(ImageSource source) async {
|
|
try {
|
|
final pickedFile = await _picker.pickImage(source: source);
|
|
if (pickedFile == null) return;
|
|
|
|
setState(() {
|
|
_scanState = ScanState.loading;
|
|
_errorMessage = '';
|
|
});
|
|
|
|
final imageFile = File(pickedFile.path);
|
|
setState(() {
|
|
_image = imageFile;
|
|
_webImage = null;
|
|
});
|
|
|
|
// Analyze the plant image
|
|
setState(() {
|
|
_isAnalyzing = true;
|
|
});
|
|
|
|
try {
|
|
// Attempt to analyze the image using Gemini service
|
|
final result = await _geminiService.diagnosePlant(_image!.path);
|
|
|
|
setState(() {
|
|
_diagnosisResult = result;
|
|
_scanState = ScanState.result;
|
|
_isAnalyzing = false;
|
|
});
|
|
} catch (e) {
|
|
debugPrint('Error analyzing image: $e');
|
|
setState(() {
|
|
_isAnalyzing = false;
|
|
_scanState = ScanState.error;
|
|
_errorMessage =
|
|
'Terjadi kesalahan saat menganalisis gambar: ${e.toString()}';
|
|
});
|
|
}
|
|
} catch (e) {
|
|
setState(() {
|
|
_scanState = ScanState.error;
|
|
_errorMessage = e.toString();
|
|
});
|
|
}
|
|
}
|
|
|
|
// Gallery picking
|
|
Future<void> _pickImageFromGallery() async {
|
|
try {
|
|
final pickedFile = await _picker.pickImage(source: ImageSource.gallery);
|
|
if (pickedFile == null) return;
|
|
|
|
setState(() {
|
|
_scanState = ScanState.loading;
|
|
_errorMessage = '';
|
|
});
|
|
|
|
final imageFile = File(pickedFile.path);
|
|
setState(() {
|
|
_image = imageFile;
|
|
_webImage = null;
|
|
});
|
|
|
|
// Analyze the plant image
|
|
setState(() {
|
|
_isAnalyzing = true;
|
|
});
|
|
|
|
try {
|
|
// Attempt to analyze the image using Gemini service
|
|
final result = await _geminiService.diagnosePlant(_image!.path);
|
|
|
|
setState(() {
|
|
_diagnosisResult = result;
|
|
_scanState = ScanState.result;
|
|
_isAnalyzing = false;
|
|
});
|
|
} catch (e) {
|
|
debugPrint('Error analyzing image: $e');
|
|
setState(() {
|
|
_isAnalyzing = false;
|
|
_scanState = ScanState.error;
|
|
_errorMessage =
|
|
'Terjadi kesalahan saat menganalisis gambar: ${e.toString()}';
|
|
});
|
|
}
|
|
} catch (e) {
|
|
setState(() {
|
|
_scanState = ScanState.error;
|
|
_errorMessage = e.toString();
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _deleteMessageForEveryone(Message message) async {
|
|
setState(() {
|
|
_messages.removeWhere((m) => m.id == message.id);
|
|
});
|
|
try {
|
|
await _messageService.deleteMessage(message);
|
|
// ...snackbar...
|
|
} catch (e) {
|
|
// ...error handling...
|
|
}
|
|
}
|
|
}
|