1093 lines
36 KiB
Dart
1093 lines
36 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
import '../providers/sensor_provider.dart';
|
|
import '../providers/ac_provider.dart';
|
|
import '../providers/ac_config_provider.dart';
|
|
import '../providers/ac_setup_provider.dart';
|
|
import '../providers/auth_provider.dart';
|
|
import '../models/sensor_model.dart';
|
|
import '../models/ac_model.dart';
|
|
import 'monitoring_history_page.dart';
|
|
import 'profile_page.dart';
|
|
import 'login_page.dart';
|
|
import '../core/app_color.dart';
|
|
|
|
class DashboardPage extends StatefulWidget {
|
|
const DashboardPage({super.key});
|
|
|
|
@override
|
|
State<DashboardPage> createState() => _DashboardPageState();
|
|
}
|
|
|
|
class _DashboardPageState extends State<DashboardPage> {
|
|
int currentIndex = 0;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
context.read<SensorProvider>().startListening();
|
|
context.read<ACProvider>().startListening();
|
|
context.read<ACConfigProvider>().startListening();
|
|
context.read<ACSetupProvider>().startListening();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final sensorProvider = context.watch<SensorProvider>();
|
|
final sensor = sensorProvider.sensor;
|
|
final acProvider = context.watch<ACProvider>();
|
|
final ac = acProvider.ac;
|
|
final acConfig = context.watch<ACConfigProvider>();
|
|
final setupProvider = context.watch<ACSetupProvider>();
|
|
|
|
final pages = [
|
|
_buildDashboard(
|
|
sensor,
|
|
sensorProvider.isLoading,
|
|
sensorProvider.errorMessage,
|
|
ac,
|
|
acProvider,
|
|
acConfig,
|
|
setupProvider,
|
|
),
|
|
const MonitoringHistoryPage(),
|
|
const ProfilePage(),
|
|
];
|
|
|
|
return Scaffold(
|
|
body: pages[currentIndex],
|
|
bottomNavigationBar: BottomNavigationBar(
|
|
currentIndex: currentIndex,
|
|
onTap: (value) => setState(() => currentIndex = value),
|
|
selectedItemColor: Colors.blueAccent,
|
|
type: BottomNavigationBarType.fixed,
|
|
items: const [
|
|
BottomNavigationBarItem(
|
|
icon: Icon(Icons.dashboard),
|
|
label: 'Dashboard',
|
|
),
|
|
BottomNavigationBarItem(icon: Icon(Icons.history), label: 'Histori'),
|
|
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profil'),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// ====================== DASHBOARD BUILD ======================
|
|
|
|
Widget _buildDashboard(
|
|
SensorModel? sensor,
|
|
bool isLoading,
|
|
String? errorMessage,
|
|
ACModel ac,
|
|
ACProvider acProvider,
|
|
ACConfigProvider acConfig,
|
|
ACSetupProvider setupProvider,
|
|
) {
|
|
Widget bodyContent;
|
|
|
|
if (isLoading) {
|
|
bodyContent = const Center(
|
|
child: CircularProgressIndicator(color: Colors.blueAccent),
|
|
);
|
|
} else if (sensor == null) {
|
|
bodyContent = Center(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(Icons.cloud_off, size: 80, color: Colors.grey),
|
|
const SizedBox(height: 16),
|
|
const Text(
|
|
'Data Tidak Tersedia',
|
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
errorMessage ?? 'Database kosong atau koneksi ke IoT terputus.',
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(color: Colors.grey),
|
|
),
|
|
const SizedBox(height: 24),
|
|
ElevatedButton.icon(
|
|
onPressed: () {
|
|
context.read<SensorProvider>().startListening();
|
|
context.read<ACProvider>().startListening();
|
|
},
|
|
icon: const Icon(Icons.refresh),
|
|
label: const Text('Coba Lagi'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.blueAccent,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
} else {
|
|
final isAirGood =
|
|
sensor.overallStatus == 'Sangat Baik' ||
|
|
sensor.overallStatus == 'Baik';
|
|
|
|
bodyContent = ListView(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
children: [
|
|
// Header
|
|
Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: const BoxDecoration(
|
|
color: Colors.blueAccent,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Icon(Icons.ac_unit, color: Colors.white, size: 24),
|
|
),
|
|
const SizedBox(width: 12),
|
|
const Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Smart AC',
|
|
style: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
Text(
|
|
'IoT Dashboard',
|
|
style: TextStyle(fontSize: 12, color: Colors.grey),
|
|
),
|
|
],
|
|
),
|
|
const Spacer(),
|
|
// Live Badge
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 10,
|
|
vertical: 4,
|
|
),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(
|
|
color: Colors.green.withValues(alpha: 0.5),
|
|
),
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: const Row(
|
|
children: [
|
|
Icon(
|
|
Icons.fiber_manual_record,
|
|
color: Colors.green,
|
|
size: 10,
|
|
),
|
|
SizedBox(width: 4),
|
|
Text(
|
|
'Live',
|
|
style: TextStyle(
|
|
color: Colors.green,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
IconButton(
|
|
icon: const Icon(Icons.logout, color: Colors.white70),
|
|
onPressed: () async {
|
|
await context.read<AuthProvider>().logout();
|
|
if (!mounted) return;
|
|
Navigator.pushAndRemoveUntil(
|
|
context,
|
|
MaterialPageRoute(builder: (_) => const LoginPage()),
|
|
(route) => false,
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Air Quality Banner
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: isAirGood ? Colors.green : Colors.red,
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
isAirGood
|
|
? Icons.check_circle_outline
|
|
: Icons.warning_amber_outlined,
|
|
color: Colors.white,
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Text(
|
|
isAirGood
|
|
? 'Kualitas udara baik — semua parameter normal'
|
|
: 'Kualitas udara buruk — Buka ventilasi ruangan',
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
const Text(
|
|
'Sensor Data',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white70,
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
|
|
// 2x2 Sensor Grid
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: _buildSensorCard(
|
|
title: 'Suhu',
|
|
value: '${sensor.temperature.toStringAsFixed(1)} °C',
|
|
status: sensor.temperatureStatus,
|
|
icon: Icons.thermostat,
|
|
iconColor: Colors.blue,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: _buildSensorCard(
|
|
title: 'Kelembaban',
|
|
value: '${sensor.humidity.toStringAsFixed(1)} %',
|
|
status: sensor.humidityStatus,
|
|
icon: Icons.water_drop,
|
|
iconColor: Colors.green,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: _buildSensorCard(
|
|
title: 'PM2.5',
|
|
value: '${sensor.pm25.toStringAsFixed(2)} µg/m³',
|
|
status: sensor.pm25Status,
|
|
icon: Icons.grain,
|
|
iconColor: Colors.amber,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: _buildSensorCard(
|
|
title: 'Karbon Dioksida',
|
|
value: '${sensor.co2.toStringAsFixed(0)} ppm',
|
|
status: sensor.co2Status,
|
|
icon: Icons.co2,
|
|
iconColor: Colors.redAccent,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// ============ AC CARD (inline setup + manual control) ============
|
|
_buildACCard(ac, acProvider, acConfig, setupProvider),
|
|
|
|
const SizedBox(height: 24),
|
|
],
|
|
);
|
|
}
|
|
|
|
return Scaffold(
|
|
backgroundColor: AppColor.background,
|
|
body: SafeArea(child: bodyContent),
|
|
);
|
|
}
|
|
|
|
// ====================== AC CARD ======================
|
|
// This single card handles both setup flow and manual control inline.
|
|
|
|
Widget _buildACCard(
|
|
ACModel ac,
|
|
ACProvider acProvider,
|
|
ACConfigProvider acConfig,
|
|
ACSetupProvider setupProvider,
|
|
) {
|
|
// Determine if we're currently in setup flow
|
|
final bool isInSetup = setupProvider.mode == 'config' ||
|
|
setupProvider.mode == 'sweep';
|
|
|
|
// If in active setup flow, show the setup UI
|
|
if (isInSetup) {
|
|
return _buildSetupFlowCard(setupProvider);
|
|
}
|
|
|
|
// If has config, show manual control
|
|
if (acConfig.hasConfig) {
|
|
return _buildManualControlCard(ac, acProvider, setupProvider);
|
|
}
|
|
|
|
// No config and not in setup — show "start config" prompt
|
|
return _buildStartConfigCard(setupProvider);
|
|
}
|
|
|
|
// ====================== START CONFIG CARD ======================
|
|
|
|
Widget _buildStartConfigCard(ACSetupProvider setupProvider) {
|
|
return Card(
|
|
color: AppColor.cardBg,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildCardHeader(
|
|
icon: Icons.tune,
|
|
title: 'Konfigurasi AC',
|
|
subtitle: 'Pilih merk lalu uji model hingga cocok',
|
|
),
|
|
const SizedBox(height: 20),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton.icon(
|
|
onPressed: setupProvider.isSending
|
|
? null
|
|
: () => setupProvider.startConfigMode(),
|
|
icon: setupProvider.isSending
|
|
? const SizedBox(
|
|
width: 16,
|
|
height: 16,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
color: Colors.white,
|
|
),
|
|
)
|
|
: const Icon(Icons.settings),
|
|
label: Text(
|
|
setupProvider.isSending
|
|
? 'Mengirim...'
|
|
: 'Mulai Konfigurasi AC',
|
|
),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.blueAccent,
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
const Text(
|
|
'Langkah singkat:',
|
|
style: TextStyle(color: Colors.grey),
|
|
),
|
|
const SizedBox(height: 6),
|
|
const Text(
|
|
'1) Tekan "Mulai Konfigurasi".\n'
|
|
'2) Pilih merek AC dari daftar.\n'
|
|
'3) Untuk tiap model tekan "YA" bila AC merespon.',
|
|
style: TextStyle(color: Colors.white70, fontSize: 12),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ====================== SETUP FLOW CARD ======================
|
|
|
|
Widget _buildSetupFlowCard(ACSetupProvider setupProvider) {
|
|
return Card(
|
|
color: AppColor.cardBg,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildCardHeader(
|
|
icon: Icons.tune,
|
|
title: 'Konfigurasi AC',
|
|
subtitle: setupProvider.message,
|
|
trailing: TextButton.icon(
|
|
onPressed: setupProvider.isSending
|
|
? null
|
|
: () => setupProvider.cancelConfig(),
|
|
icon: const Icon(Icons.close, size: 16, color: Colors.redAccent),
|
|
label: const Text(
|
|
'Batal',
|
|
style: TextStyle(color: Colors.redAccent, fontSize: 12),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Step: select_brand — show brand list
|
|
if (setupProvider.step == 'select_brand')
|
|
_buildBrandGrid(setupProvider),
|
|
|
|
// Step: confirm — show sweep confirmation
|
|
if (setupProvider.step == 'confirm')
|
|
_buildSweepConfirm(setupProvider),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildBrandGrid(ACSetupProvider setupProvider) {
|
|
if (setupProvider.brands.isEmpty) {
|
|
return const Center(
|
|
child: Padding(
|
|
padding: EdgeInsets.all(20),
|
|
child: Column(
|
|
children: [
|
|
CircularProgressIndicator(color: Colors.blueAccent),
|
|
SizedBox(height: 12),
|
|
Text(
|
|
'Memuat daftar merek...',
|
|
style: TextStyle(color: Colors.grey),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Pilih Merek AC:',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
...setupProvider.brands.map((brand) {
|
|
final int index = brand['index'] ?? 0;
|
|
final String desc = brand['description'] ?? brand['name'] ?? '';
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 8),
|
|
child: SizedBox(
|
|
width: double.infinity,
|
|
child: OutlinedButton.icon(
|
|
onPressed: setupProvider.isSending
|
|
? null
|
|
: () => setupProvider.selectBrand(index),
|
|
icon: const Icon(Icons.air, size: 18),
|
|
label: Text(desc),
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: Colors.blueAccent,
|
|
side: BorderSide(
|
|
color: Colors.blueAccent.withValues(alpha: 0.4),
|
|
),
|
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildSweepConfirm(ACSetupProvider setupProvider) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Show current brand/model being tested
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.blueAccent.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(
|
|
color: Colors.blueAccent.withValues(alpha: 0.3),
|
|
),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Menguji: ${setupProvider.brandDesc}',
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
if (setupProvider.modelName.isNotEmpty)
|
|
Text(
|
|
'Model: ${setupProvider.modelName}',
|
|
style: const TextStyle(color: Colors.white70, fontSize: 12),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
const Text(
|
|
'Apakah AC merespon sinyal ini?',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.w600,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: ElevatedButton.icon(
|
|
onPressed: setupProvider.isSending
|
|
? null
|
|
: () => setupProvider.confirm('y'),
|
|
icon: const Icon(Icons.check, size: 18),
|
|
label: const Text('YA'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.green,
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: ElevatedButton.icon(
|
|
onPressed: setupProvider.isSending
|
|
? null
|
|
: () => setupProvider.confirm('n'),
|
|
icon: const Icon(Icons.close, size: 18),
|
|
label: const Text('TIDAK'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.redAccent,
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: OutlinedButton(
|
|
onPressed: setupProvider.isSending
|
|
? null
|
|
: () => setupProvider.confirm('skip'),
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: Colors.white70,
|
|
side: const BorderSide(color: Colors.white30),
|
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
child: const Text('SKIP'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
// ====================== MANUAL CONTROL CARD ======================
|
|
|
|
Widget _buildManualControlCard(
|
|
ACModel ac,
|
|
ACProvider acProvider,
|
|
ACSetupProvider setupProvider,
|
|
) {
|
|
return Card(
|
|
color: AppColor.cardBg,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Section header with reset button
|
|
_buildCardHeader(
|
|
icon: Icons.tune,
|
|
title: 'Kontrol Manual',
|
|
subtitle: 'Kirim sinyal IR ke AC via ESP32',
|
|
trailing: IconButton(
|
|
icon: const Icon(Icons.settings, color: Colors.blueAccent, size: 20),
|
|
tooltip: 'Konfigurasi Ulang',
|
|
onPressed: () => _showResetConfirmation(setupProvider),
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// Power, Swing, & Control Mode (Switch) Button Row
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
flex: 5,
|
|
child: OutlinedButton.icon(
|
|
onPressed: () => acProvider.setPower(!ac.power),
|
|
icon: Icon(
|
|
ac.power ? Icons.power_off : Icons.power_settings_new,
|
|
color: ac.power ? Colors.red : Colors.green,
|
|
size: 18,
|
|
),
|
|
label: Text(
|
|
ac.power ? 'Matikan' : 'Nyalakan',
|
|
style: TextStyle(
|
|
color: ac.power ? Colors.red : Colors.green,
|
|
fontSize: 11,
|
|
),
|
|
),
|
|
style: OutlinedButton.styleFrom(
|
|
side: BorderSide(
|
|
color: ac.power
|
|
? Colors.red.withValues(alpha: 0.5)
|
|
: Colors.green.withValues(alpha: 0.5),
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 6),
|
|
Expanded(
|
|
flex: 5,
|
|
child: OutlinedButton.icon(
|
|
onPressed: () => acProvider.toggleSwing(),
|
|
icon: Icon(
|
|
Icons.swap_vert,
|
|
color: ac.swing ? Colors.blueAccent : Colors.white60,
|
|
size: 18,
|
|
),
|
|
label: Text(
|
|
ac.swing ? 'Swing ON' : 'Swing OFF',
|
|
style: TextStyle(
|
|
color: ac.swing ? Colors.blueAccent : Colors.white60,
|
|
fontSize: 11,
|
|
),
|
|
),
|
|
style: OutlinedButton.styleFrom(
|
|
side: BorderSide(
|
|
color: ac.swing
|
|
? Colors.blueAccent.withValues(alpha: 0.5)
|
|
: Colors.white30,
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 6),
|
|
Expanded(
|
|
flex: 4,
|
|
child: Container(
|
|
height: 44, // Match the height of OutlinedButton with vertical padding 10
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
ac.controlMode == 'auto' ? 'Auto' : 'Manual',
|
|
style: TextStyle(
|
|
color: ac.controlMode == 'auto' ? Colors.blueAccent : Colors.orangeAccent,
|
|
fontSize: 9,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 1),
|
|
SizedBox(
|
|
height: 20,
|
|
child: Transform.scale(
|
|
scale: 0.65,
|
|
child: Switch.adaptive(
|
|
value: ac.controlMode == 'auto',
|
|
activeThumbColor: Colors.blueAccent,
|
|
activeTrackColor: Colors.blueAccent.withValues(alpha: 0.3),
|
|
inactiveThumbColor: Colors.orangeAccent,
|
|
inactiveTrackColor: Colors.orangeAccent.withValues(alpha: 0.3),
|
|
onChanged: (bool value) {
|
|
acProvider.setControlMode(value ? 'auto' : 'manual');
|
|
},
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Mode & Fan cyclic buttons + Temp in a row
|
|
Row(
|
|
children: [
|
|
// Mode cyclic button
|
|
Expanded(
|
|
child: _buildCyclicButton(
|
|
label: 'Mode',
|
|
value: ac.mode,
|
|
icon: _modeIcon(ac.mode),
|
|
color: Colors.blueAccent,
|
|
onTap: () => acProvider.cycleMode(),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
// Fan cyclic button
|
|
Expanded(
|
|
child: _buildCyclicButton(
|
|
label: 'Kipas',
|
|
value: ac.fan,
|
|
icon: Icons.air,
|
|
color: Colors.tealAccent,
|
|
onTap: () => acProvider.cycleFan(),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
// Temperature control
|
|
Expanded(
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 4),
|
|
decoration: BoxDecoration(
|
|
color: Colors.black26,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Text(
|
|
'Suhu',
|
|
style: TextStyle(
|
|
color: Colors.grey,
|
|
fontSize: 9,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Row(
|
|
mainAxisAlignment:
|
|
MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
GestureDetector(
|
|
onTap: () =>
|
|
acProvider.setTemp(ac.temp - 1),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(3),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(
|
|
color: Colors.white30,
|
|
),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Icon(
|
|
Icons.remove,
|
|
size: 12,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
Text(
|
|
'${ac.temp}°',
|
|
style: const TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
GestureDetector(
|
|
onTap: () =>
|
|
acProvider.setTemp(ac.temp + 1),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(3),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(
|
|
color: Colors.white30,
|
|
),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Icon(
|
|
Icons.add,
|
|
size: 12,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ====================== RESET CONFIRMATION ======================
|
|
|
|
void _showResetConfirmation(ACSetupProvider setupProvider) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
backgroundColor: AppColor.cardBg,
|
|
title: const Text(
|
|
'Konfigurasi Ulang AC',
|
|
style: TextStyle(color: Colors.white),
|
|
),
|
|
content: const Text(
|
|
'Konfigurasi saat ini akan dihapus dan Anda harus memilih merek AC lagi. Lanjutkan?',
|
|
style: TextStyle(color: Colors.white70),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(ctx).pop(),
|
|
child: const Text('Batal', style: TextStyle(color: Colors.grey)),
|
|
),
|
|
TextButton(
|
|
onPressed: () async {
|
|
Navigator.of(ctx).pop();
|
|
await setupProvider.clearConfig();
|
|
await setupProvider.startConfigMode();
|
|
},
|
|
child: const Text('Ya, Reset', style: TextStyle(color: Colors.redAccent)),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// ====================== CARD HEADER ======================
|
|
|
|
Widget _buildCardHeader({
|
|
required IconData icon,
|
|
required String title,
|
|
required String subtitle,
|
|
Widget? trailing,
|
|
}) {
|
|
return Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue.withValues(alpha: 0.1),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(icon, color: Colors.blue, size: 20),
|
|
),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: const TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
Text(
|
|
subtitle,
|
|
style: const TextStyle(fontSize: 11, color: Colors.grey),
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
?trailing,
|
|
],
|
|
);
|
|
}
|
|
|
|
// ====================== CYCLIC BUTTON ======================
|
|
|
|
Widget _buildCyclicButton({
|
|
required String label,
|
|
required String value,
|
|
required IconData icon,
|
|
required Color color,
|
|
required VoidCallback onTap,
|
|
}) {
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: Colors.black26,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: color.withValues(alpha: 0.4)),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Icon(icon, color: color, size: 16),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
label,
|
|
style: const TextStyle(color: Colors.grey, fontSize: 9),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
value,
|
|
style: TextStyle(
|
|
color: color,
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Icon(Icons.refresh, color: color.withValues(alpha: 0.5), size: 10),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
IconData _modeIcon(String mode) {
|
|
switch (mode.toUpperCase()) {
|
|
case 'COOL':
|
|
return Icons.ac_unit;
|
|
case 'DRY':
|
|
return Icons.water_drop_outlined;
|
|
case 'FAN':
|
|
return Icons.air;
|
|
case 'HEAT':
|
|
return Icons.local_fire_department;
|
|
case 'AUTO':
|
|
return Icons.auto_mode;
|
|
default:
|
|
return Icons.ac_unit;
|
|
}
|
|
}
|
|
|
|
// ====================== SENSOR CARD ======================
|
|
|
|
Widget _buildSensorCard({
|
|
required String title,
|
|
required String value,
|
|
required String status,
|
|
required IconData icon,
|
|
required Color iconColor,
|
|
}) {
|
|
return Card(
|
|
color: AppColor.cardBg,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: iconColor.withValues(alpha: 0.1),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(icon, color: iconColor, size: 20),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
title,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(
|
|
fontSize: 13,
|
|
color: Colors.grey,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
value,
|
|
style: const TextStyle(
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
_buildStatusChip(status),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStatusChip(String status) {
|
|
Color color;
|
|
if (status == 'Sangat Baik' || status == 'Baik') {
|
|
color = AppColor.success;
|
|
} else if (status == 'Buruk') {
|
|
color = AppColor.danger;
|
|
} else {
|
|
color = AppColor.warning;
|
|
}
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: color.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: color.withValues(alpha: 0.3), width: 1),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 6,
|
|
height: 6,
|
|
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
|
|
),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
status.isEmpty ? 'Normal' : status,
|
|
style: TextStyle(
|
|
color: color,
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|