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> _gallery = []; bool _isLoading = true; + bool _isRecording = false; + + late final RealtimeChannel _channel; @override void initState() { super.initState(); _loadData(); + + // 🔥 REALTIME LISTENER SUPABASE (AUTO REFRESH GALERI) + _channel = Supabase.instance.client + .channel('realtime-foto') + .onPostgresChanges( + event: PostgresChangeEvent.insert, + schema: 'public', + table: 'foto_dataset', + callback: (payload) { + _loadData(); // reload gallery otomatis saat foto baru masuk + }, + ) + .subscribe(); + } + + @override + void dispose() { + _intervalController.dispose(); + + // 🔥 MATIKAN REALTIME LISTENER + Supabase.instance.client.removeChannel(_channel); + + super.dispose(); } Future _loadData() async { + setState(() => _isLoading = true); try { final interval = await SupabaseService().getIntervalSetting(); - final images = await SupabaseService().getFotoDataset(); - setState(() { - _intervalController.text = interval.toString(); - _gallery = images; - _isLoading = false; - }); + final images = await SupabaseService().getFotoDataset(); + if (mounted) { + setState(() { + _intervalController.text = interval.toString(); + _gallery = images; + _isLoading = false; + }); + } } catch (e) { debugPrint('Error loading recording data: $e'); - setState(() => _isLoading = false); + if (mounted) setState(() => _isLoading = false); } } - void _controlRecording(bool start) async { + Future _controlRecording(bool start) async { final mqtt = Provider.of(context, listen: false); final intervalVal = int.tryParse(_intervalController.text) ?? 30; + if (intervalVal <= 0) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Interval harus lebih dari 0 menit!'), + backgroundColor: Colors.orange, + ), + ); + return; + } + if (start) { - mqtt.publish('coffee/record/interval', intervalVal.toString()); - mqtt.publish('coffee/record/cmd', 'START'); + // Simpan interval ke Supabase await SupabaseService().updateIntervalSetting(intervalVal); + // Kirim interval ke ESP32 via MQTT + mqtt.setIntervalFoto(intervalVal); + // Trigger foto pertama langsung + mqtt.triggerFoto(); + setState(() => _isRecording = true); } else { - mqtt.publish('coffee/record/cmd', 'STOP'); + setState(() => _isRecording = false); } if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(start ? 'Perekaman Dimulai' : 'Perekaman Berhenti')), + SnackBar( + content: Text(start + ? '📸 Perekaman dimulai — interval ${intervalVal} menit' + : '⏹ Perekaman dihentikan'), + backgroundColor: start ? Colors.green : Colors.red, + ), ); } } @@ -60,119 +109,230 @@ class _RecordingScreenState extends State { return Scaffold( backgroundColor: Colors.black, body: SafeArea( - child: _isLoading - ? const Center(child: CircularProgressIndicator(color: Colors.green)) - : Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - 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)) + : Padding( + padding: const EdgeInsets.all(20.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('Interval Waktu (menit)', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), - const SizedBox(height: 10), - TextField( - controller: _intervalController, - keyboardType: TextInputType.number, - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - hintText: '30', - hintStyle: const TextStyle(color: Colors.grey), - filled: true, - fillColor: Colors.black, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(15), - borderSide: const BorderSide(color: Colors.grey), - ), + // ── Header ── + const Text( + 'Perekaman', + style: TextStyle( + color: Colors.white, + fontSize: 32, + fontWeight: FontWeight.bold), + ), + const Text( + 'Dataset Citra Kopi', + style: TextStyle(color: Colors.grey, fontSize: 16), + ), + const SizedBox(height: 20), + + // ── Panel Kontrol ── + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: const Color(0xFF1A1A1A), + borderRadius: BorderRadius.circular(20), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Interval Waktu (menit)', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + TextField( + controller: _intervalController, + keyboardType: TextInputType.number, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + hintText: '30', + hintStyle: + const TextStyle(color: Colors.grey), + filled: true, + fillColor: Colors.black, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: + const BorderSide(color: Colors.grey), + ), + ), + ), + const SizedBox(height: 15), + + // Status perekaman + if (_isRecording) + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.green.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.green), + ), + child: const Row( + children: [ + Icon(Icons.circle, + color: Colors.green, size: 10), + SizedBox(width: 8), + Text('Perekaman berjalan...', + style: TextStyle( + color: Colors.green, + fontSize: 12)), + ], + ), + ), + const SizedBox(height: 10), + + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: _isRecording + ? null + : () => _controlRecording(true), + icon: const Icon(Icons.play_arrow, + color: Colors.white), + label: const Text('Mulai', + style: + TextStyle(color: Colors.white)), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + disabledBackgroundColor: + Colors.green.withOpacity(0.3), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(12), + ), + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: ElevatedButton.icon( + onPressed: !_isRecording + ? null + : () => _controlRecording(false), + icon: const Icon(Icons.stop, + color: Colors.white), + label: const Text('Berhenti', + style: + TextStyle(color: Colors.white)), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.redAccent, + disabledBackgroundColor: + Colors.redAccent.withOpacity(0.3), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(12), + ), + ), + ), + ), + ], + ), + ], ), ), const SizedBox(height: 20), - SizedBox( - width: double.infinity, - height: 50, - child: ElevatedButton( - onPressed: () => _controlRecording(true), - style: ElevatedButton.styleFrom(backgroundColor: Colors.green), - child: const Text('Mulai Perekaman', style: TextStyle(color: Colors.white)), - ), + + // ── Header Galeri ── + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Galeri Citra Dataset', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold), + ), + IconButton( + icon: const Icon(Icons.refresh, + color: Colors.green), + onPressed: _loadData, + ), + ], ), const SizedBox(height: 10), - SizedBox( - width: double.infinity, - height: 50, - child: ElevatedButton( - onPressed: () => _controlRecording(false), - style: ElevatedButton.styleFrom(backgroundColor: Colors.redAccent), - child: const Text('Berhenti Perekaman', style: TextStyle(color: Colors.white)), - ), + + // ── Grid Galeri ── + Expanded( + child: _gallery.isEmpty + ? const Center( + child: Text( + 'Belum ada data citra', + style: TextStyle(color: Colors.grey), + ), + ) + : GridView.builder( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 10, + mainAxisSpacing: 10, + ), + itemCount: _gallery.length, + itemBuilder: (context, index) { + final item = _gallery[index]; + return GestureDetector( + onTap: () => Navigator.pushNamed( + context, + '/image_result', + arguments: { + 'id': item['id'].toString(), + 'date': item['timestamp'] + ?.split(' ')[0] ?? + '', + 'time': item['timestamp'] + ?.split(' ')[1] ?? + '', + 'temp': '${item['suhu']} °C', + 'humidity': + '${item['kelembapan']} %', + 'light': + '${item['intensitas']} Lux', + 'location': 'Gudang Pengering', + 'intake': item['kipas1'] ?? 'OFF', + 'exhaust': item['kipas2'] ?? 'OFF', + 'url_foto': item['url_foto'] ?? '', + }, + ), + child: Container( + decoration: BoxDecoration( + color: const Color(0xFF1A1A1A), + borderRadius: + BorderRadius.circular(15), + image: item['url_foto'] != null + ? DecorationImage( + image: NetworkImage( + item['url_foto']), + fit: BoxFit.cover, + ) + : null, + ), + child: item['url_foto'] == null + ? const Center( + child: Icon(Icons.image, + color: Colors.grey, + size: 50), + ) + : null, + ), + ); + }, + ), ), ], ), ), - const SizedBox(height: 30), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('Galeri Citra Dataset', style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)), - IconButton( - icon: const Icon(Icons.refresh, color: Colors.green), - onPressed: _loadData, - ) - ], - ), - const SizedBox(height: 20), - Expanded( - child: _gallery.isEmpty - ? const Center(child: Text('Belum ada data citra', style: TextStyle(color: Colors.grey))) - : GridView.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - crossAxisSpacing: 10, - mainAxisSpacing: 10, - ), - itemCount: _gallery.length, - itemBuilder: (context, index) { - final item = _gallery[index]; - return GestureDetector( - onTap: () => Navigator.pushNamed(context, '/image_result', arguments: { - 'id': item['id'].toString(), - 'date': item['timestamp']?.split(' ')[0] ?? '', - 'time': item['timestamp']?.split(' ')[1] ?? '', - 'temp': '${item['suhu']} °C', - 'humidity': '${item['kelembapan']} %', - 'light': '${item['intensitas']} Lux', - 'location': 'Gudang Pengering', - 'intake': item['kipas1'] ?? 'OFF', - 'exhaust': item['kipas2'] ?? 'OFF' - }), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFF1A1A1A), - borderRadius: BorderRadius.circular(15), - image: item['url_foto'] != null ? DecorationImage( - image: NetworkImage(item['url_foto']), - fit: BoxFit.cover, - ) : null, - ), - child: item['url_foto'] == null - ? const Icon(Icons.image, color: Colors.grey, size: 50) - : null, - ), - ); - }, - ), - ), - ], - ), - ), ), ); } -} +} \ No newline at end of file diff --git a/lib/screens/register_screen.dart b/lib/screens/register_screen.dart index 48f94d2..6717e62 100644 --- a/lib/screens/register_screen.dart +++ b/lib/screens/register_screen.dart @@ -13,6 +13,7 @@ class _RegisterScreenState extends State { final _emailController = TextEditingController(); final _passwordController = TextEditingController(); bool _isLoading = false; + bool _obscurePassword = true; // Tambahan: State untuk mata password Future _handleRegister() async { setState(() => _isLoading = true); @@ -99,13 +100,24 @@ class _RegisterScreenState 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, ), ), @@ -148,4 +160,4 @@ class _RegisterScreenState extends State { ), ); } -} +} \ No newline at end of file diff --git a/lib/services/mqtt_service.dart b/lib/services/mqtt_service.dart index 82b8ca2..7aef78f 100644 --- a/lib/services/mqtt_service.dart +++ b/lib/services/mqtt_service.dart @@ -1,135 +1,194 @@ -import 'dart:io'; +import 'dart:convert'; import 'package:mqtt_client/mqtt_client.dart'; import 'package:mqtt_client/mqtt_server_client.dart'; import 'package:flutter/foundation.dart'; class MqttService extends ChangeNotifier { MqttServerClient? client; - String _temp = "25"; - String _humidity = "55"; - String _light = "125"; - bool _isIntakeOn = false; - bool _isExhaustOn = false; - String _currentMode = "MANUAL"; - double _maxTemp = 48.0; - double _maxHum = 75.0; - String get temp => _temp; - String get humidity => _humidity; - String get light => _light; - bool get isIntakeOn => _isIntakeOn; - bool get isExhaustOn => _isExhaustOn; - String get currentMode => _currentMode; - double get maxTemp => _maxTemp; - double get maxHum => _maxHum; + // Data sensor + String _suhu = "0"; + String _kelembapan = "0"; + String _intensitas = "0"; + bool _isKipas1On = false; + bool _isKipas2On = false; + String _mode = "otomatis"; + String _timestamp = "-"; - void setLimits(double t, double h) { - _maxTemp = t; - _maxHum = h; + // Batas sensor + double _maxSuhu = 45.0; + double _maxRh = 70.0; + + // Getter sensor + String get suhu => _suhu; + String get kelembapan => _kelembapan; + String get intensitas => _intensitas; + bool get isKipas1On => _isKipas1On; + bool get isKipas2On => _isKipas2On; + String get mode => _mode; + String get timestamp => _timestamp; + + // Getter batas + double get maxSuhu => _maxSuhu; + double get maxRh => _maxRh; + + Function(String, bool)? onConnectionResult; + + void setLimits(double suhuMax, double rhMax) { + _maxSuhu = suhuMax; + _maxRh = rhMax; notifyListeners(); } - // Callback for UI notifications - Function(String, bool)? onConnectionResult; - Future connect() async { - final String clientId = 'cli_kopi_${DateTime.now().millisecondsSinceEpoch % 10000}'; - + final String clientId = + 'flutter_kopi_${DateTime.now().millisecondsSinceEpoch % 10000}'; + client = MqttServerClient.withPort( - '1f7929299fd649388da5f2e234152531.s1.eu.hivemq.cloud', - clientId, - 8883 + 's1fd277a.ala.asia-southeast1.emqxsl.com', // EMQX host baru + clientId, + 8883, ); - + client!.secure = true; client!.setProtocolV311(); client!.onBadCertificate = (dynamic cert) => true; client!.keepAlivePeriod = 20; client!.connectTimeoutPeriod = 10000; - client!.onDisconnected = onDisconnected; client!.onConnected = onConnected; - client!.logging(on: true); + client!.logging(on: kDebugMode); final connMess = MqttConnectMessage() .withClientIdentifier(clientId) .authenticateAs('kopi_user', 'Kopikopi1') .startClean(); - client!.connectionMessage = connMess; try { - debugPrint('MQTT: Connecting to HiveMQ...'); await client!.connect(); } catch (e) { debugPrint('MQTT Exception: $e'); onConnectionResult?.call('Koneksi Gagal: $e', false); client!.disconnect(); + return false; } if (client?.connectionStatus?.state == MqttConnectionState.connected) { - debugPrint('MQTT: SUCCESS!'); + debugPrint('MQTT: Terhubung!'); onConnectionResult?.call('✅ Terhubung ke Server Kopi', true); _subscribeToTopics(); notifyListeners(); return true; } else { - onConnectionResult?.call('❌ Koneksi Terputus', false); + onConnectionResult?.call('❌ Koneksi Gagal', false); notifyListeners(); return false; } } void _subscribeToTopics() { - client!.subscribe("coffee/temp", MqttQos.atMostOnce); - client!.subscribe("coffee/humidity", MqttQos.atMostOnce); - client!.subscribe("coffee/light", MqttQos.atMostOnce); - client!.subscribe("coffee/intake", MqttQos.atMostOnce); - client!.subscribe("coffee/exhaust", MqttQos.atMostOnce); - client!.subscribe("coffee/config/mode", MqttQos.atMostOnce); + client!.subscribe("kopi/sensor", MqttQos.atMostOnce); + client!.subscribe("kopi/relay/kipas1", MqttQos.atMostOnce); + client!.subscribe("kopi/relay/kipas2", MqttQos.atMostOnce); + client!.subscribe("kopi/relay/mode", MqttQos.atMostOnce); client!.updates!.listen((List>? c) { - final recMess = c![0].payload as MqttPublishMessage; - final pt = MqttPublishPayload.bytesToStringAsString(recMess.payload.message); + if (c == null || c.isEmpty) return; + + final recMess = c[0].payload as MqttPublishMessage; + final pt = MqttPublishPayload.bytesToStringAsString( + recMess.payload.message); final topic = c[0].topic; - - if (topic == "coffee/temp") _temp = pt; - if (topic == "coffee/humidity") _humidity = pt; - if (topic == "coffee/light") _light = pt; - if (topic == "coffee/intake") _isIntakeOn = (pt == "ON"); - if (topic == "coffee/exhaust") _isExhaustOn = (pt == "ON"); - if (topic == "coffee/config/mode") _currentMode = pt; - + + if (topic == "kopi/sensor") { + try { + final data = jsonDecode(pt); + _suhu = data['suhu'].toString(); + _kelembapan = data['kelembapan'].toString(); + _intensitas = data['intensitas'].toString(); + _isKipas1On = data['kipas1'] == 'ON'; + _isKipas2On = data['kipas2'] == 'ON'; + _mode = data['mode'] ?? 'otomatis'; + _timestamp = data['timestamp'] ?? '-'; + } catch (e) { + debugPrint('MQTT parse error: $e'); + } + } + + if (topic == "kopi/relay/kipas1") _isKipas1On = (pt == "ON"); + if (topic == "kopi/relay/kipas2") _isKipas2On = (pt == "ON"); + if (topic == "kopi/relay/mode") _mode = pt; + notifyListeners(); }); } - void publish(String topic, String message) { - if (topic == "coffee/intake") _isIntakeOn = (message == "ON"); - if (topic == "coffee/exhaust") _isExhaustOn = (message == "ON"); - notifyListeners(); + // Kirim perintah kipas manual + void perintahKipas(String nomorKipas, bool nyala) { + final payload = jsonEncode({ + "kipas": nomorKipas, + "status": nyala ? "ON" : "OFF", + }); + _publish("kopi/relay/manual", payload); + } - if (client?.connectionStatus?.state == MqttConnectionState.connected) { - final builder = MqttClientPayloadBuilder(); - builder.addString(message); - client!.publishMessage(topic, MqttQos.exactlyOnce, builder.payload!); + // Ganti mode otomatis/manual + void gantiMode(String mode) { + _publish("kopi/relay/mode", mode); + } + + // Kirim interval foto dalam menit + void setIntervalFoto(int menit) { + _publish("kopi/setting/interval", menit.toString()); + } + + // Trigger foto manual + void triggerFoto() { + _publish("kopi/trigger/foto", "AMBIL"); + } + + // Kirim batas sensor ke ESP32 dan Node-RED + void setBatasSensor( + double suhuMin, double suhuMax, double rhMin, double rhMax) { + final payload = jsonEncode({ + "suhu_min": suhuMin, + "suhu_max": suhuMax, + "rh_min": rhMin, + "rh_max": rhMax, + }); + _publish("kopi/setting/batas", payload); + } + + void _publish(String topic, String message) { + if (client?.connectionStatus?.state != MqttConnectionState.connected) { + debugPrint('MQTT: Tidak terhubung, tidak bisa publish'); + return; } + final builder = MqttClientPayloadBuilder(); + builder.addString(message); + client!.publishMessage(topic, MqttQos.atMostOnce, builder.payload!); + debugPrint('MQTT publish → $topic : $message'); } void onConnected() { + debugPrint('MQTT: onConnected'); notifyListeners(); } void onDisconnected() { + debugPrint('MQTT: onDisconnected'); + + // 🔥 AUTO RECONNECT + Future.delayed(const Duration(seconds: 3), () { + debugPrint('MQTT: mencoba reconnect...'); + connect(); + }); + notifyListeners(); } - void updateFromLastLog(String t, String h, String l, bool i, bool e) { - _temp = t; - _humidity = h; - _light = l; - _isIntakeOn = i; - _isExhaustOn = e; - notifyListeners(); + void disconnect() { + client?.disconnect(); } -} +} \ No newline at end of file diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 5d07423..21d8f8b 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 92b6497..a620c94 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,13 +6,11 @@ import FlutterMacOS import Foundation import app_links -import path_provider_foundation import shared_preferences_foundation import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) - PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 7432de9..df74176 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,82 +5,122 @@ packages: dependency: transitive description: name: app_links - sha256: "3ced568a5d9e309e99af71285666f1f3117bddd0bd5b3317979dccc1a40cada4" + sha256: "3462d9defc61565fde4944858b59bec5be2b9d5b05f20aed190adb3ad08a7abc" url: "https://pub.dev" source: hosted - version: "3.5.1" + version: "7.0.0" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.dev" + source: hosted + version: "1.0.4" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" async: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.1" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.3.0" - charcode: - dependency: transitive - description: - name: charcode - sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a - url: "https://pub.dev" - source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" collection: dependency: transitive description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" crypto: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf url: "https://pub.dev" source: hosted - version: "3.0.3" - csslib: - dependency: transitive - description: - name: csslib - sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" - url: "https://pub.dev" - source: hosted - version: "1.0.2" + version: "3.0.7" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" url: "https://pub.dev" source: hosted - version: "1.0.8" + version: "1.0.9" + dart_jsonwebtoken: + dependency: transitive + description: + name: dart_jsonwebtoken + sha256: ad84e60181696513d04d5f2078e0bbc20365b911f46f647797317414bdc88fbe + url: "https://pub.dev" + source: hosted + version: "3.4.1" equatable: dependency: transitive description: @@ -101,18 +141,26 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" ffi: dependency: transitive description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" + ffi_leak_tracker: + dependency: transitive + description: + name: ffi_leak_tracker + sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97" + url: "https://pub.dev" + source: hosted + version: "0.1.2" file: dependency: transitive description: @@ -125,10 +173,10 @@ packages: dependency: "direct main" description: name: fl_chart - sha256: c1e26c7e48496be85104c16c040950b0436674cdf0737f3f6e95511b2529b592 + sha256: b938f77d042cbcd822936a7a359a7235bad8bd72070de1f827efc2cc297ac888 url: "https://pub.dev" source: hosted - version: "0.63.0" + version: "1.2.0" flutter: dependency: "direct main" description: flutter @@ -138,10 +186,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "6.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -156,58 +204,82 @@ packages: dependency: transitive description: name: functions_client - sha256: a49876ebae32a50eb62483c5c5ac80ed0d8da34f98ccc23986b03a8d28cee07c + sha256: "94074d62167ae634127ef6095f536835063a7dc80f2b1aa306d2346ff9023996" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.5.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" gotrue: dependency: transitive description: name: gotrue - sha256: d6362dff9a54f8c1c372bb137c858b4024c16407324d34e6473e59623c9b9f50 + sha256: "7a4172601553e61716f5c3dd243aa3297e13308e07eb85b7853c941ba585dcf5" url: "https://pub.dev" source: hosted - version: "2.11.1" + version: "2.20.0" gtk: dependency: transitive description: name: gtk - sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + sha256: "4ff85b2a16724029dd9e5bbb5a94b6918f9973f74ba571c949d2002801879cf5" url: "https://pub.dev" source: hosted - version: "2.1.0" - html: + version: "2.2.0" + hooks: dependency: transitive description: - name: html - sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + name: hooks + sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e" url: "https://pub.dev" source: hosted - version: "0.15.4" + version: "1.0.3" http: dependency: transitive description: name: http - sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.6.0" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" intl: dependency: "direct main" description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.20.2" + jni: + dependency: transitive + description: + name: jni + sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f + url: "https://pub.dev" + source: hosted + version: "1.0.0" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" jwt_decode: dependency: transitive description: @@ -216,62 +288,94 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" lints: dependency: transitive description: name: lints - sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "6.1.0" logging: dependency: transitive description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" matcher: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.17.0" mime: dependency: transitive description: name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.0" mqtt_client: dependency: "direct main" description: name: mqtt_client - sha256: "930a951a748a3feacd57bde9a608740c724f7afb0e1be1ef56f3e1fe9f689cf9" + sha256: "41c8edd3bc8efc80c1c8ebfb40081c24d12d13085faca96b9280a624eca2d893" url: "https://pub.dev" source: hosted - version: "10.3.0" + version: "10.11.11" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" + url: "https://pub.dev" + source: hosted + version: "0.17.6" nested: dependency: transitive description: @@ -280,38 +384,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + url: "https://pub.dev" + source: hosted + version: "9.3.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" path: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.1" path_provider: dependency: transitive description: name: path_provider - sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "51f0d2c554cfbc9d6a312ab35152fc77e2f0b758ce9f1a444a3a1e5b8f3c6b7f" + sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.1" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.6.0" path_provider_linux: dependency: transitive description: @@ -332,18 +452,18 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" platform: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -352,14 +472,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" + url: "https://pub.dev" + source: hosted + version: "4.0.0" postgrest: dependency: transitive description: name: postgrest - sha256: b74dc0f57b5dca5ce9f57a54b08110bf41d6fc8a0483c0fec10c79e9aa0fb2bb + sha256: "9d61b3d4a88fcf9424d400127c54d49ed1b56ec30838fc0a33a64f31d4e694cc" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.7.0" provider: dependency: "direct main" description: @@ -368,14 +496,30 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.5+1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" realtime_client: dependency: transitive description: name: realtime_client - sha256: e3089dac2121917cc0c72d42ab056fea0abbaf3c2229048fc50e64bafc731adf + sha256: "7dfccf372d2f55aacfeefb6186f65a06f3ffae383fe042dbeef9d85d33487576" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.7.3" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.dev" + source: hosted + version: "0.6.0" retry: dependency: transitive description: @@ -396,199 +540,183 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.5.5" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.23" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.5.6" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.2" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.3" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.2" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.1" storage_client: dependency: transitive description: name: storage_client - sha256: "9f9ed283943313b23a1b27139bb18986e9b152a6d34530232c702c468d98e91a" + sha256: "4801e8ca219a35e51cbb30589aba5306667ae8935b792504595a45273cef0b18" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.5.2" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" supabase: dependency: transitive description: name: supabase - sha256: c3ebddba69ddcf16d8b78e8c44c4538b0193d1cf944fde3b72eb5b279892a370 + sha256: "40e5a8833c8834e140ef53b60a6181849667eba9ca125acb7f8e24c6a769d418" url: "https://pub.dev" source: hosted - version: "2.6.3" + version: "2.10.6" supabase_flutter: dependency: "direct main" description: name: supabase_flutter - sha256: "3b5b5b492e342f63f301605d0c66f6528add285b5744f53c9fd9abd5ffdbce5b" + sha256: c02ce58abcaf86cb8055ad40bfd98bbf5b93fed3b5b56b8220d88ed03842818b url: "https://pub.dev" source: hosted - version: "2.8.4" + version: "2.12.4" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.7.10" typed_data: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" - universal_html: - dependency: transitive - description: - name: universal_html - sha256: "56536254004e24d9d8cfdb7dbbf09b74cf8df96729f38a2f5c238163e3d58971" - url: "https://pub.dev" - source: hosted - version: "2.2.4" - universal_io: - dependency: transitive - description: - name: universal_io - sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" - url: "https://pub.dev" - source: hosted - version: "2.2.2" + version: "1.4.0" url_launcher: dependency: transitive description: name: url_launcher - sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 url: "https://pub.dev" source: hosted - version: "6.2.4" + version: "6.3.2" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745 + sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572" url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.29" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" url: "https://pub.dev" source: hosted - version: "6.2.4" + version: "6.4.1" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.2.2" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.5" url_launcher_platform_interface: dependency: transitive description: @@ -601,66 +729,90 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "7fd2f55fe86cea2897b963e864dc01a7eb0719ecc65fcef4c1cc3d686d718bb2" + sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.4.2" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.5" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" + source: hosted + version: "15.2.0" web: dependency: transitive description: name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "0.1.4-beta" + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "3.0.3" win32: dependency: "direct overridden" description: name: win32 - sha256: c97defd418eef4ec88c0d1652cdce84b9f7b63dd7198e266d06ac1710d527067 + sha256: ba7d5750e3441caa1bbe31d9e516348fcf8dfcb32aa29ef87a844a59f4d1f1d0 url: "https://pub.dev" source: hosted - version: "5.0.8" + version: "6.1.0" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" yet_another_json_isolate: dependency: transitive description: name: yet_another_json_isolate - sha256: "56155e9e0002cc51ea7112857bbcdc714d4c35e176d43e4d3ee233009ff410c9" + sha256: fe45897501fa156ccefbfb9359c9462ce5dec092f05e8a56109db30be864f01e url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.1.0" sdks: - dart: ">=3.1.0 <4.0.0" - flutter: ">=3.13.0" + dart: ">=3.10.3 <4.0.0" + flutter: ">=3.38.4" diff --git a/pubspec.yaml b/pubspec.yaml index ce12010..7c8ad39 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,16 +30,16 @@ environment: dependencies: flutter: sdk: flutter - cupertino_icons: ^1.0.8 - supabase_flutter: ^2.5.0 - mqtt_client: ^10.2.0 + cupertino_icons: ^1.0.9 + supabase_flutter: ^2.12.4 + mqtt_client: ^10.11.11 provider: ^6.1.1 - fl_chart: 0.63.0 - intl: ^0.18.1 - shared_preferences: ^2.2.0 + fl_chart: ^1.2.0 + intl: ^0.20.2 + shared_preferences: ^2.5.5 dependency_overrides: - win32: 5.0.8 + win32: ^6.1.0 dev_dependencies: flutter_test: @@ -50,7 +50,7 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^3.0.0 + flutter_lints: ^6.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 8f8ee4f..642b1be 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni ) set(PLUGIN_BUNDLED_LIBRARIES)