import 'dart:convert'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' as rootBundle; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:forward_chaining_man_app/app/views/about/widget/diagram_painter.dart'; import 'package:forward_chaining_man_app/app/views/page_intro.dart'; import 'package:forward_chaining_man_app/app/views/page_login.dart'; import 'package:forward_chaining_man_app/app/views/page_profile.dart'; import 'package:get/get.dart'; import 'dart:math' as math; import 'package:url_launcher/url_launcher.dart'; class AboutPage extends StatefulWidget { const AboutPage({Key? key}) : super(key: key); @override State createState() => _AboutPageState(); } class ForwardChainingLogoPainter extends CustomPainter { final double animationValue; ForwardChainingLogoPainter({required this.animationValue}); @override void paint(Canvas canvas, Size size) { final centerX = size.width / 2; final centerY = size.height / 2; final radius = size.width * 0.32; // Create a layered neural network structure with input, hidden, and output nodes // Define the layers (3 layers: input, hidden, output) final int inputNodes = 4; final int hiddenNodes = 6; final int outputNodes = 3; // Node positions for each layer final List inputLayer = []; final List hiddenLayer = []; final List outputLayer = []; // Create a gradient for the background glow final Rect rect = Rect.fromCircle(center: Offset(centerX, centerY), radius: radius * 1.2); final gradient = RadialGradient( colors: [ Colors.indigo.shade400.withOpacity(0.2), Colors.transparent, ], stops: const [0.5, 1.0], ); final backgroundPaint = Paint() ..shader = gradient.createShader(rect) ..style = PaintingStyle.fill; canvas.drawCircle(Offset(centerX, centerY), radius * 1.2, backgroundPaint); // Paints for nodes final inputNodePaint = Paint() ..color = Colors.blue.shade400 ..style = PaintingStyle.fill; final hiddenNodePaint = Paint() ..color = Colors.indigo.shade400 ..style = PaintingStyle.fill; final outputNodePaint = Paint() ..color = Colors.purple.shade400 ..style = PaintingStyle.fill; // Paint for node glows final glowPaint = Paint() ..color = Colors.indigo.shade200.withOpacity(0.4) ..style = PaintingStyle.fill ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 8); // Paint for node borders final borderPaint = Paint() ..color = Colors.white.withOpacity(0.8) ..style = PaintingStyle.stroke ..strokeWidth = 1.5; // Paint for connections with animation final connectionPaint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = 1.5; // Create input layer nodes (left side) final inputSpacing = size.height / (inputNodes + 1); for (int i = 0; i < inputNodes; i++) { final y = (i + 1) * inputSpacing; final pulseFactor = math .sin((animationValue * 2 * math.pi) + (i * 0.5)) .clamp(-0.5, 0.5) * 0.05; final offsetX = (math.sin(animationValue * math.pi + i) * 4).clamp(-4, 4); // Position slightly to the left of center inputLayer.add(Offset( centerX - (radius * 0.7) + offsetX, y + (pulseFactor * size.height))); } // Create hidden layer nodes (center) final hiddenSpacing = size.height / (hiddenNodes + 1); for (int i = 0; i < hiddenNodes; i++) { final y = (i + 1) * hiddenSpacing; final pulseFactor = math .sin((animationValue * 2 * math.pi) + (i * 0.7)) .clamp(-0.5, 0.5) * 0.03; final offsetY = (math.sin(animationValue * math.pi * 2 + i * 0.5) * 5).clamp(-5, 5); // Position at center hiddenLayer .add(Offset(centerX + (pulseFactor * size.width), y + offsetY)); } // Create output layer nodes (right side) final outputSpacing = size.height / (outputNodes + 1); for (int i = 0; i < outputNodes; i++) { final y = (i + 1) * outputSpacing; final pulseFactor = math .sin((animationValue * 2 * math.pi) + (i * 0.9)) .clamp(-0.5, 0.5) * 0.05; final offsetX = (math.sin(animationValue * math.pi + i * 1.2) * 4).clamp(-4, 4); // Position to the right of center outputLayer.add(Offset( centerX + (radius * 0.7) + offsetX, y + (pulseFactor * size.height))); } // Draw connections with animated data flow void drawConnections( List fromLayer, List toLayer, Color baseColor) { for (int i = 0; i < fromLayer.length; i++) { for (int j = 0; j < toLayer.length; j++) { // Create a flow effect along the connection final path = Path(); path.moveTo(fromLayer[i].dx, fromLayer[i].dy); path.lineTo(toLayer[j].dx, toLayer[j].dy); // Create gradient shader for data flow effect final pathMetrics = path.computeMetrics().first; final length = pathMetrics.length; // Animate a dot along the path final flowPosition = (animationValue * 2 + (i * 0.1) + (j * 0.05)) % 1.0; final flowPoint = pathMetrics.getTangentForOffset(length * flowPosition)?.position; // Basic line connectionPaint.color = baseColor.withOpacity(0.3 + (0.2 * math.sin(animationValue * math.pi * 2 + i + j)) .clamp(0.0, 0.5)); canvas.drawPath(path, connectionPaint); // Draw data flow point if (flowPoint != null && (i + j) % 2 == 0) { // Only draw on some connections to avoid clutter final flowDotPaint = Paint() ..color = Colors.white.withOpacity(0.7) ..style = PaintingStyle.fill; canvas.drawCircle(flowPoint, 1.5, flowDotPaint); } } } } // Draw connections from input to hidden layer drawConnections(inputLayer, hiddenLayer, Colors.blue.shade500); // Draw connections from hidden to output layer drawConnections(hiddenLayer, outputLayer, Colors.purple.shade500); // Draw the nodes with glow effect void drawNodesWithEffects( List nodes, Paint nodePaint, double size) { for (int i = 0; i < nodes.length; i++) { final node = nodes[i]; // Size pulsation final pulse = 1.0 + 0.15 * math.sin((animationValue * 2 * math.pi) + (i)); final nodeSize = size * pulse.clamp(0.9, 1.15); // Draw glow canvas.drawCircle(node, nodeSize * 1.5, glowPaint); // Draw node canvas.drawCircle(node, nodeSize, nodePaint); // Draw border canvas.drawCircle(node, nodeSize, borderPaint); } } // Draw all nodes by layer drawNodesWithEffects(inputLayer, inputNodePaint, 5); drawNodesWithEffects(hiddenLayer, hiddenNodePaint, 6); drawNodesWithEffects(outputLayer, outputNodePaint, 5); // Draw central circle highlight final centerGlowPaint = Paint() ..color = Colors.indigo.withOpacity( (0.1 + 0.05 * math.sin(animationValue * math.pi * 2)) .clamp(0.05, 0.15)) ..style = PaintingStyle.fill ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 15); canvas.drawCircle(Offset(centerX, centerY), radius * 0.4, centerGlowPaint); } @override bool shouldRepaint(ForwardChainingLogoPainter oldDelegate) { return oldDelegate.animationValue != animationValue; } } class _AboutPageState extends State with TickerProviderStateMixin { late AnimationController _logoAnimationController; late AnimationController _cardAnimationController; late Animation _logoRotationAnimation; late Animation _logoScaleAnimation; late Animation _fadeInAnimation; late List> _cardSlideAnimations; // For step animation int _currentStep = 0; final int _totalSteps = 4; @override void initState() { super.initState(); // Logo animation controller _logoAnimationController = AnimationController( vsync: this, duration: const Duration(seconds: 3), )..repeat(reverse: true); // Card animation controller _cardAnimationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 1200), ); // Logo animations // Fix: Ensure the end value is <= 1.0 to prevent the assertion error _logoRotationAnimation = Tween(begin: 0, end: 0.05).animate( CurvedAnimation( parent: _logoAnimationController, curve: Curves.easeInOut, ), ); // Fix: Ensure the end value doesn't cause any curves to go beyond 1.0 _logoScaleAnimation = Tween(begin: 1.0, end: 1.08).animate( CurvedAnimation( parent: _logoAnimationController, curve: Curves.easeInOut, ), ); _fadeInAnimation = Tween(begin: 0, end: 1).animate( CurvedAnimation( parent: _cardAnimationController, // Fix: Ensure the end of the interval is <= 1.0 curve: const Interval(0, 0.6, curve: Curves.easeOut), ), ); // Card slide animations (staggered) _cardSlideAnimations = [ for (int i = 0; i < 5; i++) Tween( begin: const Offset(0, 0.5), end: Offset.zero, ).animate( CurvedAnimation( parent: _cardAnimationController, curve: Interval( 0.2 + (i * 0.12), (0.7 + (i * 0.08)).clamp(0.0, 1.0), // ✅ Perbaikan utama! curve: Curves.easeOutCubic, ), ), ), ]; // Start animations _cardAnimationController.forward(); // Start step animation Future.delayed(const Duration(seconds: 2), () { _startStepAnimation(); }); } void _startStepAnimation() { Future.delayed(const Duration(seconds: 3), () { if (mounted) { setState(() { _currentStep = (_currentStep + 1) % _totalSteps; }); _startStepAnimation(); } }); } @override void dispose() { _logoAnimationController.dispose(); _cardAnimationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ Colors.indigo.shade900, Colors.blue.shade800, Colors.blue.shade700, ], ), ), child: SafeArea( bottom: false, child: Column( children: [ // App Bar Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), child: Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(12), ), child: GestureDetector( onTap: () => Get.back(), child: const Icon( Icons.arrow_back, color: Colors.white, ), ), ), const Spacer(), const Text( 'Tentang Aplikasi', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Colors.white, ), ), const Spacer(), const SizedBox(width: 40), // Balance the layout ], ), ), Expanded( child: SingleChildScrollView( physics: const BouncingScrollPhysics(), padding: const EdgeInsets.all(24), child: Column( children: [ // Animated Logo const SizedBox(height: 24), // Title and Tagline FadeTransition( opacity: _fadeInAnimation, child: Column( children: [ const Text( 'EduGuide', style: TextStyle( fontSize: 32, fontWeight: FontWeight.bold, color: Colors.white, letterSpacing: 1.2, ), ), const SizedBox(height: 8), Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 8), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(50), ), child: const Text( 'Sistem Rekomendasi Karir & Jurusan', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white, ), ), ), ], ), ), const SizedBox(height: 40), // About App Card SlideTransition( position: _cardSlideAnimations[0], child: _buildInfoCard( title: 'Tentang Aplikasi', icon: Icons.info_outline, color: Colors.blue.shade300, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Aplikasi EduGuide adalah sistem pakar berbasis aturan (rule-based expert system) yang menggunakan metode inferensi forward chaining untuk memberikan rekomendasi jurusan dan karir yang sesuai dengan minat pengguna.', style: TextStyle(fontSize: 14, height: 1.5), ), const SizedBox(height: 16), const Text( 'Aplikasi ini dikembangkan sebagai bagian dari tugas akhir/skripsi untuk menunjukkan implementasi praktis dari metode forward chaining dalam sistem pendukung keputusan.', style: TextStyle(fontSize: 14, height: 1.5), ), ], ), ), ), const SizedBox(height: 16), // How It Works Card SlideTransition( position: _cardSlideAnimations[1], child: _buildInfoCard( title: 'Cara Kerja Forward Chaining', icon: Icons.lightbulb_outline, color: Colors.orange.shade300, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Forward Chaining adalah metode penalaran dari fakta-fakta yang diketahui menuju kesimpulan. Dalam aplikasi ini:', style: TextStyle(fontSize: 14, height: 1.5), ), const SizedBox(height: 16), // Animated steps _buildAnimatedStepExplanation(), const SizedBox(height: 20), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.amber.shade50, borderRadius: BorderRadius.circular(12), border: Border.all( color: Colors.amber.shade200, ), ), child: Row( children: [ Icon( Icons.tips_and_updates, color: Colors.amber.shade700, size: 24, ), const SizedBox(width: 12), const Expanded( child: Text( 'Dengan metode ini, sistem dapat memberikan rekomendasi yang paling sesuai berdasarkan minat kamu!', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, ), ), ), ], ), ), ], ), ), ), const SizedBox(height: 16), // Forward Chaining Visual Explanation SlideTransition( position: _cardSlideAnimations[2], child: _buildInfoCard( title: 'Visualisasi Proses', icon: Icons.bar_chart, color: Colors.green.shade300, child: Column( children: [ Image.asset( 'assets/forward_chaining_diagram.png', fit: BoxFit.contain, height: 180, errorBuilder: (context, error, stackTrace) { // Fallback if image not available return _buildForwardChainingDiagram(); }, ), const SizedBox(height: 16), const Text( 'Forward Chaining bekerja dengan mengevaluasi jawaban kamu dan mencocokkannya dengan aturan (rules) untuk menemukan rekomendasi terbaik. Ini seperti menyelesaikan teka-teki dengan petunjuk yang kamu berikan.', style: TextStyle(fontSize: 14, height: 1.5), ), ], ), ), ), const SizedBox(height: 16), // Tech Stack Card SlideTransition( position: _cardSlideAnimations[3], child: _buildInfoCard( title: 'Teknologi', icon: Icons.code, color: Colors.purple.shade300, child: Wrap( spacing: 10, runSpacing: 10, children: [ _buildTechChip( label: 'Flutter', icon: Icons.flutter_dash), _buildTechChip( label: 'Dart', icon: Icons.extension), _buildTechChip( label: 'Forward Chaining', icon: Icons.account_tree), _buildTechChip( label: 'GetX', icon: Icons.auto_awesome), _buildTechChip( label: 'Rule-Based System', icon: Icons.rule), _buildTechChip( label: 'Expert System', icon: Icons.psychology), ], ), ), ), const SizedBox(height: 16), // Developer Card SlideTransition( position: _cardSlideAnimations[4], child: _buildInfoCard( title: 'Pengembang', icon: Icons.person, color: Colors.amber.shade300, child: Row( children: [ Container( width: 80, height: 80, decoration: BoxDecoration( color: Colors.grey.shade200, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 8, offset: const Offset(0, 3), ), ], image: const DecorationImage( image: AssetImage('assets/profile_dev.png'), fit: BoxFit.cover, // Use a placeholder if no image is available onError: null, ), ), ), const SizedBox(width: 20), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Yanuar Tri Laksono', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 4), const Text( 'Mahasiswa Informatika', style: TextStyle( fontSize: 14, color: Colors.grey, ), ), const SizedBox(height: 8), Row( children: [ SocialButton( icon: FontAwesomeIcons .envelope, // Email icon onTap: () { launchUrl(Uri.parse( 'mailto:yanuartrilaksono23@gmail.com')); }, ), const SizedBox(width: 12), SocialButton( icon: FontAwesomeIcons .linkedin, // Portfolio link icon onTap: () { launchUrl(Uri.parse( 'https://www.linkedin.com/in/yanuar-tri-laksono/')); }, ), const SizedBox(width: 12), SocialButton( icon: FontAwesomeIcons .github, // GitHub icon onTap: () { launchUrl(Uri.parse( 'https://github.com/Greek-Cp')); }, ), ], ) ], ), ), ], ), ), ), const SizedBox(height: 30), // Footer FadeTransition( opacity: _fadeInAnimation, child: Column( children: [ Text( '© ${DateTime.now().year} EduGuide App', style: TextStyle( color: Colors.white.withOpacity(0.8), fontSize: 14, ), ), const SizedBox(height: 8), Text( 'Versi 1.0.0', style: TextStyle( color: Colors.white.withOpacity(0.6), fontSize: 12, ), ), ], ), ), const SizedBox(height: 40), ], ), ), ), ], ), ), ), ); } // Build the animated step explanation Widget _buildAnimatedStepExplanation() { final List> steps = [ { 'icon': Icons.question_answer, 'title': 'Langkah 1: Mengumpulkan Fakta', 'content': 'Sistem mengumpulkan jawaban "Ya" atau "Tidak" dari semua pertanyaanmu dan menyimpannya sebagai fakta.', 'color': Colors.blue.shade700, }, { 'icon': Icons.rule, 'title': 'Langkah 2: Mencocokkan Aturan', 'content': 'Sistem mencocokkan jawabanmu dengan aturan-aturan minat dan karir. Setiap jawaban "Ya" akan menambah skor pada minat tertentu.', 'color': Colors.green.shade700, }, { 'icon': Icons.calculate, 'title': 'Langkah 3: Menghitung Skor', 'content': 'Skor untuk setiap minat dan karir dihitung berdasarkan pertanyaan yang kamu jawab "Ya".', 'color': Colors.orange.shade700, }, { 'icon': Icons.star, 'title': 'Langkah 4: Memberikan Rekomendasi', 'content': 'Sistem mengurutkan hasil dan menampilkan 3 minat dengan skor tertinggi sebagai rekomendasi terbaikmu.', 'color': Colors.purple.shade700, }, ]; return Column( children: steps.asMap().entries.map((entry) { final index = entry.key; final step = entry.value; final isActive = index == _currentStep; return AnimatedContainer( duration: const Duration(milliseconds: 500), margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: isActive ? step['color'].withOpacity(0.1) : Colors.grey.shade50, borderRadius: BorderRadius.circular(12), border: Border.all( color: isActive ? step['color'] : Colors.grey.shade200, width: isActive ? 2 : 1, ), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ AnimatedContainer( duration: const Duration(milliseconds: 500), padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: isActive ? step['color'] : Colors.grey.shade200, shape: BoxShape.circle, ), child: Icon( step['icon'], color: isActive ? Colors.white : Colors.grey.shade600, size: 18, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( step['title'], style: TextStyle( fontWeight: FontWeight.w600, fontSize: 14, color: isActive ? step['color'] : Colors.black87, ), ), const SizedBox(height: 4), Text( step['content'], style: TextStyle( fontSize: 13, height: 1.4, color: isActive ? Colors.black87 : Colors.grey.shade700, ), ), ], ), ), ], ), ); }).toList(), ); } // Fallback Forward Chaining diagram if image is not available Widget _buildForwardChainingDiagram() { return Container( height: 180, decoration: BoxDecoration( color: Colors.grey.shade100, borderRadius: BorderRadius.circular(12), ), child: CustomPaint( painter: ForwardChainingDiagramPainter(), size: const Size(double.infinity, 180), ), ); } // Animated logo content with nodes and connections Widget _buildAnimatedLogoContent() { return CustomPaint( painter: ForwardChainingLogoPainter( animationValue: _logoAnimationController.value, ), child: const Icon( Icons.psychology, size: 70, color: Colors.indigo, ), ); } // Card widget with consistent styling Widget _buildInfoCard({ required String title, required IconData icon, required Color color, required Widget child, }) { return Container( width: double.infinity, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Card Header Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: color.withOpacity(0.2), borderRadius: const BorderRadius.only( topLeft: Radius.circular(24), topRight: Radius.circular(24), ), ), child: Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(12), ), child: Icon( icon, color: Colors.white, size: 20, ), ), const SizedBox(width: 12), Text( title, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), ], ), ), // Card Content Padding( padding: const EdgeInsets.all(16), child: child, ), ], ), ); } // Tech stack chip Widget _buildTechChip({required String label, required IconData icon}) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: Colors.grey.shade100, borderRadius: BorderRadius.circular(50), border: Border.all( color: Colors.grey.shade300, width: 1, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( icon, size: 16, color: Colors.indigo, ), const SizedBox(width: 6), Text( label, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: Colors.grey.shade800, ), ), ], ), ); } // Social media/contact button Widget _buildSocialButton({ required IconData icon, required VoidCallback onTap, }) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(12), child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.grey.shade200, borderRadius: BorderRadius.circular(12), ), child: Icon( icon, size: 18, color: Colors.indigo, ), ), ); } } class SocialButton extends StatelessWidget { final IconData icon; final VoidCallback onTap; const SocialButton({ required this.icon, required this.onTap, Key? key, }) : super(key: key); @override Widget build(BuildContext context) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(12), child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.grey.shade200, borderRadius: BorderRadius.circular(12), ), child: FaIcon( icon, size: 18, color: Colors.indigo, ), ), ); } }