From bd9ce4022fea869ca02238c01a21bd6658298d7a Mon Sep 17 00:00:00 2001 From: pulipakaa24 Date: Mon, 22 Dec 2025 20:26:33 -0600 Subject: [PATCH] Bunch of updates, next up is token pipeline --- ios/Flutter/AppFrameworkInfo.plist | 2 +- ios/Podfile | 2 +- ios/Podfile.lock | 4 +- ios/Runner.xcodeproj/project.pbxproj | 6 +- .../addingDevices/device_setup.dart | 264 ++++++++++-------- lib/BlindMasterScreens/day_time_picker.dart | 204 ++++++++++++++ .../individualControl/device_screen.dart | 4 +- .../individualControl/devices_menu.dart | 25 +- .../individualControl/peripheral_screen.dart | 8 +- lib/BlindMasterScreens/schedules_screen.dart | 160 ++++++++++- lib/main.dart | 2 + pubspec.lock | 68 +++-- 12 files changed, 582 insertions(+), 167 deletions(-) create mode 100644 lib/BlindMasterScreens/day_time_picker.dart diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 7c56964..1dc6cf7 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 13.0 diff --git a/ios/Podfile b/ios/Podfile index e549ee2..620e46e 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '12.0' +# platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 4e5da5b..ff2c23f 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -26,11 +26,11 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/path_provider_foundation/darwin" SPEC CHECKSUMS: - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3 flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 -PODFILE CHECKSUM: 4305caec6b40dde0ae97be1573c53de1882a07e5 +PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index d3dfa87..2af573e 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -456,7 +456,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -590,7 +590,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -642,7 +642,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/lib/BlindMasterScreens/addingDevices/device_setup.dart b/lib/BlindMasterScreens/addingDevices/device_setup.dart index 9417979..151ef09 100644 --- a/lib/BlindMasterScreens/addingDevices/device_setup.dart +++ b/lib/BlindMasterScreens/addingDevices/device_setup.dart @@ -7,6 +7,33 @@ import 'package:flutter/material.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:google_fonts/google_fonts.dart'; + +enum authTypes { + OPEN, + WEP, + WPA_PSK, + WPA2_PSK, + WPA_WPA2_PSK, + WPA2_ENTERPRISE, + WPA3_PSK, + WPA2_WPA3_PSK, + WAPI_PSK, + OWE, + WPA3_ENT_192, + WPA3_EXT_PSK, + WPA3_EXT_PSK_MIXED_MODE, + DPP, + WPA3_ENTERPRISE, + WPA2_WPA3_ENTERPRISE, + WPA_ENTERPRISE +} + +const List enterprise = [ + authTypes.WPA_ENTERPRISE,authTypes.WPA2_ENTERPRISE, + authTypes.WPA3_ENTERPRISE,authTypes.WPA2_WPA3_ENTERPRISE, + authTypes.WPA3_ENT_192 +]; + class DeviceSetup extends StatefulWidget { final BluetoothDevice device; @@ -19,17 +46,16 @@ class DeviceSetup extends StatefulWidget { class _DeviceSetupState extends State { List _services = []; - List openNetworks = []; - List pskNetworks = []; + List> networks = []; late StreamSubscription> _ssidSub; StreamSubscription>? _confirmSub; Widget? wifiList; String? message; - String? password; final passControl = TextEditingController(); + final unameControl = TextEditingController(); @override void initState() { super.initState(); @@ -44,93 +70,77 @@ class _DeviceSetupState extends State { super.dispose(); } - Future setWifiListListener(BluetoothCharacteristic ssidListChar) async { - setState(() { - wifiList = null; - }); - await ssidListChar.setNotifyValue(true); - - _ssidSub = ssidListChar.onValueReceived.listen((List value) { - List ssidList = []; - bool noNetworks = false; + Future setRefreshListener(BluetoothCharacteristic ssidRefreshChar, BluetoothCharacteristic ssidListChar) async { + await ssidRefreshChar.setNotifyValue(true); + _ssidSub = ssidRefreshChar.onValueReceived.listen((List value) async { try { - final val = utf8.decode(value); - if (val == ';') noNetworks = true; - ssidList = val - .split(';') - .map((s) => s.trim()) - .where((s) => s.isNotEmpty) - .toList(); - openNetworks = ssidList - .where((s) => s.split(',')[1] == "OPEN") - .map((s) => s.split(',')[0]) - .toList(); - pskNetworks = ssidList - .where((s) => s.split(',')[1] == "SECURED") - .map((s) => s.split(',')[0]) - .toList(); + final command = utf8.decode(value); + if (command == "Ready") { + // Device is ready, now read the WiFi list + List rawData = await ssidListChar.read(); + + try { + final val = utf8.decode(rawData); + networks = json.decode(val) as List>; + } catch (e) { + if(!mounted)return; + ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e)); + } + + // Acknowledge completion + try { + await ssidRefreshChar.write(utf8.encode("Done"), withoutResponse: ssidRefreshChar.properties.writeWithoutResponse); + } catch (e) { + if(!mounted)return; + ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(Exception("Failed to send Done"))); + } + + if(!mounted)return; + setState(() { + wifiList = networks.isEmpty + ? SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: SizedBox( + height: MediaQuery.of(context).size.height * 0.6, + child: const Center( + child: Text( + "No networks found...", + textAlign: TextAlign.center, + style: TextStyle(fontSize: 15), + ), + ), + ), + ) + : ListView( + children: [ + ...buildSSIDs() + ], + ); + }); + } } catch (e) { if(!mounted)return; ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e)); } - - setState(() { - wifiList = noNetworks - ? SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), - child: SizedBox( - height: MediaQuery.of(context).size.height * 0.6, - child: const Center( - child: Text( - "No networks found...", - textAlign: TextAlign.center, - style: TextStyle(fontSize: 15), - ), - ), - ), - ) - : ssidList.isNotEmpty - ? ListView( - children: [ - ...buildSSIDs() - ], - ) - : null; - }); }); } List buildSSIDs() { - List open = openNetworks.map((s) { + List networkList = networks.map((s) { return Card( child: ListTile( - leading: const Icon(Icons.wifi), - title: Text(s), + leading: Icon((s["rssi"] as int < -70) ? Icons.wifi_1_bar : ((s["rssi"] as int < -50) ? Icons.wifi_2_bar: Icons.wifi)), + title: Text(s["ssid"] as String), + subtitle: Text(authTypes.values[s["auth"] as int].name), trailing: const Icon(Icons.arrow_forward_ios_rounded), onTap: () { - openConnect(s); + authenticate(s); }, ), ); }).toList(); - List secure = pskNetworks.map((s) { - return Card( - child: ListTile( - leading: const Icon(Icons.wifi_password), - title: Text(s), - trailing: const Icon(Icons.arrow_forward_ios_rounded), - onTap: () { - setPassword(s); - }, - ), - ); - }).toList(); - return open + secure; - } - - Future openConnect(String ssid) async { - await transmitWiFiDetails(ssid, ""); + return networkList; } Future discoverServices() async{ @@ -157,37 +167,74 @@ class _DeviceSetupState extends State { Future initSetup() async { await discoverServices(); final ssidListChar = _services[0].characteristics.lastWhere((c) => c.uuid.str == "0000"); - await setWifiListListener(ssidListChar); + final ssidRefreshChar = _services[0].characteristics.lastWhere((c) => c.uuid.str == "0004"); + await setRefreshListener(ssidRefreshChar, ssidListChar); refreshWifiList(); } - Future setPassword(String ssid) async { - String? password = await showDialog( + bool isEnterprise(Map network) { + authTypes type = authTypes.values[network["auth"] as int]; + return enterprise.contains(type); + } + + bool isOpen(Map network) { + authTypes type = authTypes.values[network["auth"] as int]; + return type == authTypes.OPEN; + } + + Future authenticate(Map network) async { + bool ent = isEnterprise(network); + bool open = isOpen(network); + Map creds = await showDialog( context: context, builder: (dialogContext) { return AlertDialog( title: Text( - ssid, + network["ssid"], style: GoogleFonts.aBeeZee(), ), content: Form( autovalidateMode: AutovalidateMode.onUnfocus, - child: TextFormField( - controller: passControl, - obscureText: true, - decoration: const InputDecoration(hintText: "Enter password"), - validator: (value) { - if (value == null) return "null input"; - if (value.length < 8) return "not long enough!"; - return null; - }, - textInputAction: TextInputAction.send, - onFieldSubmitted: (value) => Navigator.pop(dialogContext, passControl.text), - ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (ent) + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextFormField( + controller: unameControl, + decoration: const InputDecoration(hintText: "Enter your enterprise login"), + textInputAction: TextInputAction.next, // Shows "Next" on keyboard + onFieldSubmitted: (_) => FocusScope.of(context).nextFocus(), // Moves to password + validator: (value) => (value == null || value.isEmpty) ? "Empty username!" : null, + ), + const SizedBox(height: 16), + ] + ), + if (!open) + TextFormField( + controller: passControl, + obscureText: true, + decoration: const InputDecoration(hintText: "Enter password"), + validator: (value) => (value == null || value.length < 8) ? "Not long enough!" : null, + textInputAction: TextInputAction.send, + onFieldSubmitted: (value) { + if (Form.of(context).validate()) { + Navigator.pop(dialogContext, (ent ? + {"uname": unameControl.text, "password": passControl.text} + : (open ? {} : {"password": passControl.text}))); + } + }, + ), + ] + ) + ), actions: [ TextButton( onPressed: () { + unameControl.clear(); passControl.clear(); Navigator.pop(dialogContext); }, @@ -197,6 +244,7 @@ class _DeviceSetupState extends State { onPressed: () { Navigator.pop(dialogContext, passControl.text); passControl.clear(); + unameControl.clear(); }, child: const Text("Connect"), ), @@ -205,37 +253,30 @@ class _DeviceSetupState extends State { } ); - await transmitWiFiDetails(ssid, password); + if (creds["password"] == null && !open) return; + if (creds["uname"] == null && ent) return; + await transmitWiFiDetails(network["ssid"], network["auth"], creds); } - Future transmitWiFiDetails(String ssid, String? password) async { - if (password == null) return; - + Future transmitWiFiDetails(String ssid, int auth, Map creds) async { setState(() { wifiList = null; message = "Attempting Connection..."; }); - final ssidEntryChar = _services[0].characteristics.lastWhere((c) => c.uuid.str == "0001"); + final credsChar = _services[0].characteristics.lastWhere((c) => c.uuid.str == "0001"); + Map credsJson = { + "ssid": ssid, + "auth": auth, + ...creds, // Spread operator adds all key-value pairs from creds + }; + try { + String jsonString = json.encode(credsJson); try { - await ssidEntryChar.write(utf8.encode(ssid), withoutResponse: ssidEntryChar.properties.writeWithoutResponse); + await credsChar.write(utf8.encode(jsonString), withoutResponse: credsChar.properties.writeWithoutResponse); } catch (e) { - throw Exception("SSID Write Error"); - } - } catch (e){ - if(!mounted)return; - ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e)); - refreshWifiList(); - return; - } - - final passEntryChar = _services[0].characteristics.lastWhere((c) => c.uuid.str == "0002"); - try { - try { - await passEntryChar.write(utf8.encode(password), withoutResponse: passEntryChar.properties.writeWithoutResponse); - } catch (e) { - throw Exception("Password Write Error"); + throw Exception("Credentials Write Error"); } } catch (e){ if(!mounted)return; @@ -265,7 +306,7 @@ class _DeviceSetupState extends State { }); } else if (connectResponse == "Error") { _confirmSub?.cancel(); - throw Exception("SSID/Password Incorrect"); + throw Exception("SSID/Password Incorrect / Other credential error"); } } catch (e) { if (!mounted) return; @@ -279,12 +320,13 @@ class _DeviceSetupState extends State { Future refreshWifiList() async{ final ssidRefreshChar = _services[0].characteristics.lastWhere((c) => c.uuid.str == "0004"); setState(() { + wifiList = null; message = null; }); try { try { - await ssidRefreshChar.write(utf8.encode("refresh"), withoutResponse: ssidRefreshChar.properties.writeWithoutResponse); + await ssidRefreshChar.write(utf8.encode("Start"), withoutResponse: ssidRefreshChar.properties.writeWithoutResponse); } catch (e) { throw Exception ("Refresh Error"); } diff --git a/lib/BlindMasterScreens/day_time_picker.dart b/lib/BlindMasterScreens/day_time_picker.dart new file mode 100644 index 0000000..67edf91 --- /dev/null +++ b/lib/BlindMasterScreens/day_time_picker.dart @@ -0,0 +1,204 @@ +import 'package:blind_master/main.dart'; +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class DayTimePicker extends StatefulWidget { + const DayTimePicker({super.key, required this.defaultTime, required this.sendSchedule}); + + final TimeOfDay defaultTime; + final void Function(TimeOfDay) sendSchedule; + @override + State createState() => _DayTimePickerState(); +} + +class _DayTimePickerState extends State { + TimeOfDay? scheduleTime; + double _blindPosition = 0; + String imagePath = ""; + Set days = {}; + + @override + void initState() { + super.initState(); + updateBackground(); + } + + Future selectTime() async { + scheduleTime = await showTimePicker( + context: context, + initialTime: scheduleTime ?? widget.defaultTime, + ) ?? (scheduleTime ?? widget.defaultTime); + setState(() { + updateBackground(); + }); + } + + void updateBackground() { + final hour = scheduleTime?.hour ?? widget.defaultTime.hour; + + if (hour >= 5 && hour < 10) { + imagePath = 'assets/images/MorningSill.png'; + } else if (hour >= 10 && hour < 18) { + imagePath = 'assets/images/NoonSill.png'; + } else if (hour >= 18 && hour < 22) { + imagePath = 'assets/images/EveningSill.png'; + } else { + imagePath = 'assets/images/NightSill.png'; + } + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text( + 'New Schedule', + style: GoogleFonts.aBeeZee(), + ), + content: Column( + mainAxisSize: MainAxisSize.min, // Keep column compact + children: [ + Text( + "Move to position" + ), + SizedBox( + height: MediaQuery.of(context).size.height * 0.3, + child: Container( + padding: EdgeInsets.fromLTRB(0, 20, 0, 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: MediaQuery.of(context).size.width * 0.11, + ), + Stack( + children: [ + // Background image + Align( + alignment: Alignment.center, + child: Image.asset( + imagePath, + // fit: BoxFit.cover, + width: MediaQuery.of(context).size.width * 0.45, + ), + ), + + Align( + alignment: Alignment.center, + child: Container( + margin: EdgeInsets.only(top: MediaQuery.of(context).size.width * 0.05), + height: MediaQuery.of(context).size.width * 0.43, + width: MediaQuery.of(context).size.width * 0.45, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: List.generate(10, (index) { + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + height: _blindPosition < 5 ? + 3.65 * (5 - _blindPosition) + : 3.65 * (_blindPosition - 5), + width: MediaQuery.of(context).size.width * 0.40, // example + color: const Color.fromARGB(255, 121, 85, 72), + ); + }), + ), + ) + ), + ], + ), + // Slider on the side + Align( + alignment: Alignment.centerRight, + child: RotatedBox( + quarterTurns: -1, + child: Slider( + value: _blindPosition, + activeColor: Theme.of(context).primaryColorDark, + thumbColor: Theme.of(context).primaryColorLight, + inactiveColor: Theme.of(context).primaryColorDark, + min: 0, + max: 10, + divisions: 10, + onChanged: (value) { + setState(() { + _blindPosition = value; + }); + }, + ), + ), + ) + ], + ), + ) + ), + Text( + "At" + ), + Theme( + data: Theme.of(context).copyWith( + timePickerTheme: TimePickerThemeData( + hourMinuteColor: Theme.of(context).primaryColorDark, + dialBackgroundColor: Theme.of(context).primaryColorDark, + ) + ), + child: ElevatedButton( + onPressed: selectTime, + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10) + ), + backgroundColor: Theme.of(context).primaryColorDark, + foregroundColor: Theme.of(context).highlightColor + ), + child: Text(scheduleTime?.format(context) ?? widget.defaultTime.format(context)), + ), + ), + Container( + padding: EdgeInsets.all(10), + child: Text( + "Every" + ), + ), + Wrap( + spacing: 5.0, + alignment: WrapAlignment.center, + children: DaysOfWeek.values.map((DaysOfWeek day) { + return FilterChip( + showCheckmark: false, + label: Text(day.name), + selected: days.contains(day), + selectedColor: Theme.of(context).primaryColorDark, + onSelected: (bool selected) { + setState(() { + if (selected) { + days.add(day); + } else { + days.remove(day); + } + }); + }, + ); + }).toList(), + ), + ], + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text( + "Cancel", + style: TextStyle( + color: Colors.red + ), + ) + ), + ] + ) + ], + ); + } +} \ No newline at end of file diff --git a/lib/BlindMasterScreens/individualControl/device_screen.dart b/lib/BlindMasterScreens/individualControl/device_screen.dart index a27754c..d6669c8 100644 --- a/lib/BlindMasterScreens/individualControl/device_screen.dart +++ b/lib/BlindMasterScreens/individualControl/device_screen.dart @@ -140,7 +140,7 @@ class _DeviceScreenState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => PeripheralScreen(peripheralId: peripheral['id'], + builder: (context) => PeripheralScreen(deviceName: deviceName, peripheralId: peripheral['id'], peripheralNum: peripheral['port'], deviceId: widget.deviceId,), ), ).then((_) { populatePeripherals(); }); @@ -220,7 +220,7 @@ class _DeviceScreenState extends State { ), const SizedBox(height: 20), DropdownButtonFormField( - value: port, + initialValue: port, decoration: const InputDecoration( labelText: 'Hub Port', border: OutlineInputBorder(), diff --git a/lib/BlindMasterScreens/individualControl/devices_menu.dart b/lib/BlindMasterScreens/individualControl/devices_menu.dart index 08c86e0..55c6914 100644 --- a/lib/BlindMasterScreens/individualControl/devices_menu.dart +++ b/lib/BlindMasterScreens/individualControl/devices_menu.dart @@ -159,17 +159,20 @@ class _DevicesMenuState extends State { Widget build(BuildContext context) { return Scaffold( body: deviceList ?? const Center(child: CircularProgressIndicator()), - floatingActionButton: FloatingActionButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => AddDevice()), - ); - }, - foregroundColor: Theme.of(context).highlightColor, - backgroundColor: Theme.of(context).primaryColorDark, - child: Icon(Icons.add), - ), + floatingActionButton: Container( + padding: EdgeInsets.all(25), + child:FloatingActionButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => AddDevice()), + ); + }, + foregroundColor: Theme.of(context).highlightColor, + backgroundColor: Theme.of(context).primaryColorDark, + child: Icon(Icons.add), + ), + ) ); } } \ No newline at end of file diff --git a/lib/BlindMasterScreens/individualControl/peripheral_screen.dart b/lib/BlindMasterScreens/individualControl/peripheral_screen.dart index c982a07..0c308ca 100644 --- a/lib/BlindMasterScreens/individualControl/peripheral_screen.dart +++ b/lib/BlindMasterScreens/individualControl/peripheral_screen.dart @@ -10,10 +10,11 @@ import 'package:google_fonts/google_fonts.dart'; import 'package:socket_io_client/socket_io_client.dart' as IO; class PeripheralScreen extends StatefulWidget { - const PeripheralScreen({super.key, required this.peripheralId, required this.deviceId, required this.peripheralNum}); + const PeripheralScreen({super.key, required this.peripheralId, required this.deviceId, required this.peripheralNum, required this.deviceName}); final int peripheralId; final int peripheralNum; final int deviceId; + final String deviceName; @override State createState() => _PeripheralScreenState(); } @@ -334,7 +335,7 @@ class _PeripheralScreenState extends State { return Scaffold( appBar: AppBar( title: Text( - peripheralName, + "${widget.deviceName} - $peripheralName", style: GoogleFonts.aBeeZee(), ), backgroundColor: Theme.of(context).primaryColorLight, @@ -462,7 +463,8 @@ class _PeripheralScreenState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => SchedulesScreen() + builder: (context) => SchedulesScreen(peripheralId: widget.peripheralId, periphName: peripheralName, + deviceId: widget.deviceId, peripheralNum: widget.peripheralNum, deviceName: widget.deviceName,) ) ); }, diff --git a/lib/BlindMasterScreens/schedules_screen.dart b/lib/BlindMasterScreens/schedules_screen.dart index 43c802f..6097060 100644 --- a/lib/BlindMasterScreens/schedules_screen.dart +++ b/lib/BlindMasterScreens/schedules_screen.dart @@ -1,15 +1,169 @@ +import 'dart:convert'; + +import 'package:blind_master/BlindMasterResources/error_snackbar.dart'; +import 'package:blind_master/BlindMasterResources/secure_transmissions.dart'; +import 'package:blind_master/BlindMasterScreens/day_time_picker.dart'; import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; class SchedulesScreen extends StatefulWidget { - const SchedulesScreen({super.key}); - + const SchedulesScreen({super.key, required this.peripheralId, required this.deviceId, + required this.peripheralNum, required this.deviceName, required this.periphName}); + final int peripheralId; + final int peripheralNum; + final int deviceId; + final String deviceName; + final String periphName; @override State createState() => _SchedulesScreenState(); } class _SchedulesScreenState extends State { + List> schedules = []; + Widget? scheduleList; + + @override + void initState() { + super.initState(); + getSchedules(); + } + + Future getSchedules() async { + try{ + final payload = { + "periphId": widget.deviceId + }; + final response = await securePost(payload, 'periph_schedule_list'); + if (response == null) throw Exception("no response!"); + + if (response.statusCode == 200) { + final body = json.decode(response.body) as Map; + List tempList = body['scheduledUpdates'] as List; + schedules = tempList + .whereType>() + .toList(); + } + } catch(e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + errorSnackbar(e) + ); + } + + setState(() { + scheduleList = RefreshIndicator( + onRefresh: getSchedules, + child: schedules.isEmpty + ? SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: SizedBox( + height: MediaQuery.of(context).size.height * 0.6, + child: const Center( + child: Text( + "No schedules found...\nAdd one using the '+' button", + textAlign: TextAlign.center, + style: TextStyle(fontSize: 16), + ), + ), + ), + ) + : ListView.builder( + itemCount: schedules.length, + itemBuilder: (context, i) { + final schedule = schedules[i]; + return Dismissible( + key: Key(schedule['id'].toString()), + direction: DismissDirection.endToStart, + background: Container( + color: Colors.red, + alignment: Alignment.centerRight, + padding: const EdgeInsets.symmetric(horizontal: 20), + child: const Icon(Icons.delete, color: Colors.white), + ), + confirmDismiss: (direction) async { + // Ask for confirmation (optional) + return await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Delete Schedule'), + content: const Text('Are you sure you want to delete this schedule?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: const Text('Delete'), + ), + ], + ), + ); + }, + onDismissed: (direction) { + // TODO Actually delete the schedule + // deleteDevice(device['id'], i); + }, + child: Card( + child: ListTile( + leading: const Icon(Icons.blinds), + title: Text("${schedule['pos']} every ${schedule['schedule']['daysOfWeek']} at ${schedule['schedule']['hours']}:${schedule['schedule']['minutes']}"), + trailing: const Icon(Icons.arrow_forward_ios_rounded), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => Placeholder(), + // TODO open popup for schedule setter. + ), + ).then((_) { getSchedules(); }); + }, + ), + ), + ); + }, + ), + ); + }); + return Future.delayed(Duration(milliseconds: 500)); + } + + Future sendSchedule(TimeOfDay scheduleTime) async { + return; + } + + void addSchedule() { + showDialog( + context: context, + builder: (BuildContext dialogContext) { // Use dialogContext for navigation within the dialog + return DayTimePicker(defaultTime: TimeOfDay(hour: 12, minute: 0), sendSchedule: sendSchedule); + } + ); + } + @override Widget build(BuildContext context) { - return const Placeholder(); + return Scaffold( + appBar: AppBar( + title: Text( + "Schedules: ${widget.deviceName} - ${widget.periphName}", + style: GoogleFonts.aBeeZee(), + ), + backgroundColor: Theme.of(context).primaryColorLight, + foregroundColor: Colors.white, + ), + body: scheduleList ?? const Center(child: CircularProgressIndicator()), + floatingActionButton: Container( + padding: EdgeInsets.all(25), + child: FloatingActionButton( + backgroundColor: Theme.of(context).primaryColorDark, + foregroundColor: Theme.of(context).highlightColor, + heroTag: "add", + onPressed: addSchedule, + tooltip: "Add Schedule", + child: Icon(Icons.add), + ) + ) + ); } } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index f5b3ab2..5f32cd8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,8 @@ import 'package:blind_master/BlindMasterScreens/Startup/splash_screen.dart'; import 'package:flutter/material.dart'; +enum DaysOfWeek {Su, M, Tu, W, Th, F, Sa} + void main() { runApp(const MyApp()); } diff --git a/pubspec.lock b/pubspec.lock index abc90c6..2ce6977 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -93,10 +93,10 @@ packages: dependency: "direct main" description: name: dio - sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" + sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 url: "https://pub.dev" source: hosted - version: "5.8.0+1" + version: "5.9.0" dio_web_adapter: dependency: transitive description: @@ -252,18 +252,18 @@ packages: dependency: "direct main" description: name: google_fonts - sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 + sha256: ebc94ed30fd13cefd397cb1658b593f21571f014b7d1197eeb41fb95f05d899a url: "https://pub.dev" source: hosted - version: "6.2.1" + version: "6.3.1" http: dependency: "direct main" description: name: http - sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" + sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" http_parser: dependency: transitive description: @@ -284,26 +284,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0" url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "11.0.1" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -344,6 +344,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" path: dependency: transitive description: @@ -364,18 +372,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 + sha256: "993381400e94d18469750e5b9dcb8206f15bc09f9da86b9e44a9b0092a0066db" url: "https://pub.dev" source: hosted - version: "2.2.17" + version: "2.2.18" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" path_provider_linux: dependency: transitive description: @@ -404,10 +412,10 @@ packages: dependency: transitive description: name: petitparser - sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" + sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "7.0.1" platform: dependency: transitive description: @@ -497,10 +505,10 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.6" typed_data: dependency: transitive description: @@ -513,18 +521,18 @@ packages: 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: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" url: "https://pub.dev" source: hosted - version: "15.0.0" + version: "15.0.2" web: dependency: transitive description: @@ -537,10 +545,10 @@ packages: dependency: transitive description: name: win32 - sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba" + sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03" url: "https://pub.dev" source: hosted - version: "5.13.0" + version: "5.14.0" xdg_directories: dependency: transitive description: @@ -553,10 +561,10 @@ packages: dependency: transitive description: name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.6.1" sdks: - dart: ">=3.7.2 <4.0.0" - flutter: ">=3.27.0" + dart: ">=3.8.0 <4.0.0" + flutter: ">=3.29.0"