diff --git a/android/app/build.gradle b/android/app/build.gradle
index 11460c1..dbe8e5d 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -1,3 +1,9 @@
+plugins {
+ id "com.android.application"
+ id "kotlin-android"
+ id "dev.flutter.flutter-gradle-plugin"
+}
+
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
@@ -6,47 +12,27 @@ if (localPropertiesFile.exists()) {
}
}
-def flutterRoot = localProperties.getProperty('flutter.sdk')
-if (flutterRoot == null) {
- throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
-}
-
-def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
-if (flutterVersionCode == null) {
- flutterVersionCode = '1'
-}
-
-def flutterVersionName = localProperties.getProperty('flutter.versionName')
-if (flutterVersionName == null) {
- flutterVersionName = '1.0'
-}
-
-apply plugin: 'com.android.application'
-apply plugin: 'kotlin-android'
-apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode') ?: '1'
+def flutterVersionName = localProperties.getProperty('flutter.versionName') ?: '1.0'
android {
namespace "com.example.coffee_iot_flutter"
- compileSdkVersion 34
+ compileSdk 35
ndkVersion flutter.ndkVersion
compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
- jvmTarget = '1.8'
- }
-
- sourceSets {
- main.java.srcDirs += 'src/main/kotlin'
+ jvmTarget = '17'
}
defaultConfig {
applicationId "com.example.coffee_iot_flutter"
- minSdkVersion flutter.minSdkVersion
- targetSdkVersion 34
+ minSdk flutter.minSdkVersion
+ targetSdk 35
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
@@ -58,10 +44,15 @@ android {
}
}
-flutter {
- source '../..'
+// Force pin versi androidx agar kompatibel AGP 8.6.1
+configurations.all {
+ resolutionStrategy {
+ force 'androidx.browser:browser:1.8.0'
+ force 'androidx.core:core:1.15.0'
+ force 'androidx.core:core-ktx:1.15.0'
+ }
}
-dependencies {
- implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
-}
+flutter {
+ source '../..'
+}
\ No newline at end of file
diff --git a/android/build.gradle b/android/build.gradle
index 3aaaf31..9424a15 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -1,16 +1,3 @@
-buildscript {
- ext.kotlin_version = '1.8.22'
- repositories {
- google()
- mavenCentral()
- }
-
- dependencies {
- classpath 'com.android.tools.build:gradle:8.1.0'
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- }
-}
-
allprojects {
repositories {
google()
@@ -18,14 +5,16 @@ allprojects {
}
}
-rootProject.buildDir = '../build'
+rootProject.buildDir = "../build"
+
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
+
subprojects {
project.evaluationDependsOn(':app')
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
-}
+}
\ No newline at end of file
diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html
new file mode 100644
index 0000000..79f7ed5
--- /dev/null
+++ b/android/build/reports/problems/problems-report.html
@@ -0,0 +1,663 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Gradle Configuration Cache
+
+
+
+
+
+
+ Loading...
+
+
+
+
+
+
+
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
index e4ef43f..1ae95f1 100644
--- a/android/gradle/wrapper/gradle-wrapper.properties
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
\ No newline at end of file
diff --git a/android/settings.gradle b/android/settings.gradle
index 0612a7d..936c11d 100644
--- a/android/settings.gradle
+++ b/android/settings.gradle
@@ -1,17 +1,25 @@
-include ':app'
+pluginManagement {
+ def flutterSdkPath = {
+ def properties = new Properties()
+ file("local.properties").withInputStream { properties.load(it) }
+ def flutterSdkPath = properties.getProperty("flutter.sdk")
+ assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
+ return flutterSdkPath
+ }()
-def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
-def properties = new Properties()
+ includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
-if (localPropertiesFile.exists()) {
- localPropertiesFile.withReader("UTF-8") { reader ->
- properties.load(reader)
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
}
}
-def flutterSdkPath = properties.getProperty("flutter.sdk")
-if (flutterSdkPath == null) {
- throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+plugins {
+ id "com.android.application" version "8.6.1" apply false
+ id "org.jetbrains.kotlin.android" version "2.1.0" apply false
+ id "dev.flutter.flutter-plugin-loader" version "1.0.0"
}
-apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
+include ":app"
\ No newline at end of file
diff --git a/lib/main.dart b/lib/main.dart
index 6048732..65f47e4 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -14,7 +14,7 @@ void main() async {
// Initialize Supabase with the provided URL and Publishable Key
await Supabase.initialize(
url: 'https://ddmhzzegejbsshihzext.supabase.co',
- anonKey: 'sb_publishable_IJJ8fXD9CXfjMeu8vNJ8bw_NhWyTJKy',
+ anonKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRkbWh6emVnZWpic3NoaWh6ZXh0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzcyMjIyNzAsImV4cCI6MjA5Mjc5ODI3MH0._r-viIQLMXuNNdg4tcn7sJTBO9QqSpSlF7hk43hu2Ks',
);
runApp(
diff --git a/lib/screens/analytics_screen.dart b/lib/screens/analytics_screen.dart
index 5f76424..85eb099 100644
--- a/lib/screens/analytics_screen.dart
+++ b/lib/screens/analytics_screen.dart
@@ -34,9 +34,9 @@ class _AnalyticsScreenState extends State {
@override
Widget build(BuildContext context) {
- final args = ModalRoute.of(context)!.settings.arguments as Map;
- final String title = args['title'];
- final Color color = args['color'];
+ final args = (ModalRoute.of(context)?.settings.arguments as Map?) ?? {};
+ final String title = args['title'] ?? 'Grafik';
+ final Color color = args['color'] ?? Colors.green;
final String sensorKey = title.contains('Suhu') ? 'suhu' : title.contains('Kelembaban') ? 'kelembapan' : 'intensitas';
return Scaffold(
diff --git a/lib/screens/control_settings_screen.dart b/lib/screens/control_settings_screen.dart
index 5442062..3687bab 100644
--- a/lib/screens/control_settings_screen.dart
+++ b/lib/screens/control_settings_screen.dart
@@ -12,11 +12,11 @@ class ControlSettingsScreen extends StatefulWidget {
}
class _ControlSettingsScreenState extends State {
- int _operatingMode = 1; // 0 for Auto, 1 for Manual
+ int _operatingMode = 1; // 0 = otomatis, 1 = manual
final TextEditingController _tempMinController = TextEditingController();
final TextEditingController _tempMaxController = TextEditingController();
- final TextEditingController _humMinController = TextEditingController();
- final TextEditingController _humMaxController = TextEditingController();
+ final TextEditingController _humMinController = TextEditingController();
+ final TextEditingController _humMaxController = TextEditingController();
bool _isLoading = true;
@override
@@ -25,68 +25,107 @@ class _ControlSettingsScreenState extends State {
_loadSettings();
}
+ @override
+ void dispose() {
+ _tempMinController.dispose();
+ _tempMaxController.dispose();
+ _humMinController.dispose();
+ _humMaxController.dispose();
+ super.dispose();
+ }
+
Future _loadSettings() async {
try {
final prefs = await SharedPreferences.getInstance();
- final data = await SupabaseService().getBatasSensor();
- if (data != null) {
+ final data = await SupabaseService().getBatasSensor();
+ if (data != null && mounted) {
setState(() {
_tempMinController.text = data['suhu_min'].toString();
_tempMaxController.text = data['suhu_max'].toString();
- _humMinController.text = data['rh_min'].toString();
- _humMaxController.text = data['rh_max'].toString();
- // Load last saved mode from local memory
+ _humMinController.text = data['rh_min'].toString();
+ _humMaxController.text = data['rh_max'].toString();
_operatingMode = prefs.getInt('op_mode') ?? 1;
_isLoading = false;
});
}
} catch (e) {
debugPrint('Error loading settings: $e');
- setState(() => _isLoading = false);
+ if (mounted) setState(() => _isLoading = false);
}
}
- void _saveSettings() async {
- // 1. Validation: Prevent empty inputs
- if (_tempMinController.text.isEmpty || _tempMaxController.text.isEmpty ||
- _humMinController.text.isEmpty || _humMaxController.text.isEmpty) {
+ Future _saveSettings() async {
+ if (_tempMinController.text.isEmpty ||
+ _tempMaxController.text.isEmpty ||
+ _humMinController.text.isEmpty ||
+ _humMaxController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(content: Text('Semua kolom batas harus diisi!'), backgroundColor: Colors.orange),
+ const SnackBar(
+ content: Text('Semua kolom batas harus diisi!'),
+ backgroundColor: Colors.orange,
+ ),
);
return;
}
- final mqtt = Provider.of(context, listen: false);
+ final suhuMin = double.tryParse(_tempMinController.text) ?? 30;
+ final suhuMax = double.tryParse(_tempMaxController.text) ?? 45;
+ final rhMin = double.tryParse(_humMinController.text) ?? 60;
+ final rhMax = double.tryParse(_humMaxController.text) ?? 70;
+
+ // Validasi logika
+ if (suhuMin >= suhuMax) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('Suhu min harus lebih kecil dari suhu max!'),
+ backgroundColor: Colors.orange,
+ ),
+ );
+ return;
+ }
+ if (rhMin >= rhMax) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('RH min harus lebih kecil dari RH max!'),
+ backgroundColor: Colors.orange,
+ ),
+ );
+ return;
+ }
+
+ final mqtt = Provider.of(context, listen: false);
final prefs = await SharedPreferences.getInstance();
-
+
setState(() => _isLoading = true);
try {
- // 2. Save Mode to Local Memory
+ // Simpan mode ke local storage
await prefs.setInt('op_mode', _operatingMode);
- // 3. MQTT Publish
- mqtt.publish('coffee/config/temp_max', _tempMaxController.text);
- mqtt.publish('coffee/config/hum_max', _humMaxController.text);
- mqtt.publish('coffee/config/mode', _operatingMode == 0 ? "AUTO" : "MANUAL");
+ // Kirim batas sensor ke ESP32 via MQTT → Flow 5 simpan ke Supabase
+ mqtt.setBatasSensor(suhuMin, suhuMax, rhMin, rhMax);
+
+ // Kirim mode ke ESP32
+ mqtt.gantiMode(_operatingMode == 0 ? "otomatis" : "manual");
+
+ // Update Supabase langsung juga sebagai backup
+ await SupabaseService().updateBatasSensor(suhuMin, suhuMax, rhMin, rhMax);
- // 4. Supabase Update
- await SupabaseService().updateBatasSensor(
- double.parse(_tempMinController.text),
- double.parse(_tempMaxController.text),
- double.parse(_humMinController.text),
- double.parse(_humMaxController.text),
- );
-
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(content: Text('✅ Pengaturan Tersimpan & Terkirim')),
+ const SnackBar(
+ content: Text('✅ Pengaturan Tersimpan & Terkirim ke ESP32'),
+ backgroundColor: Colors.green,
+ ),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text('❌ Gagal: $e'), backgroundColor: Colors.red),
+ SnackBar(
+ content: Text('❌ Gagal: $e'),
+ backgroundColor: Colors.red,
+ ),
);
}
} finally {
@@ -99,84 +138,133 @@ class _ControlSettingsScreenState extends State {
return Scaffold(
backgroundColor: Colors.black,
body: SafeArea(
- child: _isLoading
- ? const Center(child: CircularProgressIndicator(color: Colors.green))
- : SingleChildScrollView(
- child: Padding(
- padding: const EdgeInsets.all(20.0),
- child: Column(
- children: [
- Row(
- children: [
- Expanded(child: _buildInputCard('Suhu Min (°C)', '32', _tempMinController)),
- const SizedBox(width: 10),
- Expanded(child: _buildInputCard('Suhu Max (°C)', '48', _tempMaxController)),
- ],
- ),
- const SizedBox(height: 20),
- Row(
- children: [
- Expanded(child: _buildInputCard('Kelembaban Min (%)', '65', _humMinController)),
- const SizedBox(width: 10),
- Expanded(child: _buildInputCard('Kelembaban Max (%)', '75', _humMaxController)),
- ],
- ),
- const SizedBox(height: 20),
- Container(
- padding: const EdgeInsets.all(20),
- decoration: BoxDecoration(
- color: const Color(0xFF1A1A1A),
- borderRadius: BorderRadius.circular(20),
- ),
+ child: _isLoading
+ ? const Center(
+ child: CircularProgressIndicator(color: Colors.green))
+ : SingleChildScrollView(
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- const Text('Mode Operasi', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
- const SizedBox(height: 10),
- RadioListTile(
- value: 0,
- groupValue: _operatingMode,
- onChanged: (val) => setState(() => _operatingMode = val!),
- title: const Text('Mode Otomatis', style: TextStyle(color: Colors.white)),
- activeColor: Colors.green,
- subtitle: const Text('Kipas bekerja otomatis berdasarkan sensor', style: TextStyle(color: Colors.grey, fontSize: 10)),
+ const Text(
+ 'Pengaturan',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 32,
+ fontWeight: FontWeight.bold),
),
- RadioListTile(
- value: 1,
- groupValue: _operatingMode,
- onChanged: (val) => setState(() => _operatingMode = val!),
- title: const Text('Mode Manual', style: TextStyle(color: Colors.white)),
- activeColor: Colors.green,
- subtitle: const Text('Kontrol penuh kipas melalui dashboard HP', style: TextStyle(color: Colors.grey, fontSize: 10)),
+ const Text(
+ 'Batas Sensor & Mode Operasi',
+ style: TextStyle(color: Colors.grey, fontSize: 16),
),
+ const SizedBox(height: 30),
+
+ // ── Batas Suhu ──
+ Row(
+ children: [
+ Expanded(child: _buildInputCard(
+ 'Suhu Min (°C)', '30', _tempMinController)),
+ const SizedBox(width: 10),
+ Expanded(child: _buildInputCard(
+ 'Suhu Max (°C)', '45', _tempMaxController)),
+ ],
+ ),
+ const SizedBox(height: 15),
+
+ // ── Batas RH ──
+ Row(
+ children: [
+ Expanded(child: _buildInputCard(
+ 'RH Min (%)', '60', _humMinController)),
+ const SizedBox(width: 10),
+ Expanded(child: _buildInputCard(
+ 'RH Max (%)', '70', _humMaxController)),
+ ],
+ ),
+ const SizedBox(height: 20),
+
+ // ── Mode Operasi ──
+ Container(
+ padding: const EdgeInsets.all(20),
+ decoration: BoxDecoration(
+ color: const Color(0xFF1A1A1A),
+ borderRadius: BorderRadius.circular(20),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text('Mode Operasi',
+ style: TextStyle(
+ color: Colors.white,
+ fontWeight: FontWeight.bold,
+ fontSize: 16)),
+ const SizedBox(height: 10),
+ RadioListTile(
+ value: 0,
+ groupValue: _operatingMode,
+ onChanged: (val) =>
+ setState(() => _operatingMode = val!),
+ title: const Text('Mode Otomatis',
+ style: TextStyle(color: Colors.white)),
+ activeColor: Colors.green,
+ subtitle: const Text(
+ 'Kipas bekerja otomatis berdasarkan sensor',
+ style: TextStyle(
+ color: Colors.grey, fontSize: 10),
+ ),
+ ),
+ RadioListTile(
+ value: 1,
+ groupValue: _operatingMode,
+ onChanged: (val) =>
+ setState(() => _operatingMode = val!),
+ title: const Text('Mode Manual',
+ style: TextStyle(color: Colors.white)),
+ activeColor: Colors.green,
+ subtitle: const Text(
+ 'Kontrol penuh kipas melalui dashboard HP',
+ style: TextStyle(
+ color: Colors.grey, fontSize: 10),
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // ── Tombol Simpan ──
+ SizedBox(
+ width: double.infinity,
+ height: 55,
+ child: ElevatedButton(
+ onPressed: _saveSettings,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.green,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(15),
+ ),
+ ),
+ child: const Text(
+ 'Simpan & Kirim ke ESP32',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ color: Colors.white),
+ ),
+ ),
+ ),
+ const SizedBox(height: 100),
],
),
),
- const SizedBox(height: 20),
- SizedBox(
- width: double.infinity,
- height: 55,
- child: ElevatedButton(
- onPressed: _saveSettings,
- style: ElevatedButton.styleFrom(
- backgroundColor: Colors.green,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(15),
- ),
- ),
- child: const Text('Simpan Pengaturan', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white)),
- ),
- ),
- const SizedBox(height: 100),
- ],
- ),
- ),
- ),
+ ),
),
);
}
- Widget _buildInputCard(String title, String hint, TextEditingController controller) {
+ Widget _buildInputCard(
+ String title, String hint, TextEditingController controller) {
return Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
@@ -186,7 +274,11 @@ class _ControlSettingsScreenState extends State {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Text(title, style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold)),
+ Text(title,
+ style: const TextStyle(
+ color: Colors.white,
+ fontSize: 12,
+ fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
TextField(
controller: controller,
@@ -208,4 +300,4 @@ class _ControlSettingsScreenState extends State {
),
);
}
-}
+}
\ No newline at end of file
diff --git a/lib/screens/image_result_screen.dart b/lib/screens/image_result_screen.dart
index 493e8ec..f684684 100644
--- a/lib/screens/image_result_screen.dart
+++ b/lib/screens/image_result_screen.dart
@@ -46,14 +46,34 @@ class ImageResultScreen extends StatelessWidget {
color: const Color(0xFF1A1A1A),
borderRadius: BorderRadius.circular(20),
),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- const Icon(Icons.image, size: 80, color: Colors.grey),
- const SizedBox(height: 10),
- Text('Citra #${args['id']}', style: const TextStyle(color: Colors.grey)),
- ],
- ),
+ clipBehavior: Clip.hardEdge,
+ child: args['url_foto'] != null && args['url_foto'].toString().isNotEmpty
+ ? Image.network(
+ args['url_foto'],
+ fit: BoxFit.cover,
+ loadingBuilder: (context, child, progress) {
+ if (progress == null) return child;
+ return const Center(
+ child: CircularProgressIndicator(color: Colors.green),
+ );
+ },
+ errorBuilder: (context, error, stack) => Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Icon(Icons.broken_image, size: 80, color: Colors.grey),
+ Text('Citra #${args['id']}',
+ style: const TextStyle(color: Colors.grey)),
+ ],
+ ),
+ )
+ : Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Icon(Icons.image, size: 80, color: Colors.grey),
+ Text('Citra #${args['id']}',
+ style: const TextStyle(color: Colors.grey)),
+ ],
+ ),
),
const SizedBox(height: 20),
diff --git a/lib/screens/login_screen.dart b/lib/screens/login_screen.dart
index 20943bf..cf708cb 100644
--- a/lib/screens/login_screen.dart
+++ b/lib/screens/login_screen.dart
@@ -12,6 +12,7 @@ class _LoginScreenState extends State {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isLoading = false;
+ bool _obscurePassword = true; // Tambahan: State untuk mata password
Future _handleLogin() async {
setState(() => _isLoading = true);
@@ -80,13 +81,24 @@ class _LoginScreenState extends State {
const Divider(color: Colors.grey),
TextField(
controller: _passwordController,
- obscureText: true,
+ obscureText: _obscurePassword, // Menggunakan variabel state
style: const TextStyle(color: Colors.white),
- decoration: const InputDecoration(
- prefixIcon: Icon(Icons.lock, color: Colors.green),
- suffixIcon: Icon(Icons.visibility, color: Colors.grey),
+ decoration: InputDecoration(
+ prefixIcon: const Icon(Icons.lock, color: Colors.green),
+ // Menggunakan IconButton untuk aksi klik
+ suffixIcon: IconButton(
+ icon: Icon(
+ _obscurePassword ? Icons.visibility : Icons.visibility_off,
+ color: Colors.grey,
+ ),
+ onPressed: () {
+ setState(() {
+ _obscurePassword = !_obscurePassword;
+ });
+ },
+ ),
hintText: 'Password',
- hintStyle: TextStyle(color: Colors.grey),
+ hintStyle: const TextStyle(color: Colors.grey),
border: InputBorder.none,
),
),
@@ -127,4 +139,4 @@ class _LoginScreenState extends State {
),
);
}
-}
+}
\ No newline at end of file
diff --git a/lib/screens/monitoring_screen.dart b/lib/screens/monitoring_screen.dart
index 3bc9ac1..3ffa18f 100644
--- a/lib/screens/monitoring_screen.dart
+++ b/lib/screens/monitoring_screen.dart
@@ -23,30 +23,9 @@ class _MonitoringScreenState extends State {
Future _initData() async {
setState(() => _isInitialLoading = true);
await _loadLimits();
- await _loadLastSensorData();
if (mounted) setState(() => _isInitialLoading = false);
}
- Future _loadLastSensorData() async {
- try {
- final lastLog = await SupabaseService().getLatestLog();
- if (lastLog != null && mounted) {
- final mqtt = Provider.of(context, listen: false);
- // Update local MQTT service state with last DB values if currently "0" or default
- // This ensures the user sees the last known data immediately
- mqtt.updateFromLastLog(
- lastLog['suhu'].toString(),
- lastLog['kelembapan'].toString(),
- lastLog['intensitas'].toString(),
- lastLog['kipas1'] == 'ON',
- lastLog['kipas2'] == 'ON'
- );
- }
- } catch (e) {
- debugPrint('Error loading last log: $e');
- }
- }
-
Future _loadLimits() async {
try {
final data = await SupabaseService().getBatasSensor();
@@ -64,180 +43,299 @@ class _MonitoringScreenState extends State {
@override
Widget build(BuildContext context) {
final mqtt = Provider.of(context);
- final curTemp = double.tryParse(mqtt.temp) ?? 0;
- final curHum = double.tryParse(mqtt.humidity) ?? 0;
+ final curTemp = double.tryParse(mqtt.suhu) ?? 0;
+ final curHum = double.tryParse(mqtt.kelembapan) ?? 0;
return Scaffold(
backgroundColor: Colors.black,
body: SafeArea(
- child: _isInitialLoading
- ? const Center(child: CircularProgressIndicator(color: Colors.green))
- : RefreshIndicator(
- onRefresh: _initData,
- color: Colors.green,
- child: SingleChildScrollView(
- physics: const AlwaysScrollableScrollPhysics(),
- child: Padding(
- padding: const EdgeInsets.all(20.0),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- const Text(
- 'Monitoring',
- style: TextStyle(color: Colors.white, fontSize: 32, fontWeight: FontWeight.bold),
- ),
- const Text(
- 'Sistem Pengeringan Kopi',
- style: TextStyle(color: Colors.grey, fontSize: 16),
- ),
- const SizedBox(height: 5),
- Row(
- children: [
- Container(
- width: 8,
- height: 8,
- decoration: BoxDecoration(
- shape: BoxShape.circle,
- color: mqtt.client?.connectionStatus?.state == MqttConnectionState.connected
- ? Colors.green : Colors.red,
+ child: _isInitialLoading
+ ? const Center(
+ child: CircularProgressIndicator(color: Colors.green))
+ : RefreshIndicator(
+ onRefresh: _initData,
+ color: Colors.green,
+ child: SingleChildScrollView(
+ physics: const AlwaysScrollableScrollPhysics(),
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // ── Header ──
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ 'Monitoring',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 32,
+ fontWeight: FontWeight.bold),
),
- ),
- const SizedBox(width: 8),
- Text(
- mqtt.client?.connectionStatus?.state == MqttConnectionState.connected
- ? 'Connected' : 'Disconnected',
- style: TextStyle(
- color: mqtt.client?.connectionStatus?.state == MqttConnectionState.connected
- ? Colors.green : Colors.red,
- fontSize: 12,
+ const Text(
+ 'Sistem Pengeringan Kopi',
+ style: TextStyle(
+ color: Colors.grey, fontSize: 16),
),
- ),
- ],
- ),
- ],
- ),
- const CircleAvatar(
- backgroundColor: Color(0xFF1A1A1A),
- radius: 25,
- child: Icon(Icons.person, color: Colors.green),
- ),
- ],
- ),
- const SizedBox(height: 30),
- GridView.count(
- shrinkWrap: true,
- physics: const NeverScrollableScrollPhysics(),
- crossAxisCount: 2,
- crossAxisSpacing: 15,
- mainAxisSpacing: 15,
- children: [
- _buildSensorCard(
- 'Suhu',
- '${mqtt.temp}°C',
- Icons.thermostat,
- curTemp > mqtt.maxTemp ? Colors.red : Colors.redAccent,
- isAlert: curTemp > mqtt.maxTemp,
- onTap: () => Navigator.pushNamed(context, '/analytics', arguments: {'title': 'Grafik Suhu (°C)', 'color': Colors.redAccent, 'value': '${mqtt.temp}°C'}),
- ),
- _buildSensorCard(
- 'Kelembaban',
- '${mqtt.humidity}%',
- Icons.water_drop,
- curHum > mqtt.maxHum ? Colors.red : Colors.cyan,
- isAlert: curHum > mqtt.maxHum,
- onTap: () => Navigator.pushNamed(context, '/analytics', arguments: {'title': 'Grafik Kelembaban (%)', 'color': Colors.cyan, 'value': '${mqtt.humidity}%'}),
- ),
- _buildSensorCard(
- 'Cahaya',
- '${mqtt.light} Lux',
- Icons.wb_sunny,
- Colors.orange,
- onTap: () => Navigator.pushNamed(context, '/analytics', arguments: {'title': 'Grafik Cahaya (Lux)', 'color': Colors.orange, 'value': '${mqtt.light} Lux'}),
- ),
- _buildControlCard(
- 'Intake',
- mqtt.isIntakeOn,
- (val) async {
- final status = val ? 'ON' : 'OFF';
- mqtt.publish('coffee/intake', status);
- await SupabaseService().logFanAction(status, mqtt.isExhaustOn ? 'ON' : 'OFF', curTemp, curHum, double.tryParse(mqtt.light) ?? 0);
- },
- mqtt,
- ),
- _buildControlCard(
- 'Exhaust',
- mqtt.isExhaustOn,
- (val) async {
- final status = val ? 'ON' : 'OFF';
- mqtt.publish('coffee/exhaust', status);
- await SupabaseService().logFanAction(mqtt.isIntakeOn ? 'ON' : 'OFF', status, curTemp, curHum, double.tryParse(mqtt.light) ?? 0);
- },
- mqtt,
- ),
- ],
- ),
- if (curTemp > mqtt.maxTemp || curHum > mqtt.maxHum)
- Container(
- margin: const EdgeInsets.only(top: 20),
- padding: const EdgeInsets.all(15),
- decoration: BoxDecoration(
- color: Colors.red.withOpacity(0.2),
- borderRadius: BorderRadius.circular(15),
- border: Border.all(color: Colors.red),
- ),
- child: Row(
- children: const [
- Icon(Icons.warning_amber_rounded, color: Colors.red),
- SizedBox(width: 10),
- Expanded(
- child: Text(
- 'Peringatan: Parameter pengeringan melewati batas aman!',
- style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold, fontSize: 12),
+ const SizedBox(height: 5),
+ Row(
+ children: [
+ Container(
+ width: 8,
+ height: 8,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ color: mqtt.client?.connectionStatus
+ ?.state ==
+ MqttConnectionState.connected
+ ? Colors.green
+ : Colors.red,
+ ),
+ ),
+ const SizedBox(width: 8),
+ Text(
+ mqtt.client?.connectionStatus?.state ==
+ MqttConnectionState.connected
+ ? 'Connected'
+ : 'Disconnected',
+ style: TextStyle(
+ color: mqtt.client?.connectionStatus
+ ?.state ==
+ MqttConnectionState.connected
+ ? Colors.green
+ : Colors.red,
+ fontSize: 12,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ const CircleAvatar(
+ backgroundColor: Color(0xFF1A1A1A),
+ radius: 25,
+ child:
+ Icon(Icons.person, color: Colors.green),
+ ),
+ ],
+ ),
+ const SizedBox(height: 10),
+
+ // ── Timestamp ──
+ Text(
+ 'Update: ${mqtt.timestamp}',
+ style: const TextStyle(
+ color: Colors.grey, fontSize: 11),
+ ),
+ const SizedBox(height: 20),
+
+ // ── Mode Badge ──
+ Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12, vertical: 6),
+ decoration: BoxDecoration(
+ color: mqtt.mode == 'manual'
+ ? Colors.orange.withOpacity(0.2)
+ : Colors.green.withOpacity(0.2),
+ borderRadius: BorderRadius.circular(20),
+ border: Border.all(
+ color: mqtt.mode == 'manual'
+ ? Colors.orange
+ : Colors.green,
),
),
- ],
- ),
+ child: Text(
+ 'Mode: ${mqtt.mode.toUpperCase()}',
+ style: TextStyle(
+ color: mqtt.mode == 'manual'
+ ? Colors.orange
+ : Colors.green,
+ fontSize: 12,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ const SizedBox(height: 20),
+
+ // ── Grid Sensor + Kontrol ──
+ GridView.count(
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ crossAxisCount: 2,
+ crossAxisSpacing: 15,
+ mainAxisSpacing: 15,
+ children: [
+ _buildSensorCard(
+ 'Suhu',
+ '${mqtt.suhu}°C',
+ Icons.thermostat,
+ curTemp > mqtt.maxSuhu
+ ? Colors.red
+ : Colors.redAccent,
+ isAlert: curTemp > mqtt.maxSuhu,
+ onTap: () => Navigator.pushNamed(
+ context,
+ '/analytics',
+ arguments: {
+ 'title': 'Grafik Suhu (°C)',
+ 'color': Colors.redAccent,
+ 'value': '${mqtt.suhu}°C',
+ },
+ ),
+ ),
+ _buildSensorCard(
+ 'Kelembaban',
+ '${mqtt.kelembapan}%',
+ Icons.water_drop,
+ curHum > mqtt.maxRh
+ ? Colors.red
+ : Colors.cyan,
+ isAlert: curHum > mqtt.maxRh,
+ onTap: () => Navigator.pushNamed(
+ context,
+ '/analytics',
+ arguments: {
+ 'title': 'Grafik Kelembaban (%)',
+ 'color': Colors.cyan,
+ 'value': '${mqtt.kelembapan}%',
+ },
+ ),
+ ),
+ _buildSensorCard(
+ 'Cahaya',
+ '${mqtt.intensitas} Lux',
+ Icons.wb_sunny,
+ Colors.orange,
+ onTap: () => Navigator.pushNamed(
+ context,
+ '/analytics',
+ arguments: {
+ 'title': 'Grafik Cahaya (Lux)',
+ 'color': Colors.orange,
+ 'value': '${mqtt.intensitas} Lux',
+ },
+ ),
+ ),
+ _buildControlCard(
+ 'Kipas 1 (Exhaust)',
+ mqtt.isKipas1On,
+ (val) async {
+ mqtt.perintahKipas("1", val);
+ await SupabaseService().logFanAction(
+ val ? 'ON' : 'OFF',
+ mqtt.isKipas2On ? 'ON' : 'OFF',
+ curTemp,
+ curHum,
+ double.tryParse(mqtt.intensitas) ?? 0,
+ );
+ },
+ mqtt,
+ ),
+ _buildControlCard(
+ 'Kipas 2 (Intake)',
+ mqtt.isKipas2On,
+ (val) async {
+ mqtt.perintahKipas("2", val);
+ await SupabaseService().logFanAction(
+ mqtt.isKipas1On ? 'ON' : 'OFF',
+ val ? 'ON' : 'OFF',
+ curTemp,
+ curHum,
+ double.tryParse(mqtt.intensitas) ?? 0,
+ );
+ },
+ mqtt,
+ ),
+ ],
+ ),
+
+ // ── Alert Banner ──
+ if (curTemp > mqtt.maxSuhu || curHum > mqtt.maxRh)
+ Container(
+ margin: const EdgeInsets.only(top: 20),
+ padding: const EdgeInsets.all(15),
+ decoration: BoxDecoration(
+ color: Colors.red.withOpacity(0.2),
+ borderRadius: BorderRadius.circular(15),
+ border: Border.all(color: Colors.red),
+ ),
+ child: const Row(
+ children: [
+ Icon(Icons.warning_amber_rounded,
+ color: Colors.red),
+ SizedBox(width: 10),
+ Expanded(
+ child: Text(
+ 'Peringatan: Parameter pengeringan melewati batas aman!',
+ style: TextStyle(
+ color: Colors.red,
+ fontWeight: FontWeight.bold,
+ fontSize: 12),
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 20),
+ ],
),
- ],
+ ),
+ ),
),
- ),
- ),
- ),
),
);
}
- Widget _buildSensorCard(String title, String value, IconData icon, Color color, {bool isAlert = false, VoidCallback? onTap}) {
+ Widget _buildSensorCard(
+ String title,
+ String value,
+ IconData icon,
+ Color color, {
+ bool isAlert = false,
+ VoidCallback? onTap,
+ }) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
- color: isAlert ? Colors.red.withOpacity(0.1) : const Color(0xFF1A1A1A),
+ color: isAlert
+ ? Colors.red.withOpacity(0.1)
+ : const Color(0xFF1A1A1A),
borderRadius: BorderRadius.circular(20),
- border: isAlert ? Border.all(color: Colors.red, width: 2) : null,
+ border:
+ isAlert ? Border.all(color: Colors.red, width: 2) : null,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color, size: 40),
const SizedBox(height: 10),
- Text(title, style: const TextStyle(color: Colors.grey, fontSize: 14)),
+ Text(title,
+ style:
+ const TextStyle(color: Colors.grey, fontSize: 14)),
const SizedBox(height: 5),
- Text(value, style: const TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold)),
+ Text(value,
+ style: const TextStyle(
+ color: Colors.white,
+ fontSize: 22,
+ fontWeight: FontWeight.bold)),
],
),
),
);
}
- Widget _buildControlCard(String title, bool isOn, Function(bool) onChanged, MqttService mqtt) {
+ Widget _buildControlCard(
+ String title,
+ bool isOn,
+ Function(bool) onChanged,
+ MqttService mqtt,
+ ) {
return Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
@@ -249,20 +347,40 @@ class _MonitoringScreenState extends State {
children: [
Row(
children: [
- const Icon(Icons.settings_input_component, color: Colors.green, size: 20),
+ const Icon(Icons.settings_input_component,
+ color: Colors.green, size: 20),
const SizedBox(width: 8),
- Text(title, style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold)),
+ Expanded(
+ child: Text(title,
+ style: const TextStyle(
+ color: Colors.white,
+ fontSize: 13,
+ fontWeight: FontWeight.bold)),
+ ),
],
),
const Spacer(),
- Text('Mode: ${mqtt.currentMode}', style: const TextStyle(color: Colors.green, fontSize: 9, fontWeight: FontWeight.bold)),
+ Text(
+ 'Mode: ${mqtt.mode.toUpperCase()}',
+ style: const TextStyle(
+ color: Colors.green,
+ fontSize: 9,
+ fontWeight: FontWeight.bold),
+ ),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- Text(isOn ? 'ON' : 'OFF', style: const TextStyle(color: Colors.grey, fontSize: 12)),
+ Text(isOn ? 'ON' : 'OFF',
+ style: const TextStyle(
+ color: Colors.grey, fontSize: 12)),
SizedBox(
height: 30,
- child: Switch(value: isOn, onChanged: onChanged, activeColor: Colors.green),
+ child: Switch(
+ value: isOn,
+ onChanged:
+ mqtt.mode == 'manual' ? onChanged : null,
+ activeColor: Colors.green,
+ ),
),
],
),
@@ -270,4 +388,4 @@ class _MonitoringScreenState extends State {
),
);
}
-}
+}
\ No newline at end of file
diff --git a/lib/screens/recording_screen.dart b/lib/screens/recording_screen.dart
index 89e6a38..b55fb39 100644
--- a/lib/screens/recording_screen.dart
+++ b/lib/screens/recording_screen.dart
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/mqtt_service.dart';
import '../services/supabase_service.dart';
+import 'package:supabase_flutter/supabase_flutter.dart';
class RecordingScreen extends StatefulWidget {
const RecordingScreen({super.key});
@@ -14,43 +15,91 @@ class _RecordingScreenState extends State {
final TextEditingController _intervalController = TextEditingController();
List