977 lines
35 KiB
Dart
977 lines
35 KiB
Dart
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<AboutPage> 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<Offset> inputLayer = [];
|
|
final List<Offset> hiddenLayer = [];
|
|
final List<Offset> 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<Offset> fromLayer, List<Offset> 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<Offset> 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<AboutPage> with TickerProviderStateMixin {
|
|
late AnimationController _logoAnimationController;
|
|
late AnimationController _cardAnimationController;
|
|
late Animation<double> _logoRotationAnimation;
|
|
late Animation<double> _logoScaleAnimation;
|
|
late Animation<double> _fadeInAnimation;
|
|
late List<Animation<Offset>> _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<double>(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<double>(begin: 1.0, end: 1.08).animate(
|
|
CurvedAnimation(
|
|
parent: _logoAnimationController,
|
|
curve: Curves.easeInOut,
|
|
),
|
|
);
|
|
|
|
_fadeInAnimation = Tween<double>(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<Offset>(
|
|
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<Map<String, dynamic>> 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,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|