927 lines
29 KiB
Dart
927 lines
29 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:firebase_auth/firebase_auth.dart';
|
|
import 'package:firebase_database/firebase_database.dart';
|
|
import 'history.dart';
|
|
import 'pesan.dart';
|
|
import 'akun.dart';
|
|
import '../widgets/fade_in_up.dart';
|
|
|
|
class DashboardPage extends StatefulWidget {
|
|
const DashboardPage({super.key});
|
|
|
|
@override
|
|
State<DashboardPage> createState() => _DashboardPageState();
|
|
}
|
|
|
|
class _DashboardPageState extends State<DashboardPage> {
|
|
final user = FirebaseAuth.instance.currentUser;
|
|
final sensorRef = FirebaseDatabase.instance.ref("fermentasi");
|
|
final controlRef = FirebaseDatabase.instance.ref("control");
|
|
|
|
double suhu = 0;
|
|
double alkohol = 0;
|
|
double waktu = 0;
|
|
String status = "Belum Mulai";
|
|
bool autoMode = true;
|
|
bool ptcOn = false;
|
|
bool kipas1On = false;
|
|
bool kipas2On = false;
|
|
bool fermentasiAktif = false;
|
|
String selectedTape = "";
|
|
|
|
Timer? _timer;
|
|
int detikBerjalan = 0;
|
|
int waktuMulaiUi = 0;
|
|
double _swipeValue = 0.0;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
listenSensor();
|
|
listenControl();
|
|
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
|
if (fermentasiAktif && mounted) {
|
|
setState(() {
|
|
detikBerjalan++;
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_timer?.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
void updateStatusKematangan() {
|
|
if (autoMode) {
|
|
if (!fermentasiAktif) {
|
|
// Cek threshold bahkan saat belum mulai di Mode Otomatis
|
|
if (selectedTape == "Ketan") {
|
|
if (alkohol >= 0.32) {
|
|
status = "Terlalu Matang";
|
|
return;
|
|
} else if (alkohol >= 0.25) {
|
|
status = "Matang";
|
|
return;
|
|
}
|
|
} else if (selectedTape == "Singkong") {
|
|
if (alkohol > 5.8) {
|
|
status = "Terlalu Matang";
|
|
return;
|
|
} else if (alkohol >= 3.5) {
|
|
status = "Matang";
|
|
return;
|
|
}
|
|
}
|
|
status = "Belum Mulai";
|
|
return;
|
|
}
|
|
// Di mode otomatis, gunakan treshold manual sebagai pendeteksi langsung di UI
|
|
if (selectedTape == "Ketan") {
|
|
if (alkohol >= 0.32) {
|
|
status = "Terlalu Matang";
|
|
return;
|
|
} else if (alkohol >= 0.25) {
|
|
status = "Matang";
|
|
return;
|
|
}
|
|
} else if (selectedTape == "Singkong") {
|
|
if (alkohol > 5.8) {
|
|
status = "Terlalu Matang";
|
|
return;
|
|
} else if (alkohol >= 3.5) {
|
|
status = "Matang";
|
|
return;
|
|
}
|
|
}
|
|
// In autoMode, the status is determined by the ESP32 dynamic peak-detection state machine
|
|
// and uploaded to Firebase (/fermentasi/status), which is handled in listenSensor().
|
|
// If no status has been loaded from Firebase yet, we set a sensible default.
|
|
if (status == "Belum Mulai") {
|
|
status = "Belum Matang";
|
|
}
|
|
} else {
|
|
// Mode Manual (matches the ESP32 manual thresholds)
|
|
if (selectedTape == "Ketan") {
|
|
if (alkohol >= 0.32) {
|
|
status = "Terlalu Matang";
|
|
return;
|
|
} else if (alkohol >= 0.25) {
|
|
status = "Matang";
|
|
return;
|
|
} else {
|
|
status = "Belum Matang";
|
|
}
|
|
} else if (selectedTape == "Singkong") {
|
|
if (alkohol > 5.8) {
|
|
status = "Terlalu Matang";
|
|
return;
|
|
} else if (alkohol >= 3.5) {
|
|
status = "Matang";
|
|
return;
|
|
} else {
|
|
status = "Belum Matang";
|
|
}
|
|
}
|
|
if (!fermentasiAktif) {
|
|
status = "Belum Mulai";
|
|
}
|
|
}
|
|
}
|
|
|
|
void listenSensor() {
|
|
sensorRef.onValue.listen((event) {
|
|
final data = event.snapshot.value as Map?;
|
|
if (data != null) {
|
|
if (mounted) {
|
|
setState(() {
|
|
suhu = (data["suhu"] as num?)?.toDouble() ?? 0;
|
|
alkohol = (data["alkohol"] as num?)?.toDouble() ?? 0;
|
|
if (data["waktu"] != null) {
|
|
waktu = (data["waktu"] as num?)?.toDouble() ?? 0;
|
|
int espSeconds = (waktu * 3600).toInt();
|
|
if (fermentasiAktif) {
|
|
if ((detikBerjalan - espSeconds).abs() > 10) {
|
|
detikBerjalan = espSeconds;
|
|
}
|
|
} else {
|
|
detikBerjalan = espSeconds;
|
|
}
|
|
}
|
|
if (data["status"] != null) {
|
|
String rawStatus = data["status"].toString();
|
|
String lowerStatus = rawStatus.toLowerCase();
|
|
// Standardize intermediate phases to "Belum Matang" to match notifications & history page mapping
|
|
if (lowerStatus.contains("optimal") || lowerStatus.contains("awal") || lowerStatus.contains("menjelang matang")) {
|
|
status = "Belum Matang";
|
|
} else {
|
|
status = rawStatus;
|
|
}
|
|
} else {
|
|
updateStatusKematangan();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
void listenControl() {
|
|
controlRef.onValue.listen((event) {
|
|
final data = event.snapshot.value as Map?;
|
|
if (data != null) {
|
|
if (mounted) {
|
|
setState(() {
|
|
autoMode = data["autoMode"] ?? true;
|
|
ptcOn = data["ptc"] ?? false;
|
|
kipas1On = data["kipas1"] ?? false; // kipas1On (pemanas) baca dari "kipas1"
|
|
kipas2On = data["kipas2"] ?? false; // kipas2On (kipas pendingin) baca dari "kipas2"
|
|
fermentasiAktif = data["fermentasi_aktif"] ?? false;
|
|
waktuMulaiUi = data["waktu_mulai_ui"] ?? 0;
|
|
|
|
if (fermentasiAktif && waktuMulaiUi > 0) {
|
|
int now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
|
int diff = now - waktuMulaiUi;
|
|
if (diff >= 0 && (detikBerjalan - diff).abs() > 2) {
|
|
detikBerjalan = diff;
|
|
}
|
|
}
|
|
|
|
if (data["jenisTape"] != null) {
|
|
selectedTape = data["jenisTape"];
|
|
}
|
|
|
|
bool resetReq = data["reset_request"] ?? false;
|
|
if (resetReq) {
|
|
status = "Belum Mulai";
|
|
detikBerjalan = 0;
|
|
waktu = 0;
|
|
}
|
|
|
|
// Only update status locally if in manual mode or if fermentation is stopped
|
|
if (!autoMode || !fermentasiAktif) {
|
|
updateStatusKematangan();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
String formatWaktu(double jam) {
|
|
int total = (jam * 3600).toInt();
|
|
int h = total ~/ 3600;
|
|
int m = (total % 3600) ~/ 60;
|
|
int s = total % 60;
|
|
return "${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}";
|
|
}
|
|
|
|
String formatWaktuDetik(int totalSeconds) {
|
|
int h = totalSeconds ~/ 3600;
|
|
int m = (totalSeconds % 3600) ~/ 60;
|
|
int s = totalSeconds % 60;
|
|
return "${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}";
|
|
}
|
|
|
|
String getUsername() {
|
|
if (user?.displayName != null && user!.displayName!.isNotEmpty)
|
|
return user!.displayName!;
|
|
if (user?.email != null) return user!.email!.split("@")[0];
|
|
return "User";
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
extendBody: true,
|
|
body: Stack(
|
|
children: [
|
|
Container(
|
|
decoration: const BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
stops: [0.0, 0.4, 0.7, 1.0],
|
|
colors: [
|
|
Color(0xFFE8F5E9),
|
|
Color(0xFFFFFDF0),
|
|
Color(0xFFF1F8E9),
|
|
Color(0xFFE8F5E9),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
SafeArea(
|
|
bottom: false,
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.fromLTRB(20, 24, 20, 110),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
FadeInUp(delay: 0, child: _header()),
|
|
const SizedBox(height: 28),
|
|
FadeInUp(delay: 150, child: _startFermentation()),
|
|
const SizedBox(height: 28),
|
|
FadeInUp(delay: 300, child: _actuatorControl()),
|
|
const SizedBox(height: 20),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
bottomNavigationBar: _buildBottomNavbar(),
|
|
);
|
|
}
|
|
|
|
Widget _buildBottomNavbar() {
|
|
return Container(
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xFFE8F5E9),
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(30)),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black12,
|
|
blurRadius: 10,
|
|
offset: Offset(0, -2),
|
|
),
|
|
],
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
|
child: BottomNavigationBar(
|
|
currentIndex: 0,
|
|
onTap: (index) {
|
|
if (index == 1) {
|
|
Navigator.pushReplacement(
|
|
context,
|
|
MaterialPageRoute(builder: (context) => const HistoryPage()),
|
|
);
|
|
} else if (index == 2) {
|
|
Navigator.pushReplacement(
|
|
context,
|
|
MaterialPageRoute(builder: (context) => const AkunScreen()),
|
|
);
|
|
}
|
|
},
|
|
showSelectedLabels: false,
|
|
showUnselectedLabels: false,
|
|
backgroundColor: Colors.transparent,
|
|
elevation: 0,
|
|
type: BottomNavigationBarType.fixed,
|
|
items: [
|
|
BottomNavigationBarItem(
|
|
icon: _navIcon(Icons.home_filled, 0),
|
|
label: 'Home',
|
|
),
|
|
BottomNavigationBarItem(
|
|
icon: _navIcon(Icons.history, 1),
|
|
label: 'History',
|
|
),
|
|
BottomNavigationBarItem(
|
|
icon: _navIcon(Icons.people_alt_rounded, 2),
|
|
label: 'Profile',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _navIcon(IconData icon, int index) {
|
|
bool isSelected = index == 0;
|
|
return AnimatedContainer(
|
|
duration: const Duration(milliseconds: 300),
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(16),
|
|
gradient: isSelected
|
|
? const LinearGradient(
|
|
colors: [Color(0xFF2E7D32), Color(0xFF4CAF50)],
|
|
)
|
|
: null,
|
|
),
|
|
child: Icon(
|
|
icon,
|
|
color: isSelected ? Colors.white : const Color(0xFF81C784),
|
|
size: 24,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _header() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
"Hello!",
|
|
style: TextStyle(color: Colors.black54, fontSize: 13),
|
|
),
|
|
Text(
|
|
getUsername(),
|
|
style: const TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const Spacer(),
|
|
GestureDetector(
|
|
onTap: () => Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => const NotificationPage(),
|
|
),
|
|
),
|
|
child: Container(
|
|
width: 42,
|
|
height: 42,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF2E7D32),
|
|
borderRadius: BorderRadius.circular(14),
|
|
),
|
|
child: const Icon(
|
|
Icons.notifications_none_rounded,
|
|
color: Colors.white,
|
|
size: 22,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 20),
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 22),
|
|
decoration: BoxDecoration(
|
|
gradient: const LinearGradient(
|
|
colors: [Color(0xFF2E7D32), Color(0xFF4CAF50)],
|
|
),
|
|
borderRadius: BorderRadius.circular(28),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
"Fermentasi kamu hari ini!",
|
|
style: TextStyle(
|
|
color: Colors.white70,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
status,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: [
|
|
_chip("Alkohol ${selectedTape == 'Singkong' ? alkohol.toStringAsFixed(1) : alkohol.toStringAsFixed(2)}%"),
|
|
const SizedBox(height: 8),
|
|
_chip("Suhu ${suhu.toStringAsFixed(1)}°C"),
|
|
const SizedBox(height: 8),
|
|
_chip(
|
|
fermentasiAktif
|
|
? formatWaktuDetik(detikBerjalan)
|
|
: (status != "Belum Mulai" ? formatWaktu(waktu) : "00:00:00"),
|
|
mono: true,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _chip(String text, {bool mono = false}) {
|
|
return Container(
|
|
width: 130,
|
|
padding: const EdgeInsets.symmetric(vertical: 9),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(14),
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
text,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 12.5,
|
|
fontFamily: mono ? 'monospace' : null,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _startFermentation() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Text(
|
|
"Start Fermentation",
|
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
|
),
|
|
if (fermentasiAktif)
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF2E7D32).withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 8,
|
|
height: 8,
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xFF2E7D32),
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
const SizedBox(width: 6),
|
|
const Text(
|
|
"Running...",
|
|
style: TextStyle(
|
|
color: Color(0xFF2E7D32),
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 14),
|
|
_segmentedModeSelector(),
|
|
const SizedBox(height: 16),
|
|
_simpleStartButton(),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _segmentedModeSelector() {
|
|
bool isKetan = selectedTape == "Ketan";
|
|
bool isSingkong = selectedTape == "Singkong";
|
|
bool hasSelection = selectedTape.isNotEmpty;
|
|
return Container(
|
|
height: 52,
|
|
padding: const EdgeInsets.all(4),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFE8F5E9),
|
|
borderRadius: BorderRadius.circular(26),
|
|
),
|
|
child: LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
double totalWidth = constraints.maxWidth;
|
|
if (totalWidth.isInfinite || totalWidth <= 0) {
|
|
totalWidth = MediaQuery.of(context).size.width - 40;
|
|
}
|
|
double width = totalWidth / 2;
|
|
return Stack(
|
|
children: [
|
|
AnimatedAlign(
|
|
duration: const Duration(milliseconds: 250),
|
|
curve: Curves.easeInOut,
|
|
alignment: isKetan ? Alignment.centerLeft : (isSingkong ? Alignment.centerRight : Alignment.center),
|
|
child: AnimatedOpacity(
|
|
duration: const Duration(milliseconds: 200),
|
|
opacity: hasSelection ? 1.0 : 0.0,
|
|
child: Container(
|
|
width: width,
|
|
height: double.infinity,
|
|
decoration: BoxDecoration(
|
|
color: isKetan ? const Color(0xFF2E7D32) : const Color(0xFFF9A825),
|
|
borderRadius: BorderRadius.circular(22),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: (isKetan ? const Color(0xFF2E7D32) : const Color(0xFFF9A825))
|
|
.withOpacity(0.3),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
if (!fermentasiAktif) {
|
|
setState(() => selectedTape = "Ketan");
|
|
controlRef.update({"jenisTape": "Ketan"});
|
|
}
|
|
},
|
|
behavior: HitTestBehavior.opaque,
|
|
child: Center(
|
|
child: Text(
|
|
"Tape Ketan",
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
color: isKetan ? Colors.white : Colors.black54,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
if (!fermentasiAktif) {
|
|
setState(() => selectedTape = "Singkong");
|
|
controlRef.update({"jenisTape": "Singkong"});
|
|
}
|
|
},
|
|
behavior: HitTestBehavior.opaque,
|
|
child: Center(
|
|
child: Text(
|
|
"Tape Singkong",
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
color: isSingkong ? Colors.white : Colors.black54,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _simpleStartButton() {
|
|
Color btnColor = fermentasiAktif ? Colors.redAccent : const Color(0xFF2E7D32);
|
|
return Container(
|
|
width: double.infinity,
|
|
height: 54,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: btnColor.withOpacity(0.25),
|
|
blurRadius: 15,
|
|
offset: const Offset(0, 6),
|
|
),
|
|
],
|
|
),
|
|
child: ElevatedButton.icon(
|
|
onPressed: _toggleFermentation,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: btnColor,
|
|
foregroundColor: Colors.white,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
elevation: 0,
|
|
),
|
|
icon: Icon(
|
|
fermentasiAktif ? Icons.stop_circle_rounded : Icons.play_circle_filled_rounded,
|
|
size: 24,
|
|
),
|
|
label: Text(
|
|
fermentasiAktif ? "STOP FERMENTASI" : "MULAI FERMENTASI",
|
|
style: const TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.bold,
|
|
letterSpacing: 0.5,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _toggleFermentation() {
|
|
if (!fermentasiAktif && selectedTape.isEmpty) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text("Pilih jenis tape terlebih dahulu!"),
|
|
backgroundColor: Colors.redAccent,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
int nowUnix = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
|
Map<String, dynamic> updates = {
|
|
"fermentasi_aktif": !fermentasiAktif,
|
|
"jenisTape": selectedTape,
|
|
};
|
|
if (!fermentasiAktif) {
|
|
updates["reset_request"] = true;
|
|
updates["waktu_mulai_ui"] = nowUnix;
|
|
}
|
|
|
|
controlRef.update(updates);
|
|
|
|
if (!fermentasiAktif) {
|
|
setState(() {
|
|
detikBerjalan = 0;
|
|
waktuMulaiUi = nowUnix;
|
|
});
|
|
}
|
|
}
|
|
|
|
Widget _actuatorControl() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
"Actuator Control",
|
|
style: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
color: Color(0xFF2D3142),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Column(
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: _buildActuatorCard(
|
|
title: "Kipas Pemanas",
|
|
subtitle: "Heater Fan",
|
|
isActive: kipas1On,
|
|
icon: Icons.toys_outlined,
|
|
activeGradient: const [Color(0xFFFFB74D), Color(0xFFF57C00)],
|
|
onTap: () {
|
|
if (autoMode) {
|
|
_showAutoModeWarning();
|
|
} else {
|
|
controlRef.update({"kipas1": !kipas1On});
|
|
}
|
|
},
|
|
isLocked: autoMode,
|
|
),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: _buildActuatorCard(
|
|
title: "PTC Heater",
|
|
subtitle: "Pemanas PTC",
|
|
isActive: ptcOn,
|
|
icon: Icons.local_fire_department_outlined,
|
|
activeGradient: const [Color(0xFFFF8A65), Color(0xFFD84315)],
|
|
onTap: () {
|
|
if (autoMode) {
|
|
_showAutoModeWarning();
|
|
} else {
|
|
controlRef.update({"ptc": !ptcOn});
|
|
}
|
|
},
|
|
isLocked: autoMode,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 14),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: _buildActuatorCard(
|
|
title: "Kipas Pendingin",
|
|
subtitle: "Cooler Fan",
|
|
isActive: kipas2On,
|
|
icon: Icons.ac_unit_rounded,
|
|
activeGradient: const [Color(0xFF0288D1), Color(0xFF03A9F4)],
|
|
onTap: () {
|
|
if (autoMode) {
|
|
_showAutoModeWarning();
|
|
} else {
|
|
controlRef.update({"kipas2": !kipas2On});
|
|
}
|
|
},
|
|
isLocked: autoMode,
|
|
),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: _buildActuatorCard(
|
|
title: "Mode Otomatis",
|
|
subtitle: "Smart Auto",
|
|
isActive: autoMode,
|
|
icon: Icons.settings_suggest_rounded,
|
|
activeGradient: const [Color(0xFF2E7D32), Color(0xFF4CAF50)],
|
|
onTap: () {
|
|
controlRef.update({"autoMode": !autoMode});
|
|
},
|
|
isLocked: !autoMode,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
void _showAutoModeWarning() {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text("Nonaktifkan Mode Otomatis terlebih dahulu!"),
|
|
backgroundColor: Colors.redAccent,
|
|
duration: Duration(seconds: 2),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildActuatorCard({
|
|
required String title,
|
|
required String subtitle,
|
|
required bool isActive,
|
|
required IconData icon,
|
|
required List<Color> activeGradient,
|
|
required VoidCallback onTap,
|
|
required bool isLocked,
|
|
}) {
|
|
Color cardBg = Colors.white;
|
|
Color borderCol = isActive ? activeGradient[0] : Colors.grey.withOpacity(0.08);
|
|
double borderWidth = isActive ? 1.5 : 1.0;
|
|
|
|
Color iconBg = isActive ? activeGradient[0] : const Color(0xFFF5F7F8);
|
|
Color iconCol = isActive ? Colors.white : Colors.black38;
|
|
|
|
Color titleCol = Colors.black87;
|
|
Color subtitleCol = isActive ? activeGradient[0].withOpacity(0.8) : Colors.black38;
|
|
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 250),
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: cardBg,
|
|
borderRadius: BorderRadius.circular(24),
|
|
border: Border.all(
|
|
color: borderCol,
|
|
width: borderWidth,
|
|
),
|
|
boxShadow: [
|
|
if (isActive)
|
|
BoxShadow(
|
|
color: activeGradient[0].withOpacity(0.12),
|
|
blurRadius: 16,
|
|
offset: const Offset(0, 8),
|
|
)
|
|
else
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.01),
|
|
blurRadius: 6,
|
|
offset: const Offset(0, 3),
|
|
)
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
// Glowing Icon Wrapper
|
|
AnimatedContainer(
|
|
duration: const Duration(milliseconds: 250),
|
|
padding: const EdgeInsets.all(10),
|
|
decoration: BoxDecoration(
|
|
color: iconBg,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(
|
|
icon,
|
|
color: iconCol,
|
|
size: 22,
|
|
),
|
|
),
|
|
// Lock Icon or custom Switch Toggle
|
|
isLocked
|
|
? const Icon(
|
|
Icons.lock_outline_rounded,
|
|
color: Colors.black26,
|
|
size: 20,
|
|
)
|
|
: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 250),
|
|
width: 36,
|
|
height: 20,
|
|
padding: const EdgeInsets.symmetric(horizontal: 2),
|
|
decoration: BoxDecoration(
|
|
color: isActive ? activeGradient[0].withOpacity(0.15) : Colors.black.withOpacity(0.08),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: AnimatedAlign(
|
|
duration: const Duration(milliseconds: 200),
|
|
alignment: isActive ? Alignment.centerRight : Alignment.centerLeft,
|
|
child: Container(
|
|
width: 14,
|
|
height: 14,
|
|
decoration: BoxDecoration(
|
|
color: isActive ? activeGradient[0] : Colors.black26,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 18),
|
|
Text(
|
|
title,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.bold,
|
|
color: titleCol,
|
|
),
|
|
),
|
|
const SizedBox(height: 3),
|
|
Text(
|
|
subtitle,
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
color: subtitleCol,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|