diff --git a/lib/main.dart b/lib/main.dart index c544f227..ac002f85 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:go_router/go_router.dart'; import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart'; import 'package:syncrow_web/pages/home/bloc/home_bloc.dart'; import 'package:syncrow_web/pages/home/bloc/home_event.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart'; import 'package:syncrow_web/services/locator.dart'; import 'package:syncrow_web/utils/app_routes.dart'; @@ -14,7 +15,8 @@ import 'package:syncrow_web/utils/theme/theme.dart'; Future main() async { try { - const environment = String.fromEnvironment('FLAVOR', defaultValue: 'development'); + const environment = + String.fromEnvironment('FLAVOR', defaultValue: 'development'); await dotenv.load(fileName: '.env.$environment'); WidgetsFlutterBinding.ensureInitialized(); initialSetup(); @@ -46,10 +48,14 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MultiBlocProvider( providers: [ - BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())), + BlocProvider( + create: (context) => HomeBloc()..add(const FetchUserInfo())), BlocProvider( create: (context) => VisitorPasswordBloc(), - ) + ), + BlocProvider( + create: (context) => RoutineBloc(), + ), ], child: MaterialApp.router( debugShowCheckedModeBanner: false, diff --git a/lib/pages/routiens/helper/ac_helper.dart b/lib/pages/routiens/helper/ac_helper.dart index b14acd3a..9ba0b224 100644 --- a/lib/pages/routiens/helper/ac_helper.dart +++ b/lib/pages/routiens/helper/ac_helper.dart @@ -1,219 +1,330 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/models/ac/ac_function.dart'; import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class ACHelper { - static Future showACFunctionsDialog( + static Future?> showACFunctionsDialog( BuildContext context, List> functions, ) async { List acFunctions = functions.whereType().toList(); - // Track multiple selections using a map - Map selectedValues = {}; - List selectedFunctions = []; + String? selectedFunction; + dynamic selectedValue = 20; + String? selectedCondition = "=="; + List _selectedConditions = [false, true, false]; - await showDialog( + return showDialog?>( context: context, builder: (BuildContext context) { - return Dialog( - child: Container( - width: 600, - height: 450, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(20), - ), - padding: const EdgeInsets.only(top: 20), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'AC Functions', - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: ColorsManager.primaryColorWithOpacity, - fontWeight: FontWeight.bold, - ), - ), - Padding( - padding: - const EdgeInsets.symmetric(vertical: 15, horizontal: 50), - child: Container( - height: 1, - width: double.infinity, - color: ColorsManager.greyColor, - ), - ), - Expanded( - child: Row( - children: [ - Expanded( - child: ListView.separated( - itemCount: acFunctions.length, - separatorBuilder: (_, __) => const Divider( - color: ColorsManager.dividerColor, - ), - itemBuilder: (context, index) { - final function = acFunctions[index]; - final isSelected = - selectedValues.containsKey(function.code); - return ListTile( - tileColor: - isSelected ? Colors.grey.shade100 : null, - leading: SvgPicture.asset( - function.icon, - width: 24, - height: 24, - ), - title: Text( - function.operationName, - style: context.textTheme.bodyMedium, - ), - trailing: isSelected - ? Icon( - Icons.check_circle, - color: - ColorsManager.primaryColorWithOpacity, - size: 20, - ) - : const Icon( - Icons.arrow_forward_ios, - size: 16, - color: ColorsManager.textGray, - ), - onTap: () { - if (isSelected) { - selectedValues.remove(function.code); - selectedFunctions.removeWhere( - (f) => f.function == function.code); - } - (context as Element).markNeedsBuild(); - }, - ); - }, - ), - ), - Expanded( - child: Builder( - builder: (context) { - final selectedFunction = acFunctions.firstWhere( - (f) => selectedValues.containsKey(f.code), - orElse: () => acFunctions.first, - ); - return _buildValueSelector( - context, - selectedFunction, - selectedValues[selectedFunction.code], - (value) { - selectedValues[selectedFunction.code] = value; - // Update or add the function data - final functionData = DeviceFunctionData( - entityId: selectedFunction.deviceId, - function: selectedFunction.code, - operationName: selectedFunction.operationName, - value: value, - valueDescription: _getValueDescription( - selectedFunction, value), - ); - - final existingIndex = - selectedFunctions.indexWhere((f) => - f.function == selectedFunction.code); - if (existingIndex != -1) { - selectedFunctions[existingIndex] = - functionData; - } else { - selectedFunctions.add(functionData); - } - - (context as Element).markNeedsBuild(); - }, - ); - }, - ), - ), - ], - ), - ), - Container( - height: 1, - width: double.infinity, - color: ColorsManager.greyColor, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text( - 'Cancel', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: ColorsManager.greyColor), - ), - ), - TextButton( - onPressed: selectedFunctions.isNotEmpty - ? () { - // Add all selected functions to the bloc - for (final function in selectedFunctions) { - context - .read() - .add(AddFunction(function)); - } - Navigator.pop(context, true); - } - : null, - child: Text( - 'Confirm', - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: ColorsManager.primaryColorWithOpacity, - ), - ), - ), - ], - ), - ], - ), - ), + return StatefulBuilder( + builder: (context, setState) { + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: _buildDialogContent( + context, + setState, + acFunctions, + selectedFunction, + selectedValue, + selectedCondition, + _selectedConditions, + (fn) => selectedFunction = fn, + (val) => selectedValue = val, + (cond) => selectedCondition = cond, + ), + ); + }, ); }, ); } - static Widget _buildValueSelector( + /// Build dialog content for AC functions dialog + static Widget _buildDialogContent( BuildContext context, - ACFunction function, + StateSetter setState, + List acFunctions, + String? selectedFunction, dynamic selectedValue, + String? selectedCondition, + List selectedConditions, + Function(String?) onFunctionSelected, Function(dynamic) onValueSelected, + Function(String?) onConditionSelected, ) { - final values = function.getOperationalValues(); return Container( - height: 200, - padding: const EdgeInsets.symmetric(horizontal: 20), - child: ListView.builder( - itemCount: values.length, + width: selectedFunction != null ? 600 : 360, + height: 450, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.only(top: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const DialogHeader('AC Functions'), + Flexible( + child: Row( + children: [ + _buildFunctionsList( + context, + setState, + acFunctions, + selectedFunction, + onFunctionSelected, + ), + if (selectedFunction != null) + _buildValueSelector( + context, + setState, + selectedFunction, + selectedValue, + selectedCondition, + selectedConditions, + onValueSelected, + onConditionSelected, + acFunctions, + ), + ], + ), + ), + DialogFooter( + onCancel: () => Navigator.pop(context), + onConfirm: selectedFunction != null && selectedValue != null + ? () => Navigator.pop(context, { + 'function': selectedFunction, + 'value': selectedValue, + 'condition': selectedCondition ?? "==", + }) + : null, + isConfirmEnabled: selectedFunction != null && selectedValue != null, + ), + ], + ), + ); + } + + /// Build functions list for AC functions dialog + static Widget _buildFunctionsList( + BuildContext context, + StateSetter setState, + List acFunctions, + String? selectedFunction, + Function(String?) onFunctionSelected, + ) { + return Expanded( + child: ListView.separated( + shrinkWrap: false, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: acFunctions.length, + separatorBuilder: (context, index) => const Divider( + color: ColorsManager.dividerColor, + ), itemBuilder: (context, index) { - final value = values[index]; - return RadioListTile( - value: value.value, - groupValue: selectedValue, - onChanged: onValueSelected, - title: Text(value.description), + final function = acFunctions[index]; + return ListTile( + leading: SvgPicture.asset( + function.icon, + width: 24, + height: 24, + ), + title: Text( + function.operationName, + style: context.textTheme.bodyMedium, + ), + trailing: const Icon( + Icons.arrow_forward_ios, + size: 16, + color: ColorsManager.textGray, + ), + onTap: () => setState(() => onFunctionSelected(function.code)), ); }, ), ); } - static String _getValueDescription(ACFunction function, dynamic value) { - final values = function.getOperationalValues(); - final selectedValue = values.firstWhere((v) => v.value == value); - return selectedValue.description; + /// Build value selector for AC functions dialog + static Widget _buildValueSelector( + BuildContext context, + StateSetter setState, + String selectedFunction, + dynamic selectedValue, + String? selectedCondition, + List selectedConditions, + Function(dynamic) onValueSelected, + Function(String?) onConditionSelected, + List acFunctions, + ) { + if (selectedFunction == 'temp_set' || selectedFunction == 'temp_current') { + return Expanded( + child: _buildTemperatureSelector( + context, + setState, + selectedValue, + selectedCondition, + selectedConditions, + onValueSelected, + onConditionSelected, + ), + ); + } + + final selectedFn = + acFunctions.firstWhere((f) => f.code == selectedFunction); + final values = selectedFn.getOperationalValues(); + return Expanded( + child: _buildOperationalValuesList( + context, + setState, + values, + selectedValue, + onValueSelected, + ), + ); + } + + /// Build temperature selector for AC functions dialog + static Widget _buildTemperatureSelector( + BuildContext context, + StateSetter setState, + dynamic selectedValue, + String? selectedCondition, + List selectedConditions, + Function(dynamic) onValueSelected, + Function(String?) onConditionSelected, + ) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildConditionToggle( + context, + setState, + selectedConditions, + onConditionSelected, + ), + const SizedBox(height: 20), + _buildTemperatureDisplay(context, selectedValue), + const SizedBox(height: 20), + _buildTemperatureSlider( + context, + setState, + selectedValue, + onValueSelected, + ), + ], + ); + } + + /// Build condition toggle for AC functions dialog + static Widget _buildConditionToggle( + BuildContext context, + StateSetter setState, + List selectedConditions, + Function(String?) onConditionSelected, + ) { + return ToggleButtons( + onPressed: (int index) { + setState(() { + for (int i = 0; i < selectedConditions.length; i++) { + selectedConditions[i] = i == index; + } + onConditionSelected(index == 0 + ? "<" + : index == 1 + ? "==" + : ">"); + }); + }, + borderRadius: const BorderRadius.all(Radius.circular(8)), + selectedBorderColor: ColorsManager.primaryColorWithOpacity, + selectedColor: Colors.white, + fillColor: ColorsManager.primaryColorWithOpacity, + color: ColorsManager.primaryColorWithOpacity, + constraints: const BoxConstraints( + minHeight: 40.0, + minWidth: 40.0, + ), + isSelected: selectedConditions, + children: const [Text("<"), Text("="), Text(">")], + ); + } + + /// Build temperature display for AC functions dialog + static Widget _buildTemperatureDisplay( + BuildContext context, dynamic selectedValue) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + decoration: BoxDecoration( + color: ColorsManager.primaryColorWithOpacity.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + '${selectedValue ?? 20}°C', + style: context.textTheme.headlineMedium!.copyWith( + color: ColorsManager.primaryColorWithOpacity, + ), + ), + ); + } + + static Widget _buildTemperatureSlider( + BuildContext context, + StateSetter setState, + dynamic selectedValue, + Function(dynamic) onValueSelected, + ) { + final currentValue = selectedValue is int ? selectedValue.toDouble() : 20.0; + return Slider( + value: currentValue, + min: 16, + max: 30, + divisions: 14, + label: '${currentValue.toInt()}°C', + onChanged: (value) { + setState(() => onValueSelected(value.toInt())); + }, + ); + } + + static Widget _buildOperationalValuesList( + BuildContext context, + StateSetter setState, + List values, + dynamic selectedValue, + Function(dynamic) onValueSelected, + ) { + return ListView.builder( + shrinkWrap: false, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: values.length, + itemBuilder: (context, index) { + final value = values[index]; + return ListTile( + leading: SvgPicture.asset( + value.icon, + width: 24, + height: 24, + ), + title: Text( + value.description, + style: context.textTheme.bodyMedium, + ), + trailing: Radio( + value: value.value, + groupValue: selectedValue, + onChanged: (newValue) { + setState(() => onValueSelected(newValue)); + }, + ), + ); + }, + ); } } diff --git a/lib/pages/routiens/helper/one_gang_switch_helper.dart b/lib/pages/routiens/helper/one_gang_switch_helper.dart index c9a79377..0837829b 100644 --- a/lib/pages/routiens/helper/one_gang_switch_helper.dart +++ b/lib/pages/routiens/helper/one_gang_switch_helper.dart @@ -1,34 +1,38 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart'; import 'package:syncrow_web/pages/routiens/models/gang_switches/one_gang_switch/one_gang_switch.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class OneGangSwitchHelper { - static Future showSwitchFunctionsDialog( - BuildContext context, List> functions) async { + static Future?> showSwitchFunctionsDialog( + BuildContext context, + List> functions, + ) async { List> switchFunctions = functions .where( (f) => f is OneGangSwitchFunction || f is OneGangCountdownFunction) .toList(); - Map selectedValues = {}; - List selectedFunctions = []; - String? selectedCondition = "<"; - List selectedConditions = [true, false, false]; + final selectedFunctionNotifier = ValueNotifier(null); + final selectedValueNotifier = ValueNotifier(null); + final selectedConditionNotifier = ValueNotifier("<"); + final selectedConditionsNotifier = + ValueNotifier>([true, false, false]); - await showDialog( + return showDialog?>( context: context, builder: (BuildContext context) { - return StatefulBuilder( - builder: (context, setState) { + return ValueListenableBuilder( + valueListenable: selectedFunctionNotifier, + builder: (context, selectedFunction, _) { return AlertDialog( contentPadding: EdgeInsets.zero, content: Container( - width: 600, + width: selectedFunction != null ? 600 : 360, height: 450, decoration: BoxDecoration( color: Colors.white, @@ -38,22 +42,7 @@ class OneGangSwitchHelper { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text( - '1 Gang Light Switch Condition', - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: ColorsManager.primaryColorWithOpacity, - fontWeight: FontWeight.bold, - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 15, horizontal: 50), - child: Container( - height: 1, - width: double.infinity, - color: ColorsManager.greyColor, - ), - ), + const DialogHeader('1 Gang Light Switch Condition'), Expanded( child: Row( children: [ @@ -66,143 +55,67 @@ class OneGangSwitchHelper { ), itemBuilder: (context, index) { final function = switchFunctions[index]; - final isSelected = - selectedValues.containsKey(function.code); - return ListTile( - tileColor: - isSelected ? Colors.grey.shade100 : null, - leading: SvgPicture.asset( - function.icon, - width: 24, - height: 24, - ), - title: Text( - function.operationName, - style: context.textTheme.bodyMedium, - ), - trailing: isSelected - ? Icon( - Icons.check_circle, - color: ColorsManager - .primaryColorWithOpacity, - size: 20, - ) - : const Icon( - Icons.arrow_forward_ios, - size: 16, - color: ColorsManager.textGray, - ), - onTap: () { - if (isSelected) { - selectedValues.remove(function.code); - selectedFunctions.removeWhere( - (f) => f.function == function.code); - } - (context as Element).markNeedsBuild(); - }, - ); - }, - ), - ), - // Right side: Value selector - Expanded( - child: Builder( - builder: (context) { - final selectedFn = switchFunctions.firstWhere( - (f) => selectedValues.containsKey(f.code), - orElse: () => switchFunctions.first, - ) as BaseSwitchFunction; - - if (selectedFn is OneGangCountdownFunction) { - return _buildCountDownSelector( - context, - setState, - selectedValues[selectedFn.code] ?? 0, - selectedCondition, - selectedConditions, - (value) { - selectedValues[selectedFn.code] = value; - final functionData = DeviceFunctionData( - entityId: selectedFn.deviceId, - function: selectedFn.code, - operationName: selectedFn.operationName, - value: value, - condition: selectedCondition, - valueDescription: '${value} sec', - ); - - final existingIndex = - selectedFunctions.indexWhere((f) => - f.function == selectedFn.code); - if (existingIndex != -1) { - selectedFunctions[existingIndex] = - functionData; - } else { - selectedFunctions.add(functionData); - } - (context as Element).markNeedsBuild(); - }, - (condition) { - setState(() { - selectedCondition = condition; - }); - }, - ); - } - - final values = - selectedFn.getOperationalValues(); - return ListView.builder( - itemCount: values.length, - itemBuilder: (context, index) { - final value = values[index]; + return ValueListenableBuilder( + valueListenable: selectedFunctionNotifier, + builder: (context, selectedFunction, _) { + final isSelected = + selectedFunction == function.code; return ListTile( + tileColor: isSelected + ? Colors.grey.shade100 + : null, leading: SvgPicture.asset( - value.icon, + function.icon, width: 24, height: 24, ), title: Text( - value.description, + function.operationName, style: context.textTheme.bodyMedium, ), - trailing: Radio( - value: value.value, - groupValue: - selectedValues[selectedFn.code], - onChanged: (newValue) { - selectedValues[selectedFn.code] = - newValue; - final functionData = - DeviceFunctionData( - entityId: selectedFn.deviceId, - function: selectedFn.code, - operationName: - selectedFn.operationName, - value: newValue, - valueDescription: value.description, - ); - - final existingIndex = - selectedFunctions.indexWhere( - (f) => - f.function == - selectedFn.code); - if (existingIndex != -1) { - selectedFunctions[existingIndex] = - functionData; - } else { - selectedFunctions.add(functionData); - } - (context as Element).markNeedsBuild(); - }, + trailing: const Icon( + Icons.arrow_forward_ios, + size: 16, + color: ColorsManager.textGray, ), + onTap: () { + selectedFunctionNotifier.value = + function.code; + selectedValueNotifier.value = + function is OneGangCountdownFunction + ? 0 + : null; + }, ); }, ); }, ), ), + // Right side: Value selector + if (selectedFunction != null) + ValueListenableBuilder( + valueListenable: selectedFunctionNotifier, + builder: (context, selectedFunction, _) { + final selectedFn = switchFunctions.firstWhere( + (f) => f.code == selectedFunction, + ); + return Expanded( + child: selectedFn is OneGangCountdownFunction + ? _buildCountDownSelector( + context, + selectedValueNotifier, + selectedConditionNotifier, + selectedConditionsNotifier, + ) + : _buildOperationalValuesList( + context, + selectedFn as BaseSwitchFunction, + selectedValueNotifier, + ), + ); + }, + ), ], ), ), @@ -211,41 +124,36 @@ class OneGangSwitchHelper { width: double.infinity, color: ColorsManager.greyColor, ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text( - 'Cancel', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: ColorsManager.greyColor), - ), - ), - TextButton( - onPressed: selectedFunctions.isNotEmpty - ? () { - for (final function in selectedFunctions) { - context - .read() - .add(AddFunction(function)); - } - Navigator.pop(context, true); - } - : null, - child: Text( - 'Confirm', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith( - color: ColorsManager.primaryColorWithOpacity, - ), - ), - ), - ], + DialogFooter( + onCancel: () => Navigator.pop(context), + onConfirm: selectedFunctionNotifier.value != null && + selectedValueNotifier.value != null + ? () { + final selectedFn = switchFunctions.firstWhere( + (f) => f.code == selectedFunctionNotifier.value, + ); + final value = selectedValueNotifier.value; + final functionData = DeviceFunctionData( + entityId: selectedFn.deviceId, + function: selectedFn.code, + operationName: selectedFn.operationName, + value: value, + condition: selectedConditionNotifier.value, + valueDescription: + selectedFn is OneGangCountdownFunction + ? '${value} sec' + : ((selectedFn as BaseSwitchFunction) + .getOperationalValues() + .firstWhere((v) => v.value == value) + .description), + ); + Navigator.pop( + context, {selectedFn.code: functionData}); + } + : null, + isConfirmEnabled: + selectedFunctionNotifier.value != null && + selectedValueNotifier.value != null, ), ], ), @@ -257,77 +165,101 @@ class OneGangSwitchHelper { ); } - /// Build countdown selector for switch functions dialog static Widget _buildCountDownSelector( BuildContext context, - StateSetter setState, - dynamic selectedValue, - String? selectedCondition, - List selectedConditions, - Function(dynamic) onValueSelected, - Function(String?) onConditionSelected, + ValueNotifier valueNotifier, + ValueNotifier conditionNotifier, + ValueNotifier> conditionsNotifier, ) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildConditionToggle( - context, - setState, - selectedConditions, - onConditionSelected, - ), - const SizedBox(height: 20), - Text( - '${selectedValue.toString()} sec', - style: Theme.of(context).textTheme.headlineMedium, - ), - const SizedBox(height: 20), - Slider( - value: selectedValue.toDouble(), - min: 0, - max: 300, // 5 minutes in seconds - divisions: 300, - onChanged: (value) { - setState(() { - onValueSelected(value.toInt()); - }); - }, - ), - ], + return ValueListenableBuilder( + valueListenable: valueNotifier, + builder: (context, value, _) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ValueListenableBuilder>( + valueListenable: conditionsNotifier, + builder: (context, selectedConditions, _) { + return ToggleButtons( + onPressed: (int index) { + final newConditions = List.filled(3, false); + newConditions[index] = true; + conditionsNotifier.value = newConditions; + conditionNotifier.value = index == 0 + ? "<" + : index == 1 + ? "==" + : ">"; + }, + borderRadius: const BorderRadius.all(Radius.circular(8)), + selectedBorderColor: ColorsManager.primaryColorWithOpacity, + selectedColor: Colors.white, + fillColor: ColorsManager.primaryColorWithOpacity, + color: ColorsManager.primaryColorWithOpacity, + constraints: const BoxConstraints( + minHeight: 40.0, + minWidth: 40.0, + ), + isSelected: selectedConditions, + children: const [Text("<"), Text("="), Text(">")], + ); + }, + ), + const SizedBox(height: 20), + Text( + '${value ?? 0} sec', + style: Theme.of(context).textTheme.headlineMedium, + ), + const SizedBox(height: 20), + Slider( + value: (value ?? 0).toDouble(), + min: 0, + max: 300, + divisions: 300, + onChanged: (newValue) { + valueNotifier.value = newValue.toInt(); + }, + ), + ], + ); + }, ); } - /// Build condition toggle for AC functions dialog - static Widget _buildConditionToggle( + static Widget _buildOperationalValuesList( BuildContext context, - StateSetter setState, - List selectedConditions, - Function(String?) onConditionSelected, + BaseSwitchFunction function, + ValueNotifier valueNotifier, ) { - return ToggleButtons( - onPressed: (int index) { - setState(() { - for (int i = 0; i < selectedConditions.length; i++) { - selectedConditions[i] = i == index; - } - onConditionSelected(index == 0 - ? "<" - : index == 1 - ? "==" - : ">"); - }); + final values = function.getOperationalValues(); + return ValueListenableBuilder( + valueListenable: valueNotifier, + builder: (context, selectedValue, _) { + return ListView.builder( + itemCount: values.length, + itemBuilder: (context, index) { + final value = values[index]; + return ListTile( + leading: SvgPicture.asset( + value.icon, + width: 24, + height: 24, + ), + title: Text( + value.description, + style: context.textTheme.bodyMedium, + ), + trailing: Radio( + value: value.value, + groupValue: selectedValue, + onChanged: (newValue) { + valueNotifier.value = newValue; + }, + ), + ); + }, + ); }, - borderRadius: const BorderRadius.all(Radius.circular(8)), - selectedBorderColor: ColorsManager.primaryColorWithOpacity, - selectedColor: Colors.white, - fillColor: ColorsManager.primaryColorWithOpacity, - color: ColorsManager.primaryColorWithOpacity, - constraints: const BoxConstraints( - minHeight: 40.0, - minWidth: 40.0, - ), - isSelected: selectedConditions, - children: const [Text("<"), Text("="), Text(">")], ); } } diff --git a/lib/pages/routiens/helper/two_gang_switch_helper.dart b/lib/pages/routiens/helper/two_gang_switch_helper.dart index 6f2febc7..ae6abac4 100644 --- a/lib/pages/routiens/helper/two_gang_switch_helper.dart +++ b/lib/pages/routiens/helper/two_gang_switch_helper.dart @@ -5,6 +5,8 @@ import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart'; import 'package:syncrow_web/pages/routiens/models/gang_switches/two_gang_switch/two_gang_switch.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; @@ -18,20 +20,23 @@ class TwoGangSwitchHelper { f is TwoGangCountdown1Function || f is TwoGangCountdown2Function) .toList(); - Map selectedValues = {}; - List selectedFunctions = []; - String? selectedCondition = "<"; - List selectedConditions = [true, false, false]; + + final selectedFunctionNotifier = ValueNotifier(null); + final selectedValueNotifier = ValueNotifier(null); + final selectedConditionNotifier = ValueNotifier('<'); + final selectedConditionsNotifier = + ValueNotifier>([true, false, false]); await showDialog( context: context, builder: (BuildContext context) { - return StatefulBuilder( - builder: (context, setState) { + return ValueListenableBuilder( + valueListenable: selectedFunctionNotifier, + builder: (context, selectedFunction, _) { return AlertDialog( contentPadding: EdgeInsets.zero, content: Container( - width: 600, + width: selectedFunction != null ? 600 : 300, height: 450, decoration: BoxDecoration( color: Colors.white, @@ -41,22 +46,7 @@ class TwoGangSwitchHelper { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text( - '2 Gangs Light Switch Condition', - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: ColorsManager.primaryColorWithOpacity, - fontWeight: FontWeight.bold, - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 15, horizontal: 50), - child: Container( - height: 1, - width: double.infinity, - color: ColorsManager.greyColor, - ), - ), + const DialogHeader('2 Gangs Light Switch Condition'), Expanded( child: Row( children: [ @@ -69,187 +59,105 @@ class TwoGangSwitchHelper { ), itemBuilder: (context, index) { final function = switchFunctions[index]; - final isSelected = - selectedValues.containsKey(function.code); - return ListTile( - tileColor: - isSelected ? Colors.grey.shade100 : null, - leading: SvgPicture.asset( - function.icon, - width: 24, - height: 24, - ), - title: Text( - function.operationName, - style: context.textTheme.bodyMedium, - ), - trailing: isSelected - ? Icon( - Icons.check_circle, - color: ColorsManager - .primaryColorWithOpacity, - size: 20, - ) - : const Icon( - Icons.arrow_forward_ios, - size: 16, - color: ColorsManager.textGray, - ), - onTap: () { - if (isSelected) { - selectedValues.remove(function.code); - selectedFunctions.removeWhere( - (f) => f.function == function.code); - } - (context as Element).markNeedsBuild(); - }, - ); - }, - ), - ), - // Right side: Value selector - Expanded( - child: Builder( - builder: (context) { - final selectedFn = switchFunctions.firstWhere( - (f) => selectedValues.containsKey(f.code), - orElse: () => switchFunctions.first, - ) as BaseSwitchFunction; - - if (selectedFn is TwoGangCountdown1Function || - selectedFn is TwoGangCountdown2Function) { - return _buildCountDownSelector( - context, - setState, - selectedValues[selectedFn.code] ?? 0, - selectedCondition, - selectedConditions, - (value) { - selectedValues[selectedFn.code] = value; - final functionData = DeviceFunctionData( - entityId: selectedFn.deviceId, - function: selectedFn.code, - operationName: selectedFn.operationName, - value: value, - condition: selectedCondition, - valueDescription: '${value} sec', - ); - - final existingIndex = - selectedFunctions.indexWhere((f) => - f.function == selectedFn.code); - if (existingIndex != -1) { - selectedFunctions[existingIndex] = - functionData; - } else { - selectedFunctions.add(functionData); - } - (context as Element).markNeedsBuild(); - }, - (condition) { - setState(() { - selectedCondition = condition; - }); - }, - ); - } - - final values = - selectedFn.getOperationalValues(); - return ListView.builder( - itemCount: values.length, - itemBuilder: (context, index) { - final value = values[index]; + return ValueListenableBuilder( + valueListenable: selectedFunctionNotifier, + builder: (context, selectedFunction, _) { + final isSelected = + selectedFunction == function.code; return ListTile( + tileColor: isSelected + ? Colors.grey.shade100 + : null, leading: SvgPicture.asset( - value.icon, + function.icon, width: 24, height: 24, ), title: Text( - value.description, + function.operationName, style: context.textTheme.bodyMedium, ), - trailing: Radio( - value: value.value, - groupValue: - selectedValues[selectedFn.code], - onChanged: (newValue) { - selectedValues[selectedFn.code] = - newValue; - final functionData = - DeviceFunctionData( - entityId: selectedFn.deviceId, - function: selectedFn.code, - operationName: - selectedFn.operationName, - value: newValue, - valueDescription: value.description, - ); - - final existingIndex = - selectedFunctions.indexWhere( - (f) => - f.function == - selectedFn.code); - if (existingIndex != -1) { - selectedFunctions[existingIndex] = - functionData; - } else { - selectedFunctions.add(functionData); - } - (context as Element).markNeedsBuild(); - }, + trailing: const Icon( + Icons.arrow_forward_ios, + size: 16, + color: ColorsManager.textGray, ), + onTap: () { + selectedFunctionNotifier.value = + function.code; + selectedValueNotifier.value = function + is TwoGangCountdown1Function || + function + is TwoGangCountdown2Function + ? 0 + : null; + }, ); }, ); }, ), ), + // Right side: Value selector + if (selectedFunction != null) + Expanded( + child: ValueListenableBuilder( + valueListenable: selectedValueNotifier, + builder: (context, selectedValue, _) { + final selectedFn = switchFunctions.firstWhere( + (f) => f.code == selectedFunction, + ); + + if (selectedFn is TwoGangCountdown1Function || + selectedFn is TwoGangCountdown2Function) { + return _buildCountDownSelector( + context, + selectedValueNotifier, + selectedConditionNotifier, + selectedConditionsNotifier, + ); + } + + return _buildOperationalValuesList( + context, + selectedFn as BaseSwitchFunction, + selectedValueNotifier, + ); + }, + ), + ), ], ), ), - Container( - height: 1, - width: double.infinity, - color: ColorsManager.greyColor, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text( - 'Cancel', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: ColorsManager.greyColor), - ), - ), - TextButton( - onPressed: selectedFunctions.isNotEmpty - ? () { - for (final function in selectedFunctions) { - context - .read() - .add(AddFunction(function)); - } - Navigator.pop(context, true); - } - : null, - child: Text( - 'Confirm', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith( - color: ColorsManager.primaryColorWithOpacity, - ), - ), - ), - ], + DialogFooter( + onCancel: () => Navigator.pop(context), + onConfirm: selectedFunction != null && + selectedValueNotifier.value != null + ? () { + final selectedFn = switchFunctions.firstWhere( + (f) => f.code == selectedFunction, + ); + final value = selectedValueNotifier.value; + final functionData = DeviceFunctionData( + entityId: selectedFn.deviceId, + function: selectedFn.code, + operationName: selectedFn.operationName, + value: value, + condition: selectedConditionNotifier.value, + valueDescription: selectedFn + is TwoGangCountdown1Function || + selectedFn is TwoGangCountdown2Function + ? '${value} sec' + : ((selectedFn as BaseSwitchFunction) + .getOperationalValues() + .firstWhere((v) => v.value == value) + .description), + ); + Navigator.pop( + context, {selectedFn.code: functionData}); + } + : null, + isConfirmEnabled: selectedFunction != null, ), ], ), @@ -261,77 +169,101 @@ class TwoGangSwitchHelper { ); } - /// Build countdown selector for switch functions dialog - static Widget _buildCountDownSelector( + static Widget _buildOperationalValuesList( BuildContext context, - StateSetter setState, - dynamic selectedValue, - String? selectedCondition, - List selectedConditions, - Function(dynamic) onValueSelected, - Function(String?) onConditionSelected, + BaseSwitchFunction function, + ValueNotifier valueNotifier, ) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildConditionToggle( - context, - setState, - selectedConditions, - onConditionSelected, - ), - const SizedBox(height: 20), - Text( - '${selectedValue.toString()} sec', - style: Theme.of(context).textTheme.headlineMedium, - ), - const SizedBox(height: 20), - Slider( - value: selectedValue.toDouble(), - min: 0, - max: 300, // 5 minutes in seconds - divisions: 300, - onChanged: (value) { - setState(() { - onValueSelected(value.toInt()); - }); + final values = function.getOperationalValues(); + return ValueListenableBuilder( + valueListenable: valueNotifier, + builder: (context, selectedValue, _) { + return ListView.builder( + itemCount: values.length, + itemBuilder: (context, index) { + final value = values[index]; + return ListTile( + leading: SvgPicture.asset( + value.icon, + width: 24, + height: 24, + ), + title: Text( + value.description, + style: context.textTheme.bodyMedium, + ), + trailing: Radio( + value: value.value, + groupValue: selectedValue, + onChanged: (newValue) { + valueNotifier.value = newValue; + }, + ), + ); }, - ), - ], + ); + }, ); } - /// Build condition toggle for AC functions dialog - static Widget _buildConditionToggle( + static Widget _buildCountDownSelector( BuildContext context, - StateSetter setState, - List selectedConditions, - Function(String?) onConditionSelected, + ValueNotifier valueNotifier, + ValueNotifier conditionNotifier, + ValueNotifier> conditionsNotifier, ) { - return ToggleButtons( - onPressed: (int index) { - setState(() { - for (int i = 0; i < selectedConditions.length; i++) { - selectedConditions[i] = i == index; - } - onConditionSelected(index == 0 - ? "<" - : index == 1 - ? "==" - : ">"); - }); + return ValueListenableBuilder( + valueListenable: valueNotifier, + builder: (context, value, _) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ValueListenableBuilder>( + valueListenable: conditionsNotifier, + builder: (context, selectedConditions, _) { + return ToggleButtons( + onPressed: (int index) { + final newConditions = List.filled(3, false); + newConditions[index] = true; + conditionsNotifier.value = newConditions; + conditionNotifier.value = index == 0 + ? "<" + : index == 1 + ? "==" + : ">"; + }, + borderRadius: const BorderRadius.all(Radius.circular(8)), + selectedBorderColor: ColorsManager.primaryColorWithOpacity, + selectedColor: Colors.white, + fillColor: ColorsManager.primaryColorWithOpacity, + color: ColorsManager.primaryColorWithOpacity, + constraints: const BoxConstraints( + minHeight: 40.0, + minWidth: 40.0, + ), + isSelected: selectedConditions, + children: const [Text("<"), Text("="), Text(">")], + ); + }, + ), + const SizedBox(height: 20), + Text( + '${value ?? 0} sec', + style: Theme.of(context).textTheme.headlineMedium, + ), + const SizedBox(height: 20), + Slider( + value: (value ?? 0).toDouble(), + min: 0, + max: 300, + divisions: 300, + onChanged: (newValue) { + valueNotifier.value = newValue.toInt(); + }, + ), + ], + ); }, - borderRadius: const BorderRadius.all(Radius.circular(8)), - selectedBorderColor: ColorsManager.primaryColorWithOpacity, - selectedColor: Colors.white, - fillColor: ColorsManager.primaryColorWithOpacity, - color: ColorsManager.primaryColorWithOpacity, - constraints: const BoxConstraints( - minHeight: 40.0, - minWidth: 40.0, - ), - isSelected: selectedConditions, - children: const [Text("<"), Text("="), Text(">")], ); } } diff --git a/lib/pages/routiens/view/create_new_routine_view.dart b/lib/pages/routiens/view/create_new_routine_view.dart index 99b4df71..c81e878f 100644 --- a/lib/pages/routiens/view/create_new_routine_view.dart +++ b/lib/pages/routiens/view/create_new_routine_view.dart @@ -12,82 +12,79 @@ class CreateNewRoutineView extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => RoutineBloc(), - child: Container( - alignment: Alignment.topLeft, - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const RoutineSearchAndButtons(), - const SizedBox(height: 20), - Flexible( - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: Card( - child: Container( - decoration: BoxDecoration( - color: ColorsManager.whiteColors, - borderRadius: BorderRadius.circular(15), - ), - child: const ConditionsRoutinesDevicesView()), - ), + return Container( + alignment: Alignment.topLeft, + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const RoutineSearchAndButtons(), + const SizedBox(height: 20), + Flexible( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: Card( + child: Container( + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(15), + ), + child: const ConditionsRoutinesDevicesView()), ), - const SizedBox( - width: 10, - ), - Expanded( - child: Column( - children: [ - /// IF Container - Expanded( - child: Card( - margin: EdgeInsets.zero, - child: Container( - decoration: const BoxDecoration( - color: ColorsManager.whiteColors, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(15), - topRight: Radius.circular(15), - ), + ), + const SizedBox( + width: 10, + ), + Expanded( + child: Column( + children: [ + /// IF Container + Expanded( + child: Card( + margin: EdgeInsets.zero, + child: Container( + decoration: const BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(15), + topRight: Radius.circular(15), ), - child: const IfContainer(), ), + child: const IfContainer(), ), ), - Container( - height: 2, - width: double.infinity, - color: ColorsManager.dialogBlueTitle, - ), + ), + Container( + height: 2, + width: double.infinity, + color: ColorsManager.dialogBlueTitle, + ), - /// THEN Container - Expanded( - child: Card( - margin: EdgeInsets.zero, - child: Container( - decoration: const BoxDecoration( - color: ColorsManager.boxColor, - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(15), - bottomRight: Radius.circular(15), - ), + /// THEN Container + Expanded( + child: Card( + margin: EdgeInsets.zero, + child: Container( + decoration: const BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(15), + bottomRight: Radius.circular(15), ), - child: const ThenContainer(), ), + child: const ThenContainer(), ), ), - ], - ), + ), + ], ), - ], - ), + ), + ], ), - ], - ), + ), + ], ), ); } diff --git a/lib/pages/routiens/view/routines_view.dart b/lib/pages/routiens/view/routines_view.dart index 5a19db08..b229dd62 100644 --- a/lib/pages/routiens/view/routines_view.dart +++ b/lib/pages/routiens/view/routines_view.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/view/create_new_routine_view.dart'; import 'package:syncrow_web/utils/color_manager.dart'; class RoutinesView extends StatelessWidget { @@ -8,54 +10,67 @@ class RoutinesView extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text("Create New Routines", - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - color: ColorsManager.grayColor, - )), - SizedBox( - height: 200, - width: 150, - child: GestureDetector( - onTap: () { - BlocProvider.of(context).add( - const CreateNewRoutineViewEvent(true), - ); - }, - child: Card( - elevation: 3, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - color: ColorsManager.whiteColors, - child: Center( - child: Container( - decoration: BoxDecoration( - color: ColorsManager.graysColor, - borderRadius: BorderRadius.circular(120), - border: Border.all(color: ColorsManager.greyColor, width: 2.0), - ), - height: 70, - width: 70, - child: Icon( - Icons.add, - color: ColorsManager.dialogBlueTitle, - size: 40, - ), - )), + return BlocBuilder( + builder: (context, state) { + if (state is ShowCreateRoutineState && state.showCreateRoutine) { + return const CreateNewRoutineView(); + } + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + "Create New Routines", + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: ColorsManager.grayColor, + ), ), - ), + SizedBox( + height: 200, + width: 150, + child: GestureDetector( + onTap: () { + BlocProvider.of(context).add( + const CreateNewRoutineViewEvent(true), + ); + }, + child: Card( + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + color: ColorsManager.whiteColors, + child: Center( + child: Container( + decoration: BoxDecoration( + color: ColorsManager.graysColor, + borderRadius: BorderRadius.circular(120), + border: Border.all( + color: ColorsManager.greyColor, + width: 2.0, + ), + ), + height: 70, + width: 70, + child: Icon( + Icons.add, + color: ColorsManager.dialogBlueTitle, + size: 40, + ), + ), + ), + ), + ), + ), + const Spacer(), + ], ), - const Spacer(), - ], - ), + ); + }, ); } } diff --git a/lib/pages/routiens/widgets/conditions_routines_devices_view.dart b/lib/pages/routiens/widgets/conditions_routines_devices_view.dart index 91100220..2728d329 100644 --- a/lib/pages/routiens/widgets/conditions_routines_devices_view.dart +++ b/lib/pages/routiens/widgets/conditions_routines_devices_view.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; import 'package:syncrow_web/pages/routiens/widgets/routine_devices.dart'; import 'package:syncrow_web/pages/routiens/widgets/routines_title_widget.dart'; @@ -11,87 +13,107 @@ class ConditionsRoutinesDevicesView extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: EdgeInsets.symmetric(horizontal: 8.0), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const ConditionTitleAndSearchBar(), - const SizedBox( - height: 10, - ), - const Wrap( - spacing: 10, - runSpacing: 10, + return BlocBuilder( + builder: (context, state) { + return const Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - DraggableCard( - imagePath: Assets.tabToRun, - title: 'Tab to run', + ConditionTitleAndSearchBar(), + SizedBox(height: 10), + Wrap( + spacing: 10, + runSpacing: 10, + children: [ + DraggableCard( + imagePath: Assets.tabToRun, + title: 'Tab to run', + deviceData: { + 'deviceId': 'tab_to_run', + 'type': 'trigger', + 'name': 'Tab to run', + }, + ), + DraggableCard( + imagePath: Assets.map, + title: 'Location', + deviceData: { + 'deviceId': 'location', + 'type': 'trigger', + 'name': 'Location', + }, + ), + DraggableCard( + imagePath: Assets.weather, + title: 'Weather', + deviceData: { + 'deviceId': 'weather', + 'type': 'trigger', + 'name': 'Weather', + }, + ), + DraggableCard( + imagePath: Assets.schedule, + title: 'Schedule', + deviceData: { + 'deviceId': 'schedule', + 'type': 'trigger', + 'name': 'Schedule', + }, + ), + ], ), - DraggableCard( - imagePath: Assets.map, - title: 'Location', + const SizedBox(height: 10), + const TitleRoutine( + title: 'Conditions', + subtitle: '(THEN)', ), - DraggableCard( - imagePath: Assets.weather, - title: 'Weather', + const SizedBox(height: 10), + const Wrap( + spacing: 10, + runSpacing: 10, + children: [ + DraggableCard( + imagePath: Assets.notification, + title: 'Send Notification', + deviceData: { + 'deviceId': 'notification', + 'type': 'action', + 'name': 'Send Notification', + }, + ), + DraggableCard( + imagePath: Assets.delay, + title: 'Delay the action', + deviceData: { + 'deviceId': 'delay', + 'type': 'action', + 'name': 'Delay the action', + }, + ), + ], ), - DraggableCard( - imagePath: Assets.schedule, - title: 'Schedule', + const SizedBox(height: 10), + const TitleRoutine( + title: 'Routines', + subtitle: '(THEN)', ), + const SizedBox(height: 10), + const ScenesAndAutomations(), + const SizedBox(height: 10), + const TitleRoutine( + title: 'Devices', + subtitle: '', + ), + const SizedBox(height: 10), + const RoutineDevices(), ], ), - const SizedBox( - height: 10, - ), - const TitleRoutine( - title: 'Conditions', - subtitle: '(THEN)', - ), - const SizedBox( - height: 10, - ), - const Wrap( - spacing: 10, - runSpacing: 10, - children: [ - DraggableCard( - imagePath: Assets.notification, - title: 'Send Notification', - ), - DraggableCard( - imagePath: Assets.delay, - title: 'Delay the action', - ), - ], - ), - const SizedBox( - height: 10, - ), - const TitleRoutine( - title: 'Routines', - subtitle: '(THEN)', - ), - const SizedBox( - height: 10, - ), - const ScenesAndAutomations(), - const SizedBox( - height: 10, - ), - const TitleRoutine( - title: 'Devices', - subtitle: '', - ), - const SizedBox( - height: 10, - ), - const RoutineDevices(), - ], - ), - ), + ), + ); + }, ); } } diff --git a/lib/pages/routiens/widgets/dialog_footer.dart b/lib/pages/routiens/widgets/dialog_footer.dart new file mode 100644 index 00000000..90c6baec --- /dev/null +++ b/lib/pages/routiens/widgets/dialog_footer.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class DialogFooter extends StatelessWidget { + final VoidCallback onCancel; + final VoidCallback? onConfirm; + final bool isConfirmEnabled; + + const DialogFooter({ + Key? key, + required this.onCancel, + required this.onConfirm, + required this.isConfirmEnabled, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + decoration: const BoxDecoration( + border: Border( + top: BorderSide( + color: ColorsManager.greyColor, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildFooterButton( + context, + 'Cancel', + onCancel, + width: isConfirmEnabled ? 299 : 179, + ), + if (isConfirmEnabled) + Row( + children: [ + Container(width: 1, height: 50, color: ColorsManager.greyColor), + _buildFooterButton( + context, + 'Confirm', + onConfirm, + width: 299, + ), + ], + ), + ], + ), + ); + } + + Widget _buildFooterButton( + BuildContext context, + String text, + VoidCallback? onTap, { + required double width, + }) { + return GestureDetector( + onTap: onTap, + child: SizedBox( + height: 50, + width: width, + child: Center( + child: Text( + text, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: text == 'Confirm' + ? ColorsManager.primaryColorWithOpacity + : ColorsManager.textGray, + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/routiens/widgets/dialog_header.dart b/lib/pages/routiens/widgets/dialog_header.dart new file mode 100644 index 00000000..6701b5b0 --- /dev/null +++ b/lib/pages/routiens/widgets/dialog_header.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class DialogHeader extends StatelessWidget { + final String title; + + const DialogHeader(this.title, {super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text( + title, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.primaryColorWithOpacity, + fontWeight: FontWeight.bold, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 50), + child: Container( + height: 1, + width: double.infinity, + color: ColorsManager.greyColor, + ), + ), + ], + ); + } +} diff --git a/lib/pages/routiens/widgets/dragable_card.dart b/lib/pages/routiens/widgets/dragable_card.dart index 7ee078ff..227e440b 100644 --- a/lib/pages/routiens/widgets/dragable_card.dart +++ b/lib/pages/routiens/widgets/dragable_card.dart @@ -2,156 +2,139 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class DraggableCard extends StatelessWidget { + final String imagePath; + final String title; + final Map deviceData; + const DraggableCard({ super.key, required this.imagePath, required this.title, - this.titleColor, - this.isDragged = false, - this.isDisabled = false, - this.deviceData, + required this.deviceData, }); - final String imagePath; - final String title; - final Color? titleColor; - final bool isDragged; - final bool isDisabled; - final Map? deviceData; - @override Widget build(BuildContext context) { - Widget card = Draggable>( - data: deviceData ?? - { - 'key': UniqueKey().toString(), - 'imagePath': imagePath, - 'title': title, - }, - feedback: Transform.rotate( - angle: -0.1, - child: _buildCardContent(context), - ), - childWhenDragging: _buildGreyContainer(), - child: _buildCardContent(context), - ); - - if (isDisabled) { - card = AbsorbPointer(child: card); - } - - return card; - } - - Widget _buildCardContent(BuildContext context) { return BlocBuilder( builder: (context, state) { - // Filter functions for this device final deviceFunctions = state.selectedFunctions - .where((f) => f.entityId == deviceData?['deviceId']) + .where((f) => f.entityId == deviceData['deviceId']) .toList(); - return Card( - color: ColorsManager.whiteColors, - child: SizedBox( - width: 90, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox( - height: 123, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - height: 50, - width: 50, - decoration: BoxDecoration( - color: ColorsManager.CircleImageBackground, - borderRadius: BorderRadius.circular(90), - border: Border.all( - color: ColorsManager.graysColor, - ), - ), - padding: const EdgeInsets.all(8), - child: imagePath.contains('.svg') - ? SvgPicture.asset( - imagePath, - ) - : Image.network(imagePath), + return Draggable>( + data: deviceData, + feedback: Transform.rotate( + angle: -0.1, + child: _buildCardContent(context, deviceFunctions), + ), + childWhenDragging: _buildGreyContainer(), + child: _buildCardContent(context, deviceFunctions), + ); + }, + ); + } + + Widget _buildCardContent( + BuildContext context, List deviceFunctions) { + return Card( + color: ColorsManager.whiteColors, + child: SizedBox( + width: 90, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + height: 123, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + height: 50, + width: 50, + decoration: BoxDecoration( + color: ColorsManager.CircleImageBackground, + borderRadius: BorderRadius.circular(90), + border: Border.all( + color: ColorsManager.graysColor, ), - const SizedBox(height: 8), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 3), + ), + padding: const EdgeInsets.all(8), + child: imagePath.contains('.svg') + ? SvgPicture.asset( + imagePath, + ) + : Image.network(imagePath), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 3), + child: Text( + title, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + maxLines: 2, + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.blackColor, + fontSize: 12, + ), + ), + ), + ], + ), + ), + if (deviceFunctions.isNotEmpty) ...[ + const Divider(height: 1), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 4), + itemCount: deviceFunctions.length, + itemBuilder: (context, index) { + final function = deviceFunctions[index]; + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( child: Text( - title, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - maxLines: 2, + '${function.operationName}: ${function.valueDescription}', style: context.textTheme.bodySmall?.copyWith( - color: titleColor ?? ColorsManager.blackColor, - fontSize: 12, + fontSize: 9, + color: ColorsManager.textGray, + height: 1.2, + ), + overflow: TextOverflow.ellipsis, + ), + ), + InkWell( + onTap: () { + context.read().add( + RemoveFunction(function), + ); + }, + child: const Padding( + padding: EdgeInsets.all(2), + child: Icon( + Icons.close, + size: 12, + color: ColorsManager.textGray, ), ), ), ], - ), - ), - if (deviceFunctions.isNotEmpty) ...[ - const Divider(height: 1), - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - padding: - const EdgeInsets.symmetric(vertical: 4, horizontal: 4), - itemCount: deviceFunctions.length, - itemBuilder: (context, index) { - final function = deviceFunctions[index]; - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: Text( - '${function.operationName}: ${function.valueDescription}', - style: context.textTheme.bodySmall?.copyWith( - fontSize: 9, - color: ColorsManager.textGray, - height: 1.2, - ), - overflow: TextOverflow.ellipsis, - ), - ), - InkWell( - onTap: () { - context.read().add( - RemoveFunction(function), - ); - }, - child: const Padding( - padding: EdgeInsets.all(2), - child: Icon( - Icons.close, - size: 12, - color: ColorsManager.textGray, - ), - ), - ), - ], - ); - }, - ), - ], - ], - ), - ), - ); - }, + ); + }, + ), + ], + ], + ), + ), ); } diff --git a/lib/pages/routiens/widgets/if_container.dart b/lib/pages/routiens/widgets/if_container.dart index ba0d67ab..af1394c6 100644 --- a/lib/pages/routiens/widgets/if_container.dart +++ b/lib/pages/routiens/widgets/if_container.dart @@ -31,6 +31,7 @@ class IfContainer extends StatelessWidget { key: Key(item['key']!), imagePath: item['imagePath']!, title: item['title']!, + deviceData: item, )) .toList(), ), diff --git a/lib/pages/routiens/widgets/routine_devices.dart b/lib/pages/routiens/widgets/routine_devices.dart index c845af19..7d97de18 100644 --- a/lib/pages/routiens/widgets/routine_devices.dart +++ b/lib/pages/routiens/widgets/routine_devices.dart @@ -1,20 +1,19 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; class RoutineDevices extends StatelessWidget { - const RoutineDevices({ - super.key, - }); + const RoutineDevices({super.key}); @override Widget build(BuildContext context) { + // Get the RoutineBloc instance from the parent + final routineBloc = context.read(); + return BlocProvider( - create: (context) => DeviceManagementBloc() - ..add( - FetchDevices(), - ), + create: (context) => DeviceManagementBloc()..add(FetchDevices()), child: BlocBuilder( builder: (context, state) { if (state is DeviceManagementLoaded) { @@ -25,24 +24,34 @@ class RoutineDevices extends StatelessWidget { device.productType == '2G' || device.productType == '3G') .toList(); - return Wrap( - spacing: 10, - runSpacing: 10, - children: deviceList.asMap().entries.map((entry) { - final device = entry.value; - return DraggableCard( - imagePath: device.getDefaultIcon(device.productType), - title: device.name ?? '', - deviceData: { - 'key': UniqueKey().toString(), - 'imagePath': device.getDefaultIcon(device.productType), - 'title': device.name ?? '', - 'deviceId': device.uuid, - 'productType': device.productType, - 'functions': device.functions, - }, - ); - }).toList(), + + // Provide the RoutineBloc to the child widgets + return BlocProvider.value( + value: routineBloc, + child: BlocBuilder( + builder: (context, routineState) { + return Wrap( + spacing: 10, + runSpacing: 10, + children: deviceList.asMap().entries.map((entry) { + final device = entry.value; + return DraggableCard( + imagePath: device.getDefaultIcon(device.productType), + title: device.name ?? '', + deviceData: { + 'key': UniqueKey().toString(), + 'imagePath': + device.getDefaultIcon(device.productType), + 'title': device.name ?? '', + 'deviceId': device.uuid, + 'productType': device.productType, + 'functions': device.functions, + }, + ); + }).toList(), + ); + }, + ), ); } return const Center(child: CircularProgressIndicator()); diff --git a/lib/pages/routiens/widgets/scenes_and_automations.dart b/lib/pages/routiens/widgets/scenes_and_automations.dart index b455b2f5..ef40b605 100644 --- a/lib/pages/routiens/widgets/scenes_and_automations.dart +++ b/lib/pages/routiens/widgets/scenes_and_automations.dart @@ -4,40 +4,51 @@ import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; -class ScenesAndAutomations extends StatelessWidget { +class ScenesAndAutomations extends StatefulWidget { const ScenesAndAutomations({ super.key, }); + @override + State createState() => _ScenesAndAutomationsState(); +} + +class _ScenesAndAutomationsState extends State { + @override + void initState() { + super.initState(); + context.read() + ..add(const LoadScenes(spaceId)) + ..add(const LoadAutomation(spaceId)); + } + @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => RoutineBloc() - ..add( - const LoadScenes(spaceId), - ) - ..add( - const LoadAutomation(spaceId), - ), - child: BlocBuilder( - builder: (context, state) { - if (state.scenes.isNotEmpty || state.automations.isNotEmpty) { - var scenes = [...state.scenes, ...state.automations]; - return Wrap( - spacing: 10, - runSpacing: 10, - children: scenes.asMap().entries.map((entry) { - final scene = entry.value; - return DraggableCard( - imagePath: Assets.logo, - title: scene.name, - ); - }).toList(), - ); - } - return const Center(child: CircularProgressIndicator()); - }, - ), + return BlocBuilder( + builder: (context, state) { + if (state.scenes.isNotEmpty || state.automations.isNotEmpty) { + var scenes = [...state.scenes, ...state.automations]; + return Wrap( + spacing: 10, + runSpacing: 10, + children: scenes.asMap().entries.map((entry) { + final scene = entry.value; + return DraggableCard( + imagePath: Assets.logo, + title: scene.name, + deviceData: { + 'deviceId': scene.id, + 'name': scene.name, + 'status': scene.status, + 'type': scene.type, + 'icon': scene.icon, + }, + ); + }).toList(), + ); + } + return const Center(child: CircularProgressIndicator()); + }, ); } } diff --git a/lib/pages/routiens/widgets/then_container.dart b/lib/pages/routiens/widgets/then_container.dart index 9bd7fd00..385ec4cb 100644 --- a/lib/pages/routiens/widgets/then_container.dart +++ b/lib/pages/routiens/widgets/then_container.dart @@ -33,6 +33,7 @@ class ThenContainer extends StatelessWidget { key: Key(item['key']!), imagePath: item['imagePath']!, title: item['title']!, + deviceData: item, )) .toList(), ),