diff --git a/lib/BlindMasterResources/timezone_picker.dart b/lib/BlindMasterResources/timezone_picker.dart new file mode 100644 index 0000000..8f14615 --- /dev/null +++ b/lib/BlindMasterResources/timezone_picker.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; + +const List> kTimezones = [ + {'display': 'Eastern Time (ET)', 'tz': 'America/New_York'}, + {'display': 'Central Time (CT)', 'tz': 'America/Chicago'}, + {'display': 'Mountain Time (MT)', 'tz': 'America/Denver'}, + {'display': 'Mountain Time – Arizona (no DST)', 'tz': 'America/Phoenix'}, + {'display': 'Pacific Time (PT)', 'tz': 'America/Los_Angeles'}, + {'display': 'Alaska Time (AKT)', 'tz': 'America/Anchorage'}, + {'display': 'Hawaii Time (HT)', 'tz': 'Pacific/Honolulu'}, + {'display': 'UTC', 'tz': 'UTC'}, +]; + +class TimezonePicker extends StatelessWidget { + const TimezonePicker({ + super.key, + required this.value, + required this.onChanged, + this.label = 'Timezone', + }); + + final String? value; + final ValueChanged onChanged; + final String label; + + @override + Widget build(BuildContext context) { + return InputDecorator( + decoration: InputDecoration( + labelText: label, + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + ), + child: DropdownButton( + value: value, + hint: const Text('Select timezone'), + isExpanded: true, + underline: const SizedBox.shrink(), + items: kTimezones + .map((tz) => DropdownMenuItem( + value: tz['tz'], + child: Text(tz['display']!), + )) + .toList(), + onChanged: onChanged, + ), + ); + } +} diff --git a/lib/BlindMasterScreens/accountManagement/account_screen.dart b/lib/BlindMasterScreens/accountManagement/account_screen.dart index a02c7cc..3dd81d2 100644 --- a/lib/BlindMasterScreens/accountManagement/account_screen.dart +++ b/lib/BlindMasterScreens/accountManagement/account_screen.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:blind_master/BlindMasterResources/error_snackbar.dart'; import 'package:blind_master/BlindMasterResources/secure_transmissions.dart'; +import 'package:blind_master/BlindMasterResources/timezone_picker.dart'; import 'package:blind_master/BlindMasterScreens/accountManagement/change_password_screen.dart'; import 'package:blind_master/BlindMasterScreens/accountManagement/change_email_screen.dart'; import 'package:flutter/material.dart'; @@ -17,6 +18,7 @@ class _AccountScreenState extends State { String? name; String? email; String? createdAt; + String? timezone; bool isLoading = true; @override @@ -38,7 +40,8 @@ class _AccountScreenState extends State { setState(() { name = body['name'] ?? 'N/A'; email = body['email'] ?? 'N/A'; - + timezone = body['timezone'] as String?; + // Parse and format the created_at timestamp if (body['created_at'] != null) { try { @@ -67,6 +70,64 @@ class _AccountScreenState extends State { } } + String _tzAbbrev(String? ianaKey) { + if (ianaKey == null) return 'None (UTC)'; + final match = kTimezones.firstWhere( + (tz) => tz['tz'] == ianaKey, + orElse: () => {'display': ianaKey, 'tz': ianaKey}, + ); + final display = match['display']!; + final parenMatch = RegExp(r'\(([^)]+)\)').firstMatch(display); + return parenMatch?.group(1) ?? display; + } + + Future _handleChangeTimezone() async { + String? selected = timezone; + final confirmed = await showDialog( + context: context, + builder: (dialogContext) { + return StatefulBuilder( + builder: (context, setDialogState) { + return AlertDialog( + title: const Text('Account Timezone'), + content: TimezonePicker( + value: selected, + label: 'Default Timezone', + onChanged: (tz) => setDialogState(() => selected = tz), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(false), + child: const Text('Cancel'), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).primaryColorLight, + foregroundColor: Colors.white, + ), + onPressed: () => Navigator.of(dialogContext).pop(true), + child: const Text('Save'), + ), + ], + ); + }, + ); + }, + ); + + if (confirmed == true && selected != null && mounted) { + try { + final response = await securePost({'timezone': selected}, 'update_user_timezone'); + if (response == null) throw Exception('No response'); + if (response.statusCode != 200) throw Exception('Failed to update timezone'); + setState(() => timezone = selected); + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e)); + } + } + } + Future _handleDeleteAccount() async { final primaryColor = Theme.of(context).primaryColorLight; @@ -268,6 +329,13 @@ class _AccountScreenState extends State { ); }, ), + Divider(height: 1), + ListTile( + leading: const Icon(Icons.access_time), + title: Text('Timezone: ${_tzAbbrev(timezone)}'), + trailing: const Icon(Icons.arrow_forward_ios, size: 16), + onTap: _handleChangeTimezone, + ), ], ), ), diff --git a/lib/BlindMasterScreens/account_screen.dart b/lib/BlindMasterScreens/account_screen.dart deleted file mode 100644 index 5979356..0000000 --- a/lib/BlindMasterScreens/account_screen.dart +++ /dev/null @@ -1,318 +0,0 @@ -import 'dart:convert'; -import 'package:blind_master/BlindMasterResources/error_snackbar.dart'; -import 'package:blind_master/BlindMasterResources/secure_transmissions.dart'; -import 'package:blind_master/BlindMasterScreens/accountManagement/change_password_screen.dart'; -import 'package:blind_master/BlindMasterScreens/accountManagement/verify_email_change_screen.dart'; -import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; - -class AccountScreen extends StatefulWidget { - const AccountScreen({super.key}); - - @override - State createState() => _AccountScreenState(); -} - -class _AccountScreenState extends State { - String? name; - String? email; - String? createdAt; - bool isLoading = true; - - @override - void initState() { - super.initState(); - fetchAccountInfo(); - } - - Future fetchAccountInfo() async { - try { - final response = await secureGet('account_info'); - - if (response == null) { - throw Exception('No response from server'); - } - - if (response.statusCode == 200) { - final body = json.decode(response.body); - setState(() { - name = body['name'] ?? 'N/A'; - email = body['email'] ?? 'N/A'; - - // Parse and format the created_at timestamp - if (body['created_at'] != null) { - try { - final DateTime dateTime = DateTime.parse(body['created_at']); - final months = ['January', 'February', 'March', 'April', 'May', 'June', - 'July', 'August', 'September', 'October', 'November', 'December']; - createdAt = '${months[dateTime.month - 1]} ${dateTime.day}, ${dateTime.year}'; - } catch (e) { - createdAt = 'N/A'; - } - } else { - createdAt = 'N/A'; - } - - isLoading = false; - }); - } else { - throw Exception('Failed to load account info'); - } - } catch (e) { - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e)); - setState(() { - isLoading = false; - }); - } - } - - Future _handleChangeEmail() async { - final TextEditingController emailController = TextEditingController(); - final formKey = GlobalKey(); - final primaryColor = Theme.of(context).primaryColorLight; - - final result = await showDialog( - context: context, - builder: (BuildContext dialogContext) { - return AlertDialog( - title: Text('Change Email'), - content: Form( - key: formKey, - child: TextFormField( - controller: emailController, - decoration: InputDecoration( - labelText: 'New Email Address', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.email), - ), - keyboardType: TextInputType.emailAddress, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter an email address'; - } - final emailRegex = RegExp(r'^[^\s@]+@[^\s@]+\.[^\s@]+$'); - if (!emailRegex.hasMatch(value)) { - return 'Please enter a valid email'; - } - if (value == email) { - return 'This is your current email'; - } - return null; - }, - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(dialogContext).pop(false), - child: Text('Cancel'), - ), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: primaryColor, - foregroundColor: Colors.white, - ), - onPressed: () async { - if (formKey.currentState!.validate()) { - Navigator.of(dialogContext).pop(true); - } - }, - child: Text('Send Verification'), - ), - ], - ); - }, - ); - - // Allow dialog to fully close before disposing controller - await Future.delayed(Duration(milliseconds: 100)); - - if (result == true && mounted) { - final newEmail = emailController.text.trim(); - - try { - // Show loading indicator - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext loadingContext) { - return Center( - child: CircularProgressIndicator( - color: primaryColor, - ), - ); - }, - ); - - final localHour = DateTime.now().hour; - final response = await securePost( - { - 'newEmail': newEmail, - 'localHour': localHour, - }, - 'request-email-change', - ); - - // Remove loading indicator - if (mounted) Navigator.of(context).pop(); - - if (response == null) { - throw Exception('No response from server'); - } - - if (response.statusCode == 200) { - if (!mounted) return; - - // Navigate to waiting screen - final success = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => EmailChangeWaitingScreen(newEmail: newEmail), - ), - ); - - // If email was changed successfully, refresh account info - if (success == true && mounted) { - await fetchAccountInfo(); - } - } else { - final body = json.decode(response.body); - throw Exception(body['error'] ?? 'Failed to send verification email'); - } - } catch (e) { - // Remove loading indicator if still showing - if (mounted && Navigator.of(context).canPop()) { - Navigator.of(context).pop(); - } - - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e)); - } finally { - emailController.dispose(); - } - } else { - emailController.dispose(); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - backgroundColor: Theme.of(context).primaryColorLight, - foregroundColor: Colors.white, - title: Text( - 'Account', - style: GoogleFonts.aBeeZee(), - ), - ), - body: isLoading - ? Center( - child: CircularProgressIndicator( - color: Theme.of(context).primaryColorLight, - ), - ) - : SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Account Info Section - Card( - elevation: 2, - child: Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Center( - child: CircleAvatar( - radius: 50, - backgroundColor: Theme.of(context).primaryColorLight, - child: Icon( - Icons.person, - size: 60, - color: Colors.white, - ), - ), - ), - SizedBox(height: 20), - _buildInfoRow('Name', name ?? 'N/A'), - Divider(height: 30), - _buildInfoRow('Email', email ?? 'N/A'), - Divider(height: 30), - _buildInfoRow('Member Since', createdAt ?? 'N/A'), - ], - ), - ), - ), - SizedBox(height: 30), - - // Account Options Section - Text( - 'Account Options', - style: GoogleFonts.aBeeZee( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - SizedBox(height: 15), - Card( - elevation: 2, - child: Column( - children: [ - ListTile( - leading: Icon(Icons.email_outlined), - title: Text('Change Email'), - trailing: Icon(Icons.arrow_forward_ios, size: 16), - onTap: _handleChangeEmail, - ), - Divider(height: 1), - ListTile( - leading: Icon(Icons.lock_outline), - title: Text('Change Password'), - trailing: Icon(Icons.arrow_forward_ios, size: 16), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ChangePasswordScreen(), - ), - ); - }, - ), - ], - ), - ), - ], - ), - ), - ), - ); - } - - Widget _buildInfoRow(String label, String value) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: TextStyle( - fontSize: 14, - color: Colors.grey[600], - fontWeight: FontWeight.w500, - ), - ), - SizedBox(height: 5), - Text( - value, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), - ], - ); - } -} diff --git a/lib/BlindMasterScreens/addingDevices/set_device_name.dart b/lib/BlindMasterScreens/addingDevices/set_device_name.dart index c9326f4..77d3715 100644 --- a/lib/BlindMasterScreens/addingDevices/set_device_name.dart +++ b/lib/BlindMasterScreens/addingDevices/set_device_name.dart @@ -4,6 +4,7 @@ import 'dart:convert'; import 'package:blind_master/BlindMasterResources/error_snackbar.dart'; import 'package:blind_master/BlindMasterResources/secure_transmissions.dart'; import 'package:blind_master/BlindMasterResources/text_inputs.dart'; +import 'package:blind_master/BlindMasterResources/timezone_picker.dart'; import 'package:blind_master/BlindMasterScreens/home_screen.dart'; import 'package:blind_master/utils_from_FBPExample/extra.dart'; import 'package:flutter/material.dart'; @@ -29,6 +30,7 @@ class SetDeviceName extends StatefulWidget { class _SetDeviceNameState extends State { final deviceNameController = TextEditingController(); + String? _selectedTimezone; Widget? screen; StreamSubscription>? _authSub; @@ -53,11 +55,22 @@ class _SetDeviceNameState extends State { "Device Name (Different from others)", controller: deviceNameController, ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: TimezonePicker( + value: _selectedTimezone, + label: 'Device Timezone (optional)', + onChanged: (tz) => setState(() { + _selectedTimezone = tz; + initScreen(); + }), + ), + ), + const SizedBox(height: 16), ElevatedButton( onPressed: onPressed, - child: Text( - "Add to Account" - ) + child: const Text("Add to Account"), ), ], ); @@ -67,6 +80,7 @@ class _SetDeviceNameState extends State { final payload = { 'deviceName': name, 'maxPorts': widget.maxPorts, + if (_selectedTimezone != null) 'timezone': _selectedTimezone, }; String? token; try { diff --git a/lib/BlindMasterScreens/groupControl/create_group_dialog.dart b/lib/BlindMasterScreens/groupControl/create_group_dialog.dart index ba2b6b5..7d62280 100644 --- a/lib/BlindMasterScreens/groupControl/create_group_dialog.dart +++ b/lib/BlindMasterScreens/groupControl/create_group_dialog.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:blind_master/BlindMasterResources/secure_transmissions.dart'; import 'package:blind_master/BlindMasterResources/text_inputs.dart'; +import 'package:blind_master/BlindMasterResources/timezone_picker.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -17,6 +18,7 @@ class _CreateGroupDialogState extends State { List> devices = []; Map>> devicePeripherals = {}; Set selectedPeripheralIds = {}; + String? _selectedTimezone; bool isLoading = true; String? errorMessage; @@ -108,6 +110,7 @@ class _CreateGroupDialogState extends State { { 'name': _nameController.text.trim(), 'peripheral_ids': selectedPeripheralIds.toList(), + if (_selectedTimezone != null) 'timezone': _selectedTimezone, }, 'add_group' ); @@ -164,6 +167,12 @@ class _CreateGroupDialogState extends State { } }, ), + const SizedBox(height: 12), + TimezonePicker( + value: _selectedTimezone, + label: 'Group Timezone (optional)', + onChanged: (tz) => setState(() => _selectedTimezone = tz), + ), const SizedBox(height: 16), Container( padding: const EdgeInsets.all(10), diff --git a/lib/BlindMasterScreens/groupControl/edit_group_dialog.dart b/lib/BlindMasterScreens/groupControl/edit_group_dialog.dart index 9351c0b..78dd9e0 100644 --- a/lib/BlindMasterScreens/groupControl/edit_group_dialog.dart +++ b/lib/BlindMasterScreens/groupControl/edit_group_dialog.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:blind_master/BlindMasterResources/secure_transmissions.dart'; +import 'package:blind_master/BlindMasterResources/timezone_picker.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -21,19 +22,28 @@ class EditGroupDialog extends StatefulWidget { } class _EditGroupDialogState extends State { + late final TextEditingController _nameController; List> devices = []; Map>> devicePeripherals = {}; Set selectedPeripheralIds = {}; + String? _selectedTimezone; bool isLoading = true; String? errorMessage; @override void initState() { super.initState(); + _nameController = TextEditingController(); selectedPeripheralIds = widget.currentPeripheralIds.toSet(); _loadDevicesAndPeripherals(); } + @override + void dispose() { + _nameController.dispose(); + super.dispose(); + } + Future _loadDevicesAndPeripherals() async { setState(() { isLoading = true; @@ -41,6 +51,16 @@ class _EditGroupDialogState extends State { }); try { + // Fetch current group timezone + final tzResponse = await secureGet( + 'group_timezone', + queryParameters: {'groupId': widget.groupId.toString()}, + ); + if (tzResponse != null && tzResponse.statusCode == 200) { + final tzBody = jsonDecode(tzResponse.body); + _selectedTimezone = tzBody['timezone'] as String?; + } + // Fetch devices final devicesResponse = await secureGet('device_list'); if (devicesResponse == null || devicesResponse.statusCode != 200) { @@ -89,20 +109,42 @@ class _EditGroupDialogState extends State { } Future _updateGroup() async { + final newName = _nameController.text.trim(); if (selectedPeripheralIds.length < 2) { - setState(() { - errorMessage = 'Please select at least 2 blinds'; - }); + setState(() { errorMessage = 'Please select at least 2 blinds'; }); return; } try { + // Rename if a new name was entered + if (newName.isNotEmpty) { + final renameResponse = await securePost( + {'groupId': widget.groupId, 'newName': newName}, + 'rename_group', + ); + if (renameResponse == null) throw Exception('No response'); + if (renameResponse.statusCode == 409) { + setState(() { errorMessage = 'A group with this name already exists'; }); + return; + } + if (renameResponse.statusCode != 200) throw Exception('Failed to rename group'); + } + + // Update timezone if set + if (_selectedTimezone != null) { + final tzResponse = await securePost( + {'groupId': widget.groupId, 'timezone': _selectedTimezone}, + 'update_group_timezone', + ); + if (tzResponse == null || tzResponse.statusCode != 200) { + throw Exception('Failed to update timezone'); + } + } + + // Update members final response = await securePost( - { - 'groupId': widget.groupId, - 'peripheral_ids': selectedPeripheralIds.toList(), - }, - 'update_group' + {'groupId': widget.groupId, 'peripheral_ids': selectedPeripheralIds.toList()}, + 'update_group', ); if (response != null && response.statusCode == 200) { @@ -113,22 +155,16 @@ class _EditGroupDialogState extends State { backgroundColor: Colors.green, ), ); - Navigator.of(context).pop(); + Navigator.of(context).pop(newName.isNotEmpty ? newName : null); } } else if (response != null && response.statusCode == 409) { final errorBody = jsonDecode(response.body); - setState(() { - errorMessage = errorBody['error'] ?? 'This combination of blinds already exists'; - }); + setState(() { errorMessage = errorBody['error'] ?? 'This combination of blinds already exists'; }); } else { - setState(() { - errorMessage = 'Failed to update group'; - }); + setState(() { errorMessage = 'Failed to update group'; }); } } catch (e) { - setState(() { - errorMessage = 'Error updating group: ${e.toString()}'; - }); + setState(() { errorMessage = 'Error updating group: ${e.toString()}'; }); } } @@ -146,11 +182,28 @@ class _EditGroupDialogState extends State { : Column( mainAxisSize: MainAxisSize.min, children: [ + TextField( + controller: _nameController, + decoration: InputDecoration( + labelText: 'New Group Name (blank to keep current)', + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), + ), + onChanged: (_) { + if (errorMessage != null) setState(() => errorMessage = null); + }, + ), + const SizedBox(height: 12), + TimezonePicker( + value: _selectedTimezone, + label: 'Timezone (blank to keep current)', + onChanged: (tz) => setState(() => _selectedTimezone = tz), + ), + const SizedBox(height: 16), Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: selectedPeripheralIds.length >= 2 - ? Theme.of(context).primaryColorLight.withValues(alpha: 0.5) + ? Theme.of(context).primaryColorLight.withValues(alpha: 0.5) : Colors.orange.withValues(alpha: 0.5), borderRadius: BorderRadius.circular(10), ), diff --git a/lib/BlindMasterScreens/individualControl/peripheral_screen.dart b/lib/BlindMasterScreens/individualControl/peripheral_screen.dart index 9bba940..08c2453 100644 --- a/lib/BlindMasterScreens/individualControl/peripheral_screen.dart +++ b/lib/BlindMasterScreens/individualControl/peripheral_screen.dart @@ -5,6 +5,7 @@ import 'package:blind_master/BlindMasterResources/blindmaster_progress_indicator import 'package:blind_master/BlindMasterResources/error_snackbar.dart'; import 'package:blind_master/BlindMasterResources/secure_transmissions.dart'; import 'package:blind_master/BlindMasterResources/text_inputs.dart'; +import 'package:blind_master/BlindMasterResources/timezone_picker.dart'; import 'package:blind_master/BlindMasterScreens/schedules_screen.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -38,6 +39,8 @@ class _PeripheralScreenState extends State { bool _movementPending = false; bool _awaitingDeviceWake = false; + String? _deviceTimezone; + final _peripheralRenameController = TextEditingController(); void getImage() { @@ -360,9 +363,28 @@ class _PeripheralScreenState extends State { if (response.statusCode != 200) throw Exception("Server Error"); final body = json.decode(response.body); if (!mounted) return; - setState(() => batterySoc = body['battery_soc'] as int?); + setState(() { + batterySoc = body['battery_soc'] as int?; + _deviceTimezone = body['timezone'] as String?; + }); } catch (e) { - // Battery SOC is non-critical; swallow silently + // Non-critical; swallow silently + } + } + + Future updateDeviceTimezone(String timezone) async { + try { + final response = await securePost( + {'deviceId': widget.deviceId, 'timezone': timezone}, + 'update_device_timezone', + ); + if (response == null) throw Exception("Auth Error"); + if (response.statusCode != 200) throw Exception("Server Error"); + if (!mounted) return; + setState(() => _deviceTimezone = timezone); + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e)); } } @@ -442,47 +464,67 @@ class _PeripheralScreenState extends State { } void rename() { + String? dialogTimezone = _deviceTimezone; showDialog( context: context, builder: (BuildContext dialogContext) { - return AlertDialog( - title: Text( - widget.isSinglePort ? "Rename Device" : "Rename Peripheral", - style: GoogleFonts.aBeeZee(), - ), - content: BlindMasterMainInput( - widget.isSinglePort ? "New Device Name" : "New Peripheral Name", - controller: _peripheralRenameController, - ), - actions: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - ElevatedButton( - onPressed: () { - Navigator.of(dialogContext).pop(); - }, - child: const Text( - "Cancel", - style: TextStyle(color: Colors.red), - ) + return StatefulBuilder( + builder: (context, setDialogState) { + return AlertDialog( + title: Text( + widget.isSinglePort ? "Rename Device" : "Rename Peripheral", + style: GoogleFonts.aBeeZee(), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + BlindMasterMainInput( + widget.isSinglePort + ? "New Device Name (blank to keep current)" + : "New Peripheral Name (blank to keep current)", + controller: _peripheralRenameController, + ), + if (widget.isSinglePort) ...[ + const SizedBox(height: 16), + TimezonePicker( + value: dialogTimezone, + label: 'Device Timezone (blank to keep current)', + onChanged: (tz) => setDialogState(() => dialogTimezone = tz), + ), + ], + ], + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + ElevatedButton( + onPressed: () => Navigator.of(dialogContext).pop(), + child: const Text( + "Cancel", + style: TextStyle(color: Colors.red), + ), + ), + ElevatedButton( + onPressed: () { + final newName = _peripheralRenameController.text.trim(); + if (widget.isSinglePort) { + if (newName.isNotEmpty) updateDeviceName(newName); + if (dialogTimezone != null) updateDeviceTimezone(dialogTimezone!); + } else { + if (newName.isNotEmpty) updatePeriphName(newName, widget.peripheralId); + } + Navigator.of(dialogContext).pop(); + }, + child: const Text("Confirm"), + ), + ], ), - ElevatedButton( - onPressed: () { - if (widget.isSinglePort) { - updateDeviceName(_peripheralRenameController.text); - } else { - updatePeriphName(_peripheralRenameController.text, widget.peripheralId); - } - Navigator.of(dialogContext).pop(); - }, - child: const Text("Confirm") - ) ], - ) - ], + ); + }, ); - } + }, ); }