From 77fc37ee524b5c40d62fa860a14c0a986c0b20f7 Mon Sep 17 00:00:00 2001 From: Greek-Cp Date: Sun, 23 Mar 2025 00:50:37 +0700 Subject: [PATCH] feat: change ui --- ios/Flutter/Debug.xcconfig | 1 + ios/Flutter/Release.xcconfig | 1 + ios/Podfile | 44 + lib/components/abstract_class.dart | 1065 ++++++++++++++--- lib/components/splashscreen.dart | 223 +++- lib/dashboard/dashboard_controller.dart | 95 +- lib/dashboard/dashboard_page.dart | 990 +++++++++++++-- lib/intro/page_intro.dart | 282 +++++ lib/main.dart | 7 +- lib/periksa/periksa_page.dart | 857 +++++++++++-- lib/routes/app_routes.dart | 6 + macos/Flutter/Flutter-Debug.xcconfig | 1 + macos/Flutter/Flutter-Release.xcconfig | 1 + macos/Podfile | 43 + macos/Podfile.lock | 35 + macos/Runner.xcodeproj/project.pbxproj | 98 +- .../contents.xcworkspacedata | 3 + pubspec.lock | 4 +- pubspec.yaml | 2 +- 19 files changed, 3279 insertions(+), 479 deletions(-) create mode 100644 ios/Podfile create mode 100644 lib/intro/page_intro.dart create mode 100644 macos/Podfile create mode 100644 macos/Podfile.lock diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..d97f17e --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/lib/components/abstract_class.dart b/lib/components/abstract_class.dart index c04409a..b30d949 100644 --- a/lib/components/abstract_class.dart +++ b/lib/components/abstract_class.dart @@ -1,9 +1,10 @@ import 'dart:io'; +import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -// Base class untuk semua halaman penyakit -abstract class BaseDiseaseDetailPage extends StatelessWidget { +// Base class untuk semua halaman penyakit dengan UI yang lebih estetik +abstract class BaseDiseaseDetailPage extends StatefulWidget { final String title; final String description; final List treatments; @@ -16,6 +17,55 @@ abstract class BaseDiseaseDetailPage extends StatelessWidget { required this.preventions, }); + @override + State createState() => _BaseDiseaseDetailPageState(); +} + +class _BaseDiseaseDetailPageState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late ScrollController _scrollController; + bool _showTitle = false; + + @override + void initState() { + super.initState(); + + // Setup animasi + _animationController = AnimationController( + duration: const Duration(milliseconds: 1200), + vsync: this, + ); + + // Setup scroll controller untuk efek app bar + _scrollController = ScrollController(); + _scrollController.addListener(_onScroll); + + // Mulai animasi + _animationController.forward(); + } + + void _onScroll() { + // Menunjukkan judul di app bar ketika scroll melewati batas tertentu + if (_scrollController.offset > 200 && !_showTitle) { + setState(() { + _showTitle = true; + }); + } else if (_scrollController.offset <= 200 && _showTitle) { + setState(() { + _showTitle = false; + }); + } + } + + @override + void dispose() { + _animationController.dispose(); + _scrollController.removeListener(_onScroll); + _scrollController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { // Dapatkan argumen dari navigasi @@ -23,195 +73,373 @@ abstract class BaseDiseaseDetailPage extends StatelessWidget { final prediction = args['prediction'] as Map; final imagePath = args['imagePath'] as String?; final confidence = (prediction['confidence'] as double) * 100; - + return Scaffold( - appBar: AppBar( - title: Text(title), - ), - body: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Tampilkan gambar jika tersedia dengan design yang lebih baik - if (imagePath != null) - Container( - width: double.infinity, - height: 250, - decoration: BoxDecoration( - border: Border(bottom: BorderSide(color: Colors.grey.shade300, width: 1)), - ), - child: Stack( - children: [ - Center( - child: Image.file( - File(imagePath), - fit: BoxFit.cover, - width: double.infinity, - height: 250, + body: Stack( + children: [ + // Content dengan scroll + CustomScrollView( + controller: _scrollController, + slivers: [ + // App bar dengan image header + _buildSliverAppBar(imagePath, confidence, prediction), + + // Content main + SliverToBoxAdapter( + child: _buildMainContent(prediction, confidence), + ), + ], + ), + + // Gradient overlay di bagian atas untuk app bar efek + Positioned( + top: 0, + left: 0, + right: 0, + height: MediaQuery.of(context).padding.top + + 56, // Safe area + app bar height + child: AnimatedOpacity( + opacity: _showTitle ? 1.0 : 0.0, + duration: const Duration(milliseconds: 200), + child: ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: Container( + color: Colors.white.withOpacity(0.8), + padding: EdgeInsets.only( + top: MediaQuery.of(context).padding.top, + ), + child: Center( + child: Text( + _showTitle ? prediction['disease'] as String : '', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), ), ), - Positioned( - right: 10, - bottom: 10, - child: Container( - padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.7), - borderRadius: BorderRadius.circular(20), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.photo_camera, color: Colors.white, size: 16), - SizedBox(width: 4), - Text( - 'Hasil Scan', - style: TextStyle( - color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - ], - ), + ), + ), + ), + ), + ), + + // Back button + Positioned( + top: MediaQuery.of(context).padding.top + 8, + left: 16, + child: _buildBackButton(), + ), + + // Share button + Positioned( + top: MediaQuery.of(context).padding.top + 8, + right: 16, + child: _buildShareButton(), + ), + ], + ), + floatingActionButton: AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + final scale = Tween(begin: 0.0, end: 1.0) + .animate(CurvedAnimation( + parent: _animationController, + curve: const Interval(0.7, 1.0, curve: Curves.elasticOut), + )) + .value; + + return Transform.scale( + scale: scale, + child: child, + ); + }, + child: FloatingActionButton.extended( + onPressed: () { + // Kembali ke halaman scan + Get.until((route) => route.settings.name == '/'); + }, + icon: const Icon(Icons.camera_alt_rounded), + label: const Text('Scan Baru'), + backgroundColor: Colors.green.shade600, + elevation: 4, + ), + ), + ); + } + + // Custom SliverAppBar dengan gambar + Widget _buildSliverAppBar( + String? imagePath, double confidence, Map prediction) { + return SliverAppBar( + expandedHeight: 280, + floating: false, + pinned: true, + automaticallyImplyLeading: false, + backgroundColor: Colors.transparent, + elevation: 0, + flexibleSpace: FlexibleSpaceBar( + background: Stack( + fit: StackFit.expand, + children: [ + // Image background + if (imagePath != null) + Hero( + tag: 'disease_image', + child: Image.file( + File(imagePath), + fit: BoxFit.cover, + ), + ) + else + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.green.shade300, + Colors.green.shade600, + ], + ), + ), + child: Center( + child: Icon( + Icons.grass_rounded, + size: 80, + color: Colors.white.withOpacity(0.5), + ), + ), + ), + + // Gradient overlay + Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.black26, + Colors.black45, + ], + ), + ), + ), + + // Status dan indikator + Positioned( + bottom: 20, + left: 20, + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: _getConfidenceColor(confidence).withOpacity(0.9), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 8, + offset: const Offset(0, 3), + ), + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + _getConfidenceIcon(confidence), + color: Colors.white, + size: 18, + ), + const SizedBox(width: 6), + Text( + '${confidence.toStringAsFixed(1)}% Keyakinan', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 14, ), ), ], ), ), - - Padding( - padding: const EdgeInsets.all(16.0), + ), + + // Classification label + Positioned( + bottom: 20, + right: 20, + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.7), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 8, + offset: const Offset(0, 3), + ), + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.health_and_safety_rounded, + color: Colors.white, + size: 18, + ), + const SizedBox(width: 6), + Text( + 'Diagnosis', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + // Widget untuk bagian konten utama + Widget _buildMainContent(Map prediction, double confidence) { + return Container( + padding: const EdgeInsets.only(bottom: 100), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(30), + topRight: Radius.circular(30), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header dengan judul penyakit + Padding( + padding: const EdgeInsets.fromLTRB(24, 24, 24, 0), + child: AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + final slideAnimation = Tween( + begin: const Offset(0, 0.5), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: _animationController, + curve: const Interval(0.0, 0.6, curve: Curves.easeOut), + )); + + final fadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _animationController, + curve: const Interval(0.0, 0.6, curve: Curves.easeOut), + )); + + return FadeTransition( + opacity: fadeAnimation, + child: SlideTransition( + position: slideAnimation, + child: child, + ), + ); + }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Header dengan nama penyakit dan tingkat keyakinan + Text( + prediction['disease'] as String, + style: const TextStyle( + fontSize: 26, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 8), Row( children: [ - Expanded( - child: Text( - prediction['disease'] as String, - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold - ), - ), - ), Container( - padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5), + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 4), decoration: BoxDecoration( - color: confidence > 80 ? Colors.green : Colors.orange, + color: Colors.green.shade50, borderRadius: BorderRadius.circular(20), - ), - child: Text( - '${confidence.toStringAsFixed(1)}%', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, + border: Border.all( + color: Colors.green.shade200, + width: 1, ), ), - ), - ], - ), - - SizedBox(height: 16), - - // Deskripsi penyakit - Text( - 'Deskripsi', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold - ), - ), - SizedBox(height: 8), - Text(description), - - SizedBox(height: 24), - - // Cara penanganan - if (treatments.isNotEmpty) ...[ - Text( - 'Penanganan', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold - ), - ), - SizedBox(height: 8), - ...treatments.map((treatment) => Padding( - padding: EdgeInsets.only(bottom: 8), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon(Icons.check_circle, color: Colors.green), - SizedBox(width: 8), - Expanded(child: Text(treatment)), - ], - ), - )).toList(), - SizedBox(height: 24), - ], - - // Cara pencegahan - Text( - 'Pencegahan', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold - ), - ), - SizedBox(height: 8), - ...preventions.map((prevention) => Padding( - padding: EdgeInsets.only(bottom: 8), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon(Icons.shield, color: Colors.blue), - SizedBox(width: 8), - Expanded(child: Text(prevention)), - ], - ), - )).toList(), - - SizedBox(height: 24), - - // Tombol untuk membagikan hasil atau mencari informasi lebih lanjut - Row( - children: [ - Expanded( - child: ElevatedButton.icon( - icon: Icon(Icons.share), - label: Text('Bagikan Hasil'), - style: ElevatedButton.styleFrom( - padding: EdgeInsets.symmetric(vertical: 12), - ), - onPressed: () { - // TODO: Implementasi fungsi berbagi - Get.snackbar( - 'Bagikan', - 'Fitur berbagi akan segera tersedia', - snackPosition: SnackPosition.BOTTOM, - ); - }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.agriculture_rounded, + size: 14, + color: Colors.green.shade700, + ), + const SizedBox(width: 4), + Text( + 'Tanaman Padi', + style: TextStyle( + fontSize: 12, + color: Colors.green.shade700, + fontWeight: FontWeight.w500, + ), + ), + ], ), ), - SizedBox(width: 12), - Expanded( - child: OutlinedButton.icon( - icon: Icon(Icons.info_outline), - label: Text('Info Lanjut'), - style: OutlinedButton.styleFrom( - padding: EdgeInsets.symmetric(vertical: 12), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: + _getConfidenceColor(confidence).withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: _getConfidenceColor(confidence) + .withOpacity(0.3), + width: 1, ), - onPressed: () { - // TODO: Implementasi fungsi info lanjut - Get.snackbar( - 'Info Lanjut', - 'Fitur informasi lanjutan akan segera tersedia', - snackPosition: SnackPosition.BOTTOM, - ); - }, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + _getSeverityIcon(confidence), + size: 14, + color: _getConfidenceColor(confidence), + ), + const SizedBox(width: 4), + Text( + _getSeverityText(confidence), + style: TextStyle( + fontSize: 12, + color: _getConfidenceColor(confidence), + fontWeight: FontWeight.w500, + ), + ), + ], ), ), ], @@ -219,17 +447,492 @@ abstract class BaseDiseaseDetailPage extends StatelessWidget { ], ), ), - ], - ), - ), - floatingActionButton: FloatingActionButton( - child: Icon(Icons.camera_alt), - onPressed: () { - // Kembali ke halaman scan - Get.until((route) => route.settings.name == '/'); - }, - tooltip: 'Scan Baru', + ), + + // Divider custom + Padding( + padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 24), + child: Container( + height: 1, + color: Colors.grey.shade200, + ), + ), + + // Description section + _buildSection( + title: 'Tentang Penyakit', + icon: Icons.info_outline_rounded, + content: widget.description, + delay: 0.1, + ), + + // Treatment section + if (widget.treatments.isNotEmpty) + _buildListSection( + title: 'Cara Penanganan', + icon: Icons.healing_rounded, + items: widget.treatments, + itemIcon: Icons.check_circle_rounded, + itemColor: Colors.green.shade600, + delay: 0.2, + ), + + // Prevention section + _buildListSection( + title: 'Cara Pencegahan', + icon: Icons.shield_rounded, + items: widget.preventions, + itemIcon: Icons.shield_moon_rounded, + itemColor: Colors.blue.shade600, + delay: 0.3, + ), + + // Action buttons + Padding( + padding: const EdgeInsets.all(24.0), + child: AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + final slideAnimation = Tween( + begin: const Offset(0, 0.5), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Interval(0.4, 0.8, curve: Curves.easeOut), + )); + + final fadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Interval(0.4, 0.8, curve: Curves.easeOut), + )); + + return FadeTransition( + opacity: fadeAnimation, + child: SlideTransition( + position: slideAnimation, + child: child, + ), + ); + }, + child: Row( + children: [ + Expanded( + child: ElevatedButton.icon( + icon: const Icon(Icons.share_rounded), + label: const Text('Bagikan Hasil'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green.shade600, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 15), + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + textStyle: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + ), + ), + onPressed: () { + // TODO: Implementasi fungsi berbagi + Get.snackbar( + 'Bagikan', + 'Fitur berbagi akan segera tersedia', + snackPosition: SnackPosition.BOTTOM, + ); + }, + ), + ), + const SizedBox(width: 16), + Expanded( + child: OutlinedButton.icon( + icon: const Icon(Icons.info_outline_rounded), + label: const Text('Info Lanjut'), + style: OutlinedButton.styleFrom( + foregroundColor: Colors.green.shade700, + side: BorderSide( + color: Colors.green.shade400, width: 1.5), + padding: const EdgeInsets.symmetric(vertical: 15), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + textStyle: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + ), + ), + onPressed: () { + // TODO: Implementasi fungsi info lanjut + Get.snackbar( + 'Info Lanjut', + 'Fitur informasi lanjutan akan segera tersedia', + snackPosition: SnackPosition.BOTTOM, + ); + }, + ), + ), + ], + ), + ), + ), + ], ), ); } -} \ No newline at end of file + + // Widget untuk section dengan text biasa + Widget _buildSection({ + required String title, + required IconData icon, + required String content, + required double delay, + }) { + return Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 24), + child: AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + final slideAnimation = Tween( + begin: const Offset(0, 0.5), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Interval(0.1 + delay, 0.6 + delay, curve: Curves.easeOut), + )); + + final fadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Interval(0.1 + delay, 0.6 + delay, curve: Curves.easeOut), + )); + + return FadeTransition( + opacity: fadeAnimation, + child: SlideTransition( + position: slideAnimation, + child: child, + ), + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.green.shade50, + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + icon, + color: Colors.green.shade700, + size: 20, + ), + ), + const SizedBox(width: 12), + Text( + title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + ], + ), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Colors.grey.shade200, + width: 1, + ), + ), + child: Text( + content, + style: const TextStyle( + fontSize: 15, + color: Colors.black87, + height: 1.5, + ), + ), + ), + ], + ), + ), + ); + } + + // Widget untuk section dengan list items + Widget _buildListSection({ + required String title, + required IconData icon, + required List items, + required IconData itemIcon, + required Color itemColor, + required double delay, + }) { + return Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 24), + child: AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + final slideAnimation = Tween( + begin: const Offset(0, 0.5), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Interval(0.1 + delay, 0.6 + delay, curve: Curves.easeOut), + )); + + final fadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Interval(0.1 + delay, 0.6 + delay, curve: Curves.easeOut), + )); + + return FadeTransition( + opacity: fadeAnimation, + child: SlideTransition( + position: slideAnimation, + child: child, + ), + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.green.shade50, + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + icon, + color: Colors.green.shade700, + size: 20, + ), + ), + const SizedBox(width: 12), + Text( + title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + ], + ), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Colors.grey.shade200, + width: 1, + ), + ), + child: Column( + children: [ + for (int i = 0; i < items.length; i++) + Padding( + padding: EdgeInsets.only( + bottom: i < items.length - 1 ? 12 : 0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + itemIcon, + color: itemColor, + size: 20, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + items[i], + style: const TextStyle( + fontSize: 15, + color: Colors.black87, + height: 1.5, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + // Widget untuk tombol kembali + Widget _buildBackButton() { + return AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + final scale = Tween( + begin: 0.0, + end: 1.0, + ) + .animate(CurvedAnimation( + parent: _animationController, + curve: const Interval(0.0, 0.4, curve: Curves.elasticOut), + )) + .value; + + return Transform.scale( + scale: scale, + child: child, + ); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.8), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 8, + offset: const Offset(0, 3), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => Get.back(), + borderRadius: BorderRadius.circular(20), + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Icon( + Icons.arrow_back_rounded, + color: Colors.black87, + size: 24, + ), + ), + ), + ), + ), + ); + } + + // Widget untuk tombol share + Widget _buildShareButton() { + return AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + final scale = Tween( + begin: 0.0, + end: 1.0, + ) + .animate(CurvedAnimation( + parent: _animationController, + curve: const Interval(0.1, 0.5, curve: Curves.elasticOut), + )) + .value; + + return Transform.scale( + scale: scale, + child: child, + ); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.8), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 8, + offset: const Offset(0, 3), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + Get.snackbar( + 'Bagikan', + 'Fitur berbagi akan segera tersedia', + snackPosition: SnackPosition.BOTTOM, + ); + }, + borderRadius: BorderRadius.circular(20), + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Icon( + Icons.share_rounded, + color: Colors.black87, + size: 24, + ), + ), + ), + ), + ), + ); + } + + // Helper functions untuk warna dan ikon yang sesuai dengan tingkat keyakinan + Color _getConfidenceColor(double confidence) { + if (confidence > 80) { + return Colors.red; + } else if (confidence > 60) { + return Colors.orange; + } else { + return Colors.green; + } + } + + IconData _getConfidenceIcon(double confidence) { + if (confidence > 80) { + return Icons.priority_high_rounded; + } else if (confidence > 60) { + return Icons.warning_amber_rounded; + } else { + return Icons.check_circle_rounded; + } + } + + IconData _getSeverityIcon(double confidence) { + if (confidence > 80) { + return Icons.dangerous_rounded; + } else if (confidence > 60) { + return Icons.warning_amber_rounded; + } else { + return Icons.check_circle_outline_rounded; + } + } + + String _getSeverityText(double confidence) { + if (confidence > 80) { + return 'Parah'; + } else if (confidence > 60) { + return 'Sedang'; + } else { + return 'Ringan'; + } + } +} diff --git a/lib/components/splashscreen.dart b/lib/components/splashscreen.dart index 71ef8bd..5de1fd6 100644 --- a/lib/components/splashscreen.dart +++ b/lib/components/splashscreen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:harvest_guard_app/routes/app_routes.dart'; +import 'dart:math' as math; class SplashScreen extends StatefulWidget { const SplashScreen({super.key}); @@ -9,81 +10,193 @@ class SplashScreen extends StatefulWidget { State createState() => _SplashScreenState(); } -class _SplashScreenState extends State { +class _SplashScreenState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _rotationAnimation; + late Animation _scaleAnimation; + @override void initState() { super.initState(); - // Tunggu selama 3 detik kemudian navigasi ke Dashboard dengan AppRoutes + + // Animation controller + _controller = AnimationController( + duration: const Duration(seconds: 3), + vsync: this, + ); + + // Rotation animation + _rotationAnimation = Tween( + begin: 0, + end: 2 * math.pi, + ).animate(CurvedAnimation( + parent: _controller, + curve: Curves.easeInOut, + )); + + // Scale animation + _scaleAnimation = Tween( + begin: 0.8, + end: 1.0, + ).animate(CurvedAnimation( + parent: _controller, + curve: Curves.elasticOut, + )); + + // Start animation + _controller.forward(); + + // Navigate to intro screen after 3 seconds Future.delayed(const Duration(seconds: 3), () { - Get.offNamed( - AppRoutes.dashboard); // Menggunakan konstanta route dari AppRoutes + Get.offNamed(AppRoutes.intro); // Change to intro route }); } + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: Colors.white, - body: SafeArea( - child: Column( - children: [ - // Spacer untuk menjaga tampilan di tengah - const Spacer(flex: 2), - - // Logo tanaman menggunakan Image.asset - Center( + body: AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.green.shade100, + Colors.green.shade300, + Colors.green.shade500, + ], + stops: [0.0, 0.5 + _controller.value * 0.2, 1.0], + ), + ), + child: SafeArea( child: Column( children: [ - Image.asset( - 'assets/images/plant.png', - height: 60, - width: 60, + const Spacer(flex: 2), + + // Logo dengan animasi + Center( + child: Column( + children: [ + ScaleTransition( + scale: _scaleAnimation, + child: RotationTransition( + turns: _rotationAnimation, + child: Container( + height: 80, + width: 80, + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(40), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + spreadRadius: 2, + ), + ], + ), + child: const Icon( + Icons.eco_rounded, + size: 50, + color: Colors.green, + ), + ), + ), + ), + + const SizedBox(height: 25), + + // Teks Harvest - Guard dengan animasi + AnimatedOpacity( + opacity: _controller.value, + duration: const Duration(milliseconds: 800), + child: const Text( + 'Harvest - Guard', + style: TextStyle( + fontSize: 26, + fontWeight: FontWeight.bold, + color: Colors.white, + letterSpacing: 1.2, + shadows: [ + Shadow( + color: Colors.black26, + offset: Offset(1, 1), + blurRadius: 3, + ), + ], + ), + ), + ), + + const SizedBox(height: 10), + + // Sub text + FadeTransition( + opacity: _controller, + child: const Text( + 'Proteksi Tanaman Padi Anda', + style: TextStyle( + fontSize: 16, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), ), - const SizedBox(height: 20), + const Spacer(flex: 3), - // Teks Harvest - Guard - const Text( - 'Harvest - Guard', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.black, + // Teks "Developed by" dengan animasi + FadeTransition( + opacity: _controller, + child: const Padding( + padding: EdgeInsets.only(bottom: 8.0), + child: Text( + 'Developed by', + style: TextStyle( + fontSize: 14, + color: Colors.white70, + ), + ), + ), + ), + + // Nama developer dengan animasi + FadeTransition( + opacity: _controller, + child: const Padding( + padding: EdgeInsets.only(bottom: 32.0), + child: Text( + 'Bahrudin Ayub', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + letterSpacing: 0.5, + ), + ), ), ), ], ), ), - - // Spacer untuk mendorong "Developed by" ke bagian bawah - const Spacer(flex: 3), - - // Teks "Developed by" - const Padding( - padding: EdgeInsets.only(bottom: 8.0), - child: Text( - 'Developed by', - style: TextStyle( - fontSize: 14, - color: Colors.black, - ), - ), - ), - - // Nama developer - const Padding( - padding: EdgeInsets.only(bottom: 32.0), - child: Text( - 'Bahrudin Ayub', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - ), - ), - ], - ), + ); + }, ), ); } diff --git a/lib/dashboard/dashboard_controller.dart b/lib/dashboard/dashboard_controller.dart index 1540b16..8b8cd44 100644 --- a/lib/dashboard/dashboard_controller.dart +++ b/lib/dashboard/dashboard_controller.dart @@ -1,34 +1,109 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:harvest_guard_app/periksa/model_controller.dart'; import 'package:harvest_guard_app/routes/app_routes.dart'; - -class DashboardController extends GetxController { +class DashboardController extends GetxController + with GetSingleTickerProviderStateMixin { // User name state var userName = "Petani".obs; - + // Reference to ModelController for scan history late final ModelController modelController; - + + // Animation controller + late AnimationController animationController; + + // Animation status + var animationCompleted = false.obs; + @override void onInit() { super.onInit(); - + // Get ModelController instance if (!Get.isRegistered()) { Get.put(ModelController()); } modelController = Get.find(); + + // Initialize animation controller - hanya jalankan sekali + animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 2000), // Durasi total animasi + ); + + // Tambahkan listener untuk memastikan nilai dalam batas + animationController.addListener(() { + // Pastikan nilai animasi selalu dalam rentang 0.0 - 1.0 + if (animationController.value < 0.0 || animationController.value > 1.0) { + animationController.value = animationController.value.clamp(0.0, 1.0); + } + }); + + // Mulai animasi sekali ketika halaman dimuat + // Pastikan animasi hanya jalan sekali dan tidak repeat + animationController.forward().then((_) { + animationCompleted.value = true; + }); + } + + @override + void onClose() { + animationController.dispose(); + super.onClose(); } // Fungsi untuk memulai pemindaian dan navigasi ke PeriksaScreen void startScanning() { - // Navigasi ke PeriksaScreen menggunakan AppRoutes - Get.toNamed(AppRoutes.periksa); + // Haptic feedback + HapticFeedback.mediumImpact(); + + // Animasi transisi custom + Get.toNamed( + AppRoutes.periksa, + ); } - + // Navigate to scan history detail screen void navigateToScanHistoryDetail() { - Get.toNamed(AppRoutes.scanHistory); + // Haptic feedback + HapticFeedback.lightImpact(); + + Get.toNamed( + AppRoutes.scanHistory, + ); } -} \ No newline at end of file + + // Open history item detail + void openHistoryDetail(dynamic historyItem) { + // Haptic feedback + HapticFeedback.lightImpact(); + + // Implement detail view navigation + Get.toNamed( + '${AppRoutes.scanHistory}/${historyItem.id}', + arguments: historyItem, + ); + } + + // Switch user profile + void switchUserProfile() { + // Haptic feedback + HapticFeedback.lightImpact(); + + // Placeholder untuk fitur profil multiple + final profiles = ["Petani", "Ahmad", "Budi", "Citra"]; + final currentIndex = profiles.indexOf(userName.value); + final nextIndex = (currentIndex + 1) % profiles.length; + + userName.value = profiles[nextIndex]; + } + + // Reset animasi ketika halaman muncul kembali + void resetAnimation() { + animationController.reset(); + animationController.forward(); + } +} diff --git a/lib/dashboard/dashboard_page.dart b/lib/dashboard/dashboard_page.dart index dcf15a8..3eb5570 100644 --- a/lib/dashboard/dashboard_page.dart +++ b/lib/dashboard/dashboard_page.dart @@ -1,158 +1,908 @@ -import 'dart:io'; +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'; -import 'package:intl/intl.dart'; -class DashboardScreen extends GetView { +// 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( - backgroundColor: Colors.white, - body: SafeArea( - child: Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header dengan greeting dan avatar - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - // Greeting text dengan nama user - Obx(() => Text( - 'Hallo, ${controller.userName.value}', - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - ), - )), + body: Stack( + children: [ + // Animated Background + _buildAnimatedBackground(), - // Avatar image + // 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, - color: Color(0xFFFFD966), ), - child: ClipOval( - child: Image.asset( - 'assets/images/avatar.png', - fit: BoxFit.cover, - ), + child: const Icon( + Icons.search, + color: Colors.white, + size: 30, ), ), ], ), + ), + ), + ); + }, + ); + } - const SizedBox(height: 50), + // 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); - // Periksa kesehatan text - const Text( - 'Periksa kesehatan padimu sekarang!', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w500, - ), - ), - - const SizedBox(height: 16), - - // Periksa kesehatan card - hanya navigasi ke PeriksaScreen - GestureDetector( - onTap: controller.startScanning, - child: Image.asset( - 'assets/images/main_button.png', - fit: BoxFit.contain, - ), - ), - - const SizedBox(height: 20), - - // Riwayat kesehatan section - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'Riwayat kesehatan', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w500, - ), + 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, + ], ), - - // Selengkapnya text button - GestureDetector( - onTap: controller.navigateToScanHistoryDetail, - child: const Text( - 'Selengkapnya', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Colors.blue, + 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, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), ), + ), + ), + ); + }, + ); + } - const SizedBox(height: 16), + // 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); - // Riwayat scan list - Expanded( - child: Obx(() { - final scanHistory = controller.modelController.scanHistoryList; - - if (scanHistory.isEmpty) { - return const Center( - child: Text( - 'Belum ada riwayat pemeriksaan', + 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, - color: Colors.grey, + fontWeight: FontWeight.bold, + color: Colors.black87, ), ), - ); - } + ], + ), + ), - // Tampilkan 3 history terbaru saja - final recentHistory = scanHistory.length > 3 - ? scanHistory.sublist(0, 3) - : scanHistory; - - return ListView.builder( - itemCount: recentHistory.length, - itemBuilder: (context, index) { - final item = recentHistory[index]; - return ScanHistoryCard( - imagePath: item.imagePath, - diseaseResult: item.diseaseResult, - timestamp: item.timestamp, - confidence: item.confidence, - diseaseId: item.diseaseId, - scanHistoryItem: item, // Menambahkan item scan history untuk fungsi hapus - ); - }, - ); - }), - ), - ], + // 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, + ), + ], + ), + ), + ), + ], + ), ), - ), - ), - floatingActionButton: FloatingActionButton( - onPressed: controller.startScanning, - backgroundColor: Colors.green, - child: const Icon( - Icons.camera_alt, - color: Colors.white, - ), - tooltip: 'Periksa Baru', + ); + }, + ); + } + + // 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), + ), + ), + ); + }, + ); + }); + }, ), ); } -} \ No newline at end of file + + // 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, + ), + ); + }, + ); + } +} diff --git a/lib/intro/page_intro.dart b/lib/intro/page_intro.dart new file mode 100644 index 0000000..9ebe477 --- /dev/null +++ b/lib/intro/page_intro.dart @@ -0,0 +1,282 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:harvest_guard_app/routes/app_routes.dart'; + +class IntroScreen extends StatefulWidget { + const IntroScreen({super.key}); + + @override + State createState() => _IntroScreenState(); +} + +class _IntroScreenState extends State { + final PageController _pageController = PageController(); + int _currentPage = 0; + + final List _introPages = [ + IntroPage( + title: 'Pilih Varietas Padi', + description: + 'Pilih jenis padi yang ingin Anda periksa dari daftar varietas yang tersedia', + icon: Icons.grass_rounded, + color: Colors.green.shade400, + ), + IntroPage( + title: 'Scan Tanaman Padi', + description: + 'Arahkan kamera ponsel Anda ke bagian tanaman padi yang ingin diperiksa', + icon: Icons.document_scanner_rounded, + color: Colors.blue.shade400, + ), + IntroPage( + title: 'Cek Hasil Diagnosis', + description: + 'Dapatkan hasil diagnosis penyakit dan rekomendasi penanganannya', + icon: Icons.checklist_rounded, + color: Colors.orange.shade400, + ), + ]; + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + // Background animasi bergerak + AnimatedPositioned( + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + top: -100 + (_currentPage * 50), + right: -100 + (_currentPage * 40), + child: Container( + height: 300, + width: 300, + decoration: BoxDecoration( + gradient: RadialGradient( + colors: [ + _introPages[_currentPage].color.withOpacity(0.7), + _introPages[_currentPage].color.withOpacity(0.0), + ], + stops: const [0.1, 1.0], + ), + shape: BoxShape.circle, + ), + ), + ), + AnimatedPositioned( + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + bottom: -80 + (_currentPage * 30), + left: -50 + (_currentPage * 20), + child: Container( + height: 250, + width: 250, + decoration: BoxDecoration( + gradient: RadialGradient( + colors: [ + _introPages[_currentPage].color.withOpacity(0.5), + _introPages[_currentPage].color.withOpacity(0.0), + ], + stops: const [0.1, 1.0], + ), + shape: BoxShape.circle, + ), + ), + ), + + // Content + Column( + children: [ + Expanded( + child: PageView.builder( + controller: _pageController, + itemCount: _introPages.length, + onPageChanged: (int page) { + setState(() { + _currentPage = page; + }); + }, + itemBuilder: (context, index) { + return _buildIntroPage(_introPages[index]); + }, + ), + ), + + // Pagination indicator + Container( + padding: const EdgeInsets.symmetric(vertical: 30), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate( + _introPages.length, + (index) => _buildDotIndicator(index), + ), + ), + ), + + // Bottom buttons + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 20, vertical: 30), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Next/Finish button + ElevatedButton( + onPressed: () { + if (_currentPage < _introPages.length - 1) { + _pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } else { + Get.offNamed(AppRoutes.dashboard); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: _introPages[_currentPage].color, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 30, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + ), + child: Text( + _currentPage < _introPages.length - 1 + ? 'Lanjut' + : 'Mulai', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildIntroPage(IntroPage page) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 30.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Icon with animated container + TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: const Duration(milliseconds: 600), + builder: (context, value, child) { + return Transform.scale( + scale: value, + child: Container( + padding: const EdgeInsets.all(25), + decoration: BoxDecoration( + color: page.color.withOpacity(0.2), + shape: BoxShape.circle, + ), + child: Icon( + page.icon, + size: 80, + color: page.color, + ), + ), + ); + }, + ), + + const SizedBox(height: 40), + + // Title + TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: const Duration(milliseconds: 800), + builder: (context, value, child) { + return Opacity( + opacity: value, + child: Transform.translate( + offset: Offset(0, 20 * (1 - value)), + child: Text( + page.title, + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + textAlign: TextAlign.center, + ), + ), + ); + }, + ), + + const SizedBox(height: 20), + + // Description + TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: const Duration(milliseconds: 1000), + builder: (context, value, child) { + return Opacity( + opacity: value, + child: Transform.translate( + offset: Offset(0, 30 * (1 - value)), + child: Text( + page.description, + style: const TextStyle( + fontSize: 16, + color: Colors.black54, + height: 1.5, + ), + textAlign: TextAlign.center, + ), + ), + ); + }, + ), + ], + ), + ); + } + + Widget _buildDotIndicator(int index) { + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + margin: const EdgeInsets.symmetric(horizontal: 5), + height: 8, + width: _currentPage == index ? 24 : 8, + decoration: BoxDecoration( + color: _currentPage == index + ? _introPages[_currentPage].color + : Colors.grey.shade300, + borderRadius: BorderRadius.circular(4), + ), + ); + } +} + +class IntroPage { + final String title; + final String description; + final IconData icon; + final Color color; + + IntroPage({ + required this.title, + required this.description, + required this.icon, + required this.color, + }); +} diff --git a/lib/main.dart b/lib/main.dart index f3f7574..5e64b18 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,17 +8,14 @@ import 'package:hive_flutter/adapters.dart'; Future main() async { runApp(const MainApp()); WidgetsFlutterBinding.ensureInitialized(); - + // Initialize Hive await Hive.initFlutter(); - + // Register adapters Hive.registerAdapter(ScanHistoryAdapter()); - - } - class MainApp extends StatelessWidget { const MainApp({super.key}); diff --git a/lib/periksa/periksa_page.dart b/lib/periksa/periksa_page.dart index 17398c1..66e8608 100644 --- a/lib/periksa/periksa_page.dart +++ b/lib/periksa/periksa_page.dart @@ -1,129 +1,756 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:harvest_guard_app/periksa/periksa_controller.dart'; +import 'dart:math' as math; - -class PeriksaScreen extends StatelessWidget { +class PeriksaScreen extends StatefulWidget { const PeriksaScreen({super.key}); + @override + State createState() => _PeriksaScreenState(); +} + +class _PeriksaScreenState extends State + with SingleTickerProviderStateMixin { + late final PeriksaController controller; + late AnimationController _animationController; + late Animation _fadeInAnimation; + late Animation _slideAnimation; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + + // Inisialisasi controller + controller = Get.put(PeriksaController()); + + // Setup animasi + _animationController = AnimationController( + duration: const Duration(milliseconds: 1200), + vsync: this, + ); + + _fadeInAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _animationController, + curve: const Interval(0.0, 0.6, curve: Curves.easeOut), + ), + ); + + _slideAnimation = Tween(begin: 50.0, end: 0.0).animate( + CurvedAnimation( + parent: _animationController, + curve: const Interval(0.0, 0.6, curve: Curves.easeOut), + ), + ); + + _scaleAnimation = Tween(begin: 0.8, end: 1.0).animate( + CurvedAnimation( + parent: _animationController, + curve: const Interval(0.2, 0.8, curve: Curves.elasticOut), + ), + ); + + // Mulai animasi + _animationController.forward(); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - final PeriksaController controller = Get.put(PeriksaController()); + final Size size = MediaQuery.of(context).size; return Scaffold( - appBar: AppBar( - title: const Text('Periksa Kesehatan Padi'), - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => Get.back(), - ), + body: Stack( + children: [ + // Background dengan efek gradient dan elemen dekoratif + _buildAnimatedBackground(), + + // Konten utama + SafeArea( + child: Column( + children: [ + // App Bar custom + _buildCustomAppBar(), + + // Konten utama dengan scroll + Expanded( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 10), + + // Judul utama dengan animasi + AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return Opacity( + opacity: _fadeInAnimation.value, + child: Transform.translate( + offset: Offset(0, _slideAnimation.value), + child: child, + ), + ); + }, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 12), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.green.shade400, + Colors.green.shade600, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: + Colors.green.shade200.withOpacity(0.5), + blurRadius: 15, + offset: const Offset(0, 5), + ), + ], + ), + child: const Text( + 'Periksa Kesehatan Padimu', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Colors.white, + letterSpacing: 0.5, + ), + ), + ), + ), + + const SizedBox(height: 40), + + // Ilustrasi + AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return Transform.scale( + scale: _scaleAnimation.value, + child: child, + ); + }, + child: Container( + width: 200, + height: 200, + decoration: BoxDecoration( + color: Colors.green.shade50, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + spreadRadius: 2, + ), + ], + ), + child: Center( + child: Stack( + alignment: Alignment.center, + children: [ + Icon( + Icons.eco_rounded, + size: 120, + color: Colors.green.shade300, + ), + Positioned( + top: 50, + right: 45, + child: Icon( + Icons.search, + size: 60, + color: Colors.green.shade700 + .withOpacity(0.8), + ), + ), + ], + ), + ), + ), + ), + + const SizedBox(height: 40), + + // Subtitle + AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + final delayedOpacity = + _animationController.value > 0.3 + ? (((_animationController.value - 0.3) / + 0.7) * + 1.0) + .clamp(0.0, 1.0) + : 0.0; + + return Opacity( + opacity: delayedOpacity, + child: Transform.translate( + offset: Offset(0, 20 * (1 - delayedOpacity)), + child: child, + ), + ); + }, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.06), + blurRadius: 8, + offset: const Offset(0, 3), + ), + ], + ), + child: Column( + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.info_outline, + size: 20, + color: Colors.green.shade700, + ), + const SizedBox(width: 8), + const Text( + 'Panduan Scanning', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + ], + ), + const SizedBox(height: 8), + const Text( + 'Pastikan foto dengan jelas di bagian daun padi untuk hasil deteksi optimal', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: Colors.black54, + height: 1.4, + ), + ), + ], + ), + ), + ), + + const SizedBox(height: 40), + + // Tombol Ambil Foto + AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + final delayedOpacity = + _animationController.value > 0.4 + ? (((_animationController.value - 0.4) / + 0.6) * + 1.0) + .clamp(0.0, 1.0) + : 0.0; + + return Opacity( + opacity: delayedOpacity, + child: Transform.translate( + offset: Offset(0, 30 * (1 - delayedOpacity)), + child: child, + ), + ); + }, + child: GestureDetector( + onTap: () => controller.takePhoto(), + child: Container( + width: 150, + height: 150, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.green.shade500, + Colors.green.shade700, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.green.shade300 + .withOpacity(0.5), + blurRadius: 15, + spreadRadius: 5, + offset: const Offset(0, 8), + ), + ], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.camera_alt_rounded, + color: Colors.white, + size: 40, + ), + ), + const SizedBox(height: 8), + const Text( + 'Ambil Foto', + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + ], + ), + ), + ), + ), + + const SizedBox(height: 30), + + // Divider dengan "atau" + AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + final delayedOpacity = + _animationController.value > 0.5 + ? (((_animationController.value - 0.5) / + 0.5) * + 1.0) + .clamp(0.0, 1.0) + : 0.0; + + return Opacity( + opacity: delayedOpacity, + child: child, + ); + }, + child: Row( + children: [ + Expanded( + child: Container( + height: 1, + color: Colors.grey.shade300, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.green.shade50, + borderRadius: BorderRadius.circular(20), + ), + child: const Text( + 'atau', + style: TextStyle( + fontSize: 14, + color: Colors.green, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + Expanded( + child: Container( + height: 1, + color: Colors.grey.shade300, + ), + ), + ], + ), + ), + + const SizedBox(height: 30), + + // Tombol Pilih dari Galeri + AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + final delayedOpacity = + _animationController.value > 0.6 + ? (((_animationController.value - 0.6) / + 0.4) * + 1.0) + .clamp(0.0, 1.0) + : 0.0; + + return Opacity( + opacity: delayedOpacity, + child: Transform.translate( + offset: Offset(0, 20 * (1 - delayedOpacity)), + child: child, + ), + ); + }, + child: GestureDetector( + onTap: () => controller.pickFromGallery(), + child: Container( + width: double.infinity, + height: 70, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + spreadRadius: 1, + offset: const Offset(0, 4), + ), + ], + border: Border.all( + color: Colors.green.shade200, + width: 1.5, + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20.0), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: Colors.green.shade50, + shape: BoxShape.circle, + ), + child: Icon( + Icons.photo_library_rounded, + size: 24, + color: Colors.green.shade600, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + const Text( + 'Pilih dari Galeri', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + const SizedBox(height: 4), + Text( + 'Format: JPG, JPEG, PNG', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + ], + ), + ), + Icon( + Icons.arrow_forward_ios, + size: 16, + color: Colors.grey.shade600, + ), + ], + ), + ), + ), + ), + ), + + const SizedBox(height: 40), + ], + ), + ), + ), + ), + ], + ), + ), + ], ), - body: SingleChildScrollView( + ); + } + + // Custom App Bar dengan efek transparan + Widget _buildCustomAppBar() { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + child: Row( + children: [ + // Back button dengan efek glassmorphism + Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.8), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 5, + spreadRadius: 1, + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () => Get.back(), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + Icons.arrow_back_ios_new_rounded, + size: 20, + color: Colors.green.shade700, + ), + ), + ), + ), + ), + + const Spacer(), + + // Info icon dengan efek glassmorphism + Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.8), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 5, + spreadRadius: 1, + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () { + _showInfoDialog(); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + Icons.help_outline_rounded, + size: 20, + color: Colors.green.shade700, + ), + ), + ), + ), + ), + ], + ), + ); + } + + // Animated Background dengan elemen dekoratif + Widget _buildAnimatedBackground() { + return Stack( + children: [ + // Gradient background + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.green.shade50, + Colors.white, + Colors.white, + ], + stops: const [0.0, 0.3, 1.0], + ), + ), + ), + + // Decorative top blob + Positioned( + top: -80, + right: -50, + child: Container( + height: 200, + width: 200, + decoration: BoxDecoration( + gradient: RadialGradient( + colors: [ + Colors.green.shade200.withOpacity(0.6), + Colors.green.shade200.withOpacity(0.0), + ], + stops: const [0.2, 1.0], + ), + shape: BoxShape.circle, + ), + ), + ), + + // Decorative bottom blob + Positioned( + bottom: 100, + left: -30, + child: Container( + height: 150, + width: 150, + decoration: BoxDecoration( + gradient: RadialGradient( + colors: [ + Colors.green.shade200.withOpacity(0.4), + Colors.green.shade200.withOpacity(0.0), + ], + stops: const [0.2, 1.0], + ), + shape: BoxShape.circle, + ), + ), + ), + + // Decorative elements + for (int i = 0; i < 5; i++) + Positioned( + top: 150 + (i * 120), + left: (i % 2 == 0) ? 20 : null, + right: (i % 2 == 0) ? null : 20, + child: AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + final delayedStart = 0.1 * i; + final localProgress = + ((_animationController.value - delayedStart) / + (1 - delayedStart)) + .clamp(0.0, 1.0); + + return Opacity( + opacity: localProgress * 0.3, + child: Transform.translate( + offset: Offset( + 0, + 20 * (1 - localProgress), + ), + child: Icon( + Icons.eco, + size: 20 + (i * 2), + color: Colors.green.shade300.withOpacity(0.3), + ), + ), + ); + }, + ), + ), + ], + ); + } + + void _showInfoDialog() { + Get.dialog( + Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), child: Padding( padding: const EdgeInsets.all(20.0), child: Column( - crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ - const SizedBox(height: 20), - // Judul utama - const Center( - child: Text( - 'Periksa Kesehatan\nPadimu Sekarang', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, + Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.green.shade100, + shape: BoxShape.circle, + ), + child: Icon( + Icons.tips_and_updates_rounded, + color: Colors.green.shade700, + size: 24, + ), ), - ), - ), - const SizedBox(height: 40), - - // Subtitle - const Center( - child: Text( - 'Foto di bagian daun padi', - style: TextStyle( - fontSize: 16, - color: Colors.black87, - ), - ), - ), - const SizedBox(height: 20), - - // Tombol Ambil Foto (lingkaran hijau) - GestureDetector( - onTap: () => controller.takePhoto(), - child: Container( - width: 150, - height: 150, - decoration: const BoxDecoration( - color: Colors.green, - shape: BoxShape.circle, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Ikon kamera dengan border putih - Container( - padding: const EdgeInsets.all(8), - child: const Icon( - Icons.center_focus_strong_outlined, - color: Colors.white, - size: 40, - ), + const SizedBox(width: 16), + const Expanded( + child: Text( + 'Tips Foto yang Baik', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, ), - const SizedBox(height: 5), - const Text( - 'Ambil Foto', - style: TextStyle( - color: Colors.white, - fontSize: 16, - ), - ), - ], + ), ), - ), - ), - - const SizedBox(height: 30), - const Text( - 'atau', - style: TextStyle( - fontSize: 16, - color: Colors.black54, - ), + ], ), const SizedBox(height: 20), - - // Tombol Pilih dari Galeri - GestureDetector( - onTap: () => controller.pickFromGallery(), - child: Container( - width: double.infinity, - height: 60, - decoration: BoxDecoration( - color: Colors.grey[300], + _buildTipItem( + Icons.brightness_5_rounded, + 'Foto dengan pencahayaan yang cukup', + ), + const SizedBox(height: 10), + _buildTipItem( + Icons.center_focus_strong_rounded, + 'Fokus pada bagian daun yang menunjukkan gejala', + ), + const SizedBox(height: 10), + _buildTipItem( + Icons.crop_rounded, + 'Hindari bayangan atau jari di foto', + ), + const SizedBox(height: 10), + _buildTipItem( + Icons.filter_rounded, + 'Jangan menggunakan filter kamera', + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () => Get.back(), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green.shade600, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), - child: const Padding( - padding: EdgeInsets.symmetric(horizontal: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'Pilih dari Galeri\n*JPG, JPEG, PNG', - style: TextStyle( - fontSize: 16, - color: Colors.black87, - ), - ), - const Icon( - Icons.insert_drive_file_outlined, - size: 30, - color: Colors.black87, - ), - ], - ), + minimumSize: const Size(double.infinity, 45), + ), + child: const Text( + 'Mengerti', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, ), ), ), @@ -133,4 +760,26 @@ class PeriksaScreen extends StatelessWidget { ), ); } + + Widget _buildTipItem(IconData icon, String text) { + return Row( + children: [ + Icon( + icon, + size: 18, + color: Colors.green.shade600, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + text, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade700, + ), + ), + ), + ], + ); + } } diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 1a753cb..2ff3fbb 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -8,6 +8,7 @@ import 'package:harvest_guard_app/components/scan_history_screen.dart'; import 'package:harvest_guard_app/components/splashscreen.dart'; import 'package:harvest_guard_app/dashboard/dashboard_binding.dart'; import 'package:harvest_guard_app/dashboard/dashboard_page.dart'; +import 'package:harvest_guard_app/intro/page_intro.dart'; import 'package:harvest_guard_app/periksa/periksa_binding.dart'; import 'package:harvest_guard_app/periksa/periksa_controller.dart'; import 'package:harvest_guard_app/periksa/periksa_page.dart'; @@ -18,9 +19,14 @@ class AppRoutes { static const String dashboard = '/dashboard'; static const String periksa = '/periksa'; static const String scanHistory = '/scan-history'; + static const String intro = '/intro'; // Daftar route aplikasi static final List pages = [ + GetPage( + name: intro, + page: () => const IntroScreen(), + ), GetPage( name: splash, page: () => const SplashScreen(), diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig index c2efd0b..4b81f9b 100644 --- a/macos/Flutter/Flutter-Debug.xcconfig +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig index c2efd0b..5caa9d1 100644 --- a/macos/Flutter/Flutter-Release.xcconfig +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Podfile b/macos/Podfile new file mode 100644 index 0000000..c795730 --- /dev/null +++ b/macos/Podfile @@ -0,0 +1,43 @@ +platform :osx, '10.14' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/macos/Podfile.lock b/macos/Podfile.lock new file mode 100644 index 0000000..4bc1576 --- /dev/null +++ b/macos/Podfile.lock @@ -0,0 +1,35 @@ +PODS: + - file_selector_macos (0.0.1): + - FlutterMacOS + - FlutterMacOS (1.0.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - tflite_flutter (0.0.1): + - FlutterMacOS + +DEPENDENCIES: + - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - tflite_flutter (from `Flutter/ephemeral/.symlinks/plugins/tflite_flutter/macos`) + +EXTERNAL SOURCES: + file_selector_macos: + :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos + FlutterMacOS: + :path: Flutter/ephemeral + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + tflite_flutter: + :path: Flutter/ephemeral/.symlinks/plugins/tflite_flutter/macos + +SPEC CHECKSUMS: + file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 + FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + tflite_flutter: d1496f2e968aa5a142fb282da8f5d754fcee5613 + +PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 + +COCOAPODS: 1.16.2 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 3cc441a..ff894bf 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -21,12 +21,14 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ + 20B381EA337542C3DD4070EA /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F97A4A17003EFDF1E242E694 /* Pods_RunnerTests.framework */; }; 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 6B879622470369E8A2157860 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7AA04D75EBF536695C72D258 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -60,11 +62,14 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 1B9D8C324CD0CFDF976861A8 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 1CF9B9D9A8593095E96C87F7 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 2E0747B455219D8FC5CEB350 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* harvest_guard_app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "harvest_guard_app.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* harvest_guard_app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = harvest_guard_app.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -76,8 +81,13 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 4148578F2DB7C29BE1987901 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 7AA04D75EBF536695C72D258 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 91A000F3F01E65B9710933E2 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + EFB6CD8707FA4682E67D3D53 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + F97A4A17003EFDF1E242E694 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -85,6 +95,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 20B381EA337542C3DD4070EA /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -92,6 +103,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 6B879622470369E8A2157860 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -125,6 +137,7 @@ 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, + B682B58076B3E8A496768CE4 /* Pods */, ); sourceTree = ""; }; @@ -172,9 +185,25 @@ path = Runner; sourceTree = ""; }; + B682B58076B3E8A496768CE4 /* Pods */ = { + isa = PBXGroup; + children = ( + EFB6CD8707FA4682E67D3D53 /* Pods-Runner.debug.xcconfig */, + 1CF9B9D9A8593095E96C87F7 /* Pods-Runner.release.xcconfig */, + 1B9D8C324CD0CFDF976861A8 /* Pods-Runner.profile.xcconfig */, + 2E0747B455219D8FC5CEB350 /* Pods-RunnerTests.debug.xcconfig */, + 91A000F3F01E65B9710933E2 /* Pods-RunnerTests.release.xcconfig */, + 4148578F2DB7C29BE1987901 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + 7AA04D75EBF536695C72D258 /* Pods_Runner.framework */, + F97A4A17003EFDF1E242E694 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -186,6 +215,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + F0F4B0455EC8BDC1BB981526 /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, @@ -204,11 +234,13 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 82D07F52A88D609488332DD1 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + 569AD5714DA21E30963986D5 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -329,6 +361,67 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; + 569AD5714DA21E30963986D5 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 82D07F52A88D609488332DD1 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + F0F4B0455EC8BDC1BB981526 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -380,6 +473,7 @@ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 2E0747B455219D8FC5CEB350 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -394,6 +488,7 @@ }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 91A000F3F01E65B9710933E2 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -408,6 +503,7 @@ }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 4148578F2DB7C29BE1987901 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..21a3cc1 100644 --- a/macos/Runner.xcworkspace/contents.xcworkspacedata +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/pubspec.lock b/pubspec.lock index 48ee3d7..ed48e83 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -798,10 +798,10 @@ packages: dependency: "direct main" description: name: tflite_flutter - sha256: ffb8651fdb116ab0131d6dc47ff73883e0f634ad1ab12bb2852eef1bbeab4a6a + sha256: "48e6fde2ad97162bb66a16a142f4c4698add9e8cd397ce9d1cc7451b55537ac1" url: "https://pub.dev" source: hosted - version: "0.10.4" + version: "0.11.0" timing: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 25159d3..e118a5d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,7 +12,7 @@ dependencies: get: ^4.7.2 image_cropper: ^9.0.0 image_picker: ^1.1.2 - tflite_flutter: ^0.10.0 + tflite_flutter: ^0.11.0 path_provider: ^2.1.0 image: ^4.1.0 hive_flutter: ^1.1.0