1204 lines
39 KiB
Dart
1204 lines
39 KiB
Dart
import 'dart:math';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:firebase_database/firebase_database.dart';
|
|
|
|
class HomePage extends StatefulWidget {
|
|
const HomePage({super.key});
|
|
|
|
@override
|
|
State<HomePage> createState() => _HomePageState();
|
|
}
|
|
|
|
class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
|
|
final dbRef = FirebaseDatabase.instance.ref("pengering");
|
|
|
|
double temperature = 0;
|
|
double humidity = 0;
|
|
|
|
String fanIn = "OFF";
|
|
String fanOut = "OFF";
|
|
String heater = "OFF";
|
|
bool stepperLeft = false;
|
|
bool stepperRight = false;
|
|
String mode = "-";
|
|
bool exhaustManual = false;
|
|
|
|
// Controller animasi untuk putaran kipas 3D
|
|
late final AnimationController _fanInController;
|
|
late final AnimationController _fanOutController;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
_fanInController = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(milliseconds: 900),
|
|
);
|
|
_fanOutController = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(milliseconds: 650),
|
|
);
|
|
|
|
dbRef.onValue.listen((event) {
|
|
final data = event.snapshot.value;
|
|
|
|
if (data != null) {
|
|
try {
|
|
final map = Map<String, dynamic>.from(data as Map);
|
|
|
|
setState(() {
|
|
if (map["suhu"] != null) {
|
|
temperature = (map["suhu"] as num).toDouble();
|
|
}
|
|
|
|
if (map["kelembapan"] != null) {
|
|
humidity = (map["kelembapan"] as num).toDouble();
|
|
}
|
|
|
|
fanIn = map["fan_in"]?.toString() ?? "OFF";
|
|
fanOut = map["fan_out"]?.toString() ?? "OFF";
|
|
heater = map["heater"]?.toString() ?? "OFF";
|
|
|
|
mode = map["status"]?.toString() ?? "-";
|
|
|
|
// ===== Sinkron tombol Exhaust =====
|
|
exhaustManual = map["exhaust_manual"] == "ON";
|
|
|
|
// ===== Sinkron tombol Stepper =====
|
|
stepperLeft = map["stepper"] == "LEFT";
|
|
stepperRight = map["stepper"] == "RIGHT";
|
|
});
|
|
|
|
// ===== Sinkron animasi kipas 3D dengan status ON/OFF =====
|
|
if (isOn(fanIn)) {
|
|
if (!_fanInController.isAnimating) _fanInController.repeat();
|
|
} else {
|
|
_fanInController.stop();
|
|
}
|
|
|
|
if (isOn(fanOut)) {
|
|
if (!_fanOutController.isAnimating) _fanOutController.repeat();
|
|
} else {
|
|
_fanOutController.stop();
|
|
}
|
|
} catch (e) {
|
|
debugPrint("ERROR PARSE: $e");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_fanInController.dispose();
|
|
_fanOutController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
bool isOn(String value) => value == "ON";
|
|
|
|
/// =========================
|
|
/// CONTROL BUTTON
|
|
/// =========================
|
|
|
|
Future<void> sendAuto(String value) async {
|
|
await dbRef.update({"auto": value});
|
|
}
|
|
|
|
Future<void> sendExhaust(String value) async {
|
|
await dbRef.update({"exhaust_manual": value});
|
|
}
|
|
|
|
Future<void> sendStepper(String value) async {
|
|
await dbRef.update({"stepper": value});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: const Color(0xFFF6F1EE),
|
|
body: SafeArea(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
children: [
|
|
/// HEADER
|
|
Container(
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
gradient: const LinearGradient(
|
|
colors: [Color(0xFF6F4E37), Color(0xFF8D6E63)],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
borderRadius: BorderRadius.circular(30),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.brown.withOpacity(0.2),
|
|
blurRadius: 15,
|
|
offset: const Offset(0, 8),
|
|
),
|
|
],
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withOpacity(0.2),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Icon(
|
|
Icons.coffee,
|
|
color: Colors.white,
|
|
size: 30,
|
|
),
|
|
),
|
|
const SizedBox(width: 15),
|
|
const Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
"Coffee Dryer",
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
SizedBox(height: 5),
|
|
Text(
|
|
"Monitoring Pengering Kopi",
|
|
style: TextStyle(
|
|
color: Colors.white70,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 25),
|
|
|
|
/// STATUS MODE
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 20,
|
|
vertical: 15,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: mode == "AUTO" ? Colors.green : Colors.orange,
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(Icons.autorenew, color: Colors.white),
|
|
const SizedBox(width: 10),
|
|
Text(
|
|
"MODE : $mode",
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
letterSpacing: 1,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 25),
|
|
|
|
/// SENSOR CARD
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: sensorCard(
|
|
title: "Suhu",
|
|
value: "${temperature.toStringAsFixed(1)}°C",
|
|
icon: Icons.thermostat,
|
|
color: Colors.redAccent,
|
|
),
|
|
),
|
|
const SizedBox(width: 15),
|
|
Expanded(
|
|
child: sensorCard(
|
|
title: "Kelembapan",
|
|
value: "${humidity.toStringAsFixed(1)}%",
|
|
icon: Icons.water_drop,
|
|
color: Colors.blue,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 30),
|
|
|
|
/// CONTROL BUTTON
|
|
Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: Text(
|
|
"Kontrol Mesin",
|
|
style: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.brown.shade800,
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 15),
|
|
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: controlButton(
|
|
title: "Mulai",
|
|
icon: Icons.play_arrow,
|
|
color: Colors.green,
|
|
onTap: () => sendAuto("START"),
|
|
),
|
|
),
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
Expanded(
|
|
child: controlButton(
|
|
title: "Berhenti",
|
|
icon: Icons.stop,
|
|
color: Colors.red,
|
|
onTap: () => sendAuto("STOP"),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
|
|
Expanded(
|
|
child: controlButton(
|
|
title: exhaustManual ? "Exhaust ON" : "Exhaust OFF",
|
|
icon: Icons.wind_power,
|
|
color: exhaustManual ? Colors.green : Colors.blueGrey,
|
|
onTap: () async {
|
|
// Tidak boleh dimatikan saat suhu masih tinggi
|
|
if (temperature > 55 && exhaustManual) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text(
|
|
"Exhaust tidak bisa dimatikan saat suhu di atas 55°C",
|
|
),
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (exhaustManual) {
|
|
await sendExhaust("OFF");
|
|
} else {
|
|
await sendExhaust("ON");
|
|
}
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: controlButton(
|
|
title: stepperLeft ? "Mundur ON" : "Mundur OFF",
|
|
icon: Icons.rotate_left,
|
|
color: stepperLeft ? Colors.green : Colors.grey,
|
|
|
|
onTap: () async {
|
|
if (mode != "STOP") {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text(
|
|
"Stepper hanya bisa dijalankan setelah mesin berhenti",
|
|
),
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
stepperLeft = !stepperLeft;
|
|
|
|
if (stepperLeft) {
|
|
stepperRight = false;
|
|
await sendStepper("LEFT");
|
|
} else {
|
|
await sendStepper("STOP");
|
|
}
|
|
|
|
setState(() {});
|
|
},
|
|
),
|
|
),
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
Expanded(
|
|
child: controlButton(
|
|
title: stepperRight ? "Maju ON" : "Maju OFF",
|
|
icon: Icons.rotate_right,
|
|
color: stepperRight ? Colors.green : Colors.grey,
|
|
|
|
onTap: () async {
|
|
if (mode != "STOP") {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text(
|
|
"Stepper hanya bisa dijalankan setelah mesin berhenti",
|
|
),
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
stepperRight = !stepperRight;
|
|
|
|
if (stepperRight) {
|
|
stepperLeft = false;
|
|
await sendStepper("RIGHT");
|
|
} else {
|
|
await sendStepper("STOP");
|
|
}
|
|
|
|
setState(() {});
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
/// PANDUAN ISTILAH - penjelasan awam soal tombol
|
|
/// Maju & Mundur, gaya sama dengan kartu
|
|
/// Heater/Intake/Exhaust di bawah
|
|
Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: Text(
|
|
"Keterangan Tombol Maju & Mundur",
|
|
style: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.brown.shade800,
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 15),
|
|
|
|
stepperGuideCard(),
|
|
|
|
const SizedBox(height: 30),
|
|
|
|
/// INFORMASI ALAT - VISUALISASI 3D
|
|
Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: Text(
|
|
"Informasi Alat",
|
|
style: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.brown.shade800,
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 15),
|
|
|
|
machineVisualization(),
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
/// PANDUAN ISTILAH - penjelasan awam soal
|
|
/// Heater, Fan Intake, dan Fan Exhaust
|
|
Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: Text(
|
|
"Apa Arti Status Ini?",
|
|
style: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.brown.shade800,
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 15),
|
|
|
|
termGuideCard(),
|
|
|
|
const SizedBox(height: 20),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// SENSOR CARD
|
|
Widget sensorCard({
|
|
required String title,
|
|
required String value,
|
|
required IconData icon,
|
|
required Color color,
|
|
}) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(25),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 15,
|
|
offset: const Offset(0, 8),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(15),
|
|
decoration: BoxDecoration(
|
|
color: color.withOpacity(0.1),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(icon, size: 35, color: color),
|
|
),
|
|
const SizedBox(height: 15),
|
|
Text(
|
|
title,
|
|
style: TextStyle(color: Colors.grey.shade700, fontSize: 16),
|
|
),
|
|
const SizedBox(height: 10),
|
|
Text(
|
|
value,
|
|
style: TextStyle(
|
|
color: color,
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// CONTROL BUTTON
|
|
Widget controlButton({
|
|
required String title,
|
|
required IconData icon,
|
|
required Color color,
|
|
required VoidCallback onTap,
|
|
}) {
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 18),
|
|
decoration: BoxDecoration(
|
|
color: color,
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: color.withOpacity(0.3),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 5),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Icon(icon, color: Colors.white, size: 30),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
title,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// =========================================================
|
|
/// VISUALISASI MESIN PENGERING
|
|
///
|
|
/// Badan mesin digambar sebagai KUBUS/KOTAK, dibuat dari 2 layer
|
|
/// Container yang saling offset (bukan proyeksi isometrik trig)
|
|
/// supaya kesan "3D"-nya tetap ada tapi bentuknya tidak gampang
|
|
/// berantakan di berbagai ukuran layar.
|
|
///
|
|
/// - Kipas EXHAUST kiri & kanan menempel PERSIS di sisi kiri/kanan
|
|
/// kubus lewat bracket kecil.
|
|
/// - Kipas INTAKE disambungkan ke bagian ATAS kubus lewat duct -
|
|
/// mewakili posisi "belakang" (karena tampilan depan datar tidak
|
|
/// bisa menunjukkan sisi belakang secara literal).
|
|
/// - HEATER digambar sebagai bola cahaya + ikon api PERSIS DI
|
|
/// TENGAH kubus, menyala saat status ON.
|
|
/// =========================================================
|
|
Widget machineVisualization() {
|
|
return Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(25),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 15,
|
|
offset: const Offset(0, 8),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Center(
|
|
child: SizedBox(
|
|
width: 300,
|
|
height: 300,
|
|
child: Stack(
|
|
clipBehavior: Clip.none,
|
|
children: [
|
|
// ===== Duct Intake: dari kubus ke kipas intake =====
|
|
Positioned(
|
|
left: 143,
|
|
top: 69,
|
|
child: Container(
|
|
width: 14,
|
|
height: 50,
|
|
decoration: BoxDecoration(
|
|
color: isOn(fanIn)
|
|
? Colors.blue.shade200
|
|
: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
),
|
|
),
|
|
|
|
// ===== Lapisan belakang kubus (kesan kedalaman 3D) =====
|
|
Positioned(
|
|
left: 80,
|
|
top: 105,
|
|
child: Container(
|
|
width: 160,
|
|
height: 140,
|
|
decoration: BoxDecoration(
|
|
color: isOn(heater)
|
|
? Colors.deepOrange.shade700
|
|
: Colors.brown.shade600,
|
|
borderRadius: BorderRadius.circular(22),
|
|
),
|
|
),
|
|
),
|
|
|
|
// ===== KUBUS PENGERING (badan mesin, layer depan) =====
|
|
Positioned(
|
|
left: 70,
|
|
top: 115,
|
|
child: Container(
|
|
width: 160,
|
|
height: 140,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(22),
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: isOn(heater)
|
|
? [
|
|
Colors.orange.shade100,
|
|
Colors.deepOrange.shade200,
|
|
]
|
|
: [Colors.brown.shade100, Colors.brown.shade300],
|
|
),
|
|
border: Border.all(
|
|
color: Colors.black.withOpacity(0.08),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
// ===== Kilau tipis di sisi atas kubus =====
|
|
Positioned(
|
|
left: 82,
|
|
top: 123,
|
|
child: Container(
|
|
width: 136,
|
|
height: 10,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withOpacity(0.3),
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
),
|
|
),
|
|
|
|
// ===== Bracket KIRI (dudukan kipas exhaust) =====
|
|
Positioned(
|
|
left: 62,
|
|
top: 135,
|
|
child: Container(
|
|
width: 16,
|
|
height: 100,
|
|
decoration: BoxDecoration(
|
|
color: isOn(heater)
|
|
? Colors.deepOrange.shade400
|
|
: Colors.brown.shade500,
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
),
|
|
),
|
|
|
|
// ===== Bracket KANAN (dudukan kipas exhaust) =====
|
|
Positioned(
|
|
left: 222,
|
|
top: 135,
|
|
child: Container(
|
|
width: 16,
|
|
height: 100,
|
|
decoration: BoxDecoration(
|
|
color: isOn(heater)
|
|
? Colors.deepOrange.shade400
|
|
: Colors.brown.shade500,
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
),
|
|
),
|
|
|
|
// ===== HEATER - persis di tengah kubus =====
|
|
Positioned(
|
|
left: 112,
|
|
top: 147,
|
|
child: Container(
|
|
width: 76,
|
|
height: 76,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
gradient: RadialGradient(
|
|
colors: isOn(heater)
|
|
? [Colors.orange.shade300, Colors.red.shade600]
|
|
: [Colors.grey.shade300, Colors.grey.shade400],
|
|
),
|
|
boxShadow: isOn(heater)
|
|
? [
|
|
BoxShadow(
|
|
color: Colors.red.withOpacity(0.5),
|
|
blurRadius: 20,
|
|
spreadRadius: 4,
|
|
),
|
|
]
|
|
: [],
|
|
),
|
|
child: const Icon(
|
|
Icons.local_fire_department,
|
|
color: Colors.white,
|
|
size: 34,
|
|
),
|
|
),
|
|
),
|
|
|
|
// ===== Kipas EXHAUST KIRI (nempel di bracket kiri) =====
|
|
Positioned(
|
|
left: 44,
|
|
top: 159,
|
|
child: fanCircle(
|
|
isOnStatus: isOn(fanOut),
|
|
controller: _fanOutController,
|
|
color: Colors.green,
|
|
size: 52,
|
|
),
|
|
),
|
|
Positioned(
|
|
left: 22,
|
|
top: 215,
|
|
child: SizedBox(
|
|
width: 96,
|
|
child: Text(
|
|
"Exhaust",
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
// ===== Kipas EXHAUST KANAN (nempel di bracket kanan) =====
|
|
Positioned(
|
|
left: 204,
|
|
top: 159,
|
|
child: fanCircle(
|
|
isOnStatus: isOn(fanOut),
|
|
controller: _fanOutController,
|
|
color: Colors.green,
|
|
size: 52,
|
|
),
|
|
),
|
|
Positioned(
|
|
left: 182,
|
|
top: 215,
|
|
child: SizedBox(
|
|
width: 96,
|
|
child: Text(
|
|
"Exhaust",
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
// ===== Kipas INTAKE (belakang, nyambung ke duct atas) =====
|
|
Positioned(
|
|
left: 127,
|
|
top: 23,
|
|
child: fanCircle(
|
|
isOnStatus: isOn(fanIn),
|
|
controller: _fanInController,
|
|
color: Colors.blue,
|
|
size: 46,
|
|
),
|
|
),
|
|
Positioned(
|
|
left: 90,
|
|
top: 5,
|
|
child: SizedBox(
|
|
width: 120,
|
|
child: Text(
|
|
"Intake (Belakang)",
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
// ===== Ringkasan status ON/OFF =====
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
statusChip("Heater", isOn(heater), Colors.red),
|
|
statusChip("Fan In", isOn(fanIn), Colors.blue),
|
|
statusChip("Fan Out", isOn(fanOut), Colors.green),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// =========================================================
|
|
/// KARTU PANDUAN TOMBOL MAJU & MUNDUR
|
|
///
|
|
/// Gaya kartu & tile-nya sengaja dibuat identik dengan
|
|
/// termGuideCard() (Heater/Intake/Exhaust) di bawah, supaya
|
|
/// konsisten secara visual di seluruh halaman - pakai ulang
|
|
/// termGuideTile() yang sudah ada.
|
|
/// =========================================================
|
|
Widget stepperGuideCard() {
|
|
return Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(25),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 15,
|
|
offset: const Offset(0, 8),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
termGuideTile(
|
|
icon: Icons.rotate_right,
|
|
color: Colors.green,
|
|
title: "Tombol Maju",
|
|
description:
|
|
"Ketika ditekan hingga muncul ON, tabung akan bergerak ke depan. "
|
|
"Tekan lagi tombolnya agar perputaran berhenti.",
|
|
onMeaning: "Tabung sedang bergerak ke depan.",
|
|
offMeaning: "Perputaran berhenti.",
|
|
onColor: Colors.green,
|
|
),
|
|
const Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 14),
|
|
child: Divider(height: 1),
|
|
),
|
|
termGuideTile(
|
|
icon: Icons.rotate_left,
|
|
color: Colors.green,
|
|
title: "Tombol Mundur",
|
|
description:
|
|
"Ketika ditekan hingga muncul ON, tabung akan bergerak ke belakang. "
|
|
"Tekan lagi tombolnya agar perputaran berhenti.",
|
|
onMeaning: "Tabung sedang bergerak ke belakang.",
|
|
offMeaning: "Perputaran berhenti.",
|
|
onColor: Colors.green,
|
|
),
|
|
const Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 14),
|
|
child: Divider(height: 1),
|
|
),
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(10),
|
|
decoration: BoxDecoration(
|
|
color: Colors.orange.withOpacity(0.1),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(
|
|
Icons.warning_amber_rounded,
|
|
color: Colors.orange.shade800,
|
|
size: 22,
|
|
),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
"Perhatian",
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.orange.shade800,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
"Tombol ini hanya bisa ditekan ketika alat sedang diam "
|
|
"atau tidak sedang melakukan pengeringan.",
|
|
style: TextStyle(
|
|
fontSize: 12.5,
|
|
color: Colors.grey.shade700,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// =========================================================
|
|
/// KARTU PANDUAN ISTILAH
|
|
///
|
|
/// Menjelaskan dengan bahasa awam apa itu Heater, Fan Intake, dan
|
|
/// Fan Exhaust, serta apa artinya ketika masing-masing ON / OFF.
|
|
/// Diletakkan sebagai kartu statis (bukan dialog) supaya pengguna
|
|
/// baru langsung bisa membaca tanpa perlu tap apa pun.
|
|
/// =========================================================
|
|
Widget termGuideCard() {
|
|
return Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(25),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 15,
|
|
offset: const Offset(0, 8),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
termGuideTile(
|
|
icon: Icons.local_fire_department,
|
|
color: Colors.red,
|
|
title: "Heater (Pemanas)",
|
|
description:
|
|
"Elemen pemanas yang menghasilkan panas untuk mengeringkan biji kopi di dalam mesin.",
|
|
onMeaning: "Sedang memanaskan udara di dalam mesin.",
|
|
offMeaning: "Mati, suhu di dalam mesin akan turun perlahan.",
|
|
onColor: Colors.red,
|
|
),
|
|
const Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 14),
|
|
child: Divider(height: 1),
|
|
),
|
|
termGuideTile(
|
|
icon: Icons.air,
|
|
color: Colors.blue,
|
|
title: "Fan Intake (Kipas Masuk)",
|
|
description:
|
|
"Kipas yang mengalirkan udara panas dari heater masuk ke ruang pengering, tempat biji kopi berada.",
|
|
onMeaning: "Sedang mengalirkan udara panas masuk ke mesin.",
|
|
offMeaning: "Berhenti, tidak ada udara panas baru yang masuk.",
|
|
onColor: Colors.blue,
|
|
),
|
|
const Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 14),
|
|
child: Divider(height: 1),
|
|
),
|
|
termGuideTile(
|
|
icon: Icons.wind_power,
|
|
color: Colors.green,
|
|
title: "Fan Exhaust (Kipas Buang)",
|
|
description:
|
|
"Kipas yang membuang udara lembap sisa proses pengeringan ke luar mesin.",
|
|
onMeaning: "Sedang membuang udara lembap keluar mesin.",
|
|
offMeaning: "Berhenti, udara lembap tidak dikeluarkan.",
|
|
onColor: Colors.green,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Satu baris penjelasan istilah: ikon + judul + deskripsi singkat,
|
|
/// lalu dua baris kecil "Saat ON" dan "Saat OFF" biar orang awam
|
|
/// langsung paham konsekuensi status yang mereka lihat di layar.
|
|
Widget termGuideTile({
|
|
required IconData icon,
|
|
required Color color,
|
|
required String title,
|
|
required String description,
|
|
required String onMeaning,
|
|
required String offMeaning,
|
|
required Color onColor,
|
|
}) {
|
|
return Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(10),
|
|
decoration: BoxDecoration(
|
|
color: color.withOpacity(0.1),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(icon, color: color, size: 22),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.brown.shade800,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
description,
|
|
style: TextStyle(fontSize: 12.5, color: Colors.grey.shade700),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Row(
|
|
children: [
|
|
Container(
|
|
width: 8,
|
|
height: 8,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: onColor,
|
|
),
|
|
),
|
|
const SizedBox(width: 6),
|
|
Expanded(
|
|
child: Text(
|
|
"ON : $onMeaning",
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
color: onColor,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
Row(
|
|
children: [
|
|
Container(
|
|
width: 8,
|
|
height: 8,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: Colors.grey.shade400,
|
|
),
|
|
),
|
|
const SizedBox(width: 6),
|
|
Expanded(
|
|
child: Text(
|
|
"OFF : $offMeaning",
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// Housing kipas bulat + baling-baling berputar (RotationTransition).
|
|
/// Murni lingkaran (tanpa label di dalamnya) supaya bisa dipasang
|
|
/// presisi di titik mana pun lewat Positioned dari luar.
|
|
Widget fanCircle({
|
|
required bool isOnStatus,
|
|
required AnimationController controller,
|
|
required Color color,
|
|
double size = 54,
|
|
}) {
|
|
return Container(
|
|
width: size,
|
|
height: size,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: Colors.white,
|
|
border: Border.all(
|
|
color: isOnStatus ? color : Colors.grey.shade400,
|
|
width: 3,
|
|
),
|
|
boxShadow: isOnStatus
|
|
? [
|
|
BoxShadow(
|
|
color: color.withOpacity(0.45),
|
|
blurRadius: 12,
|
|
spreadRadius: 1,
|
|
),
|
|
]
|
|
: [],
|
|
),
|
|
padding: EdgeInsets.all(size * 0.14),
|
|
child: RotationTransition(
|
|
turns: controller,
|
|
child: CustomPaint(
|
|
painter: _FanBladesPainter(
|
|
color: isOnStatus ? color : Colors.grey.shade400,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Chip status ON/OFF ringkas
|
|
Widget statusChip(String label, bool status, Color color) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: status ? color.withOpacity(0.15) : Colors.grey.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 8,
|
|
height: 8,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: status ? color : Colors.grey,
|
|
),
|
|
),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
"$label ${status ? 'ON' : 'OFF'}",
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
color: status ? color : Colors.grey.shade600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// =========================================================
|
|
/// PAINTER: baling-baling kipas (4 blade), diputar via
|
|
/// RotationTransition dari luar.
|
|
/// =========================================================
|
|
class _FanBladesPainter extends CustomPainter {
|
|
final Color color;
|
|
_FanBladesPainter({required this.color});
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final center = Offset(size.width / 2, size.height / 2);
|
|
final r = size.width / 2;
|
|
final paint = Paint()..color = color;
|
|
|
|
for (int i = 0; i < 4; i++) {
|
|
final angle = (pi / 2) * i;
|
|
canvas.save();
|
|
canvas.translate(center.dx, center.dy);
|
|
canvas.rotate(angle);
|
|
|
|
final blade = Path()
|
|
..moveTo(0, 0)
|
|
..quadraticBezierTo(r * 0.6, -r * 0.35, r * 0.9, 0)
|
|
..quadraticBezierTo(r * 0.6, r * 0.35, 0, 0)
|
|
..close();
|
|
|
|
canvas.drawPath(blade, paint);
|
|
canvas.restore();
|
|
}
|
|
|
|
canvas.drawCircle(center, r * 0.18, Paint()..color = Colors.white);
|
|
canvas.drawCircle(
|
|
center,
|
|
r * 0.18,
|
|
Paint()
|
|
..color = color
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = 1.5,
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant _FanBladesPainter oldDelegate) =>
|
|
oldDelegate.color != color;
|
|
} |