From 1d6673b5b0c11b0b870d94b0e62a8e676df961eb Mon Sep 17 00:00:00 2001 From: ashrafzarkanisala Date: Fri, 22 Nov 2024 16:55:30 +0300 Subject: [PATCH] adding fucntions and values to routine bloc --- lib/pages/routiens/bloc/routine_bloc.dart | 44 +- lib/pages/routiens/bloc/routine_event.dart | 16 + lib/pages/routiens/bloc/routine_state.dart | 5 + lib/pages/routiens/helper/ac_helper.dart | 562 ++++++------------ .../dialog_helper/device_dialog_helper.dart | 24 +- .../helper/one_gang_switch_helper.dart | 333 +++++++---- .../helper/three_gang_switch_helper.dart | 335 +++++++---- .../helper/two_gang_switch_helper.dart | 334 +++++++---- .../routiens/models/device_functions.dart | 44 ++ lib/pages/routiens/widgets/dragable_card.dart | 147 +++-- 10 files changed, 1094 insertions(+), 750 deletions(-) diff --git a/lib/pages/routiens/bloc/routine_bloc.dart b/lib/pages/routiens/bloc/routine_bloc.dart index dfbb824c..d69df552 100644 --- a/lib/pages/routiens/bloc/routine_bloc.dart +++ b/lib/pages/routiens/bloc/routine_bloc.dart @@ -1,5 +1,6 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; import 'package:syncrow_web/pages/routiens/models/routine_model.dart'; import 'package:syncrow_web/services/routines_api.dart'; @@ -14,27 +15,55 @@ class RoutineBloc extends Bloc { on(_onAddToThenContainer); on(_onLoadScenes); on(_onLoadAutomation); + on(_onAddFunction); + on(_onRemoveFunction); + on(_onClearFunctions); } void _onAddToIfContainer(AddToIfContainer event, Emitter emit) { if (!_isDuplicate(state.ifItems, event.item)) { - final updatedIfItems = List>.from(state.ifItems)..add(event.item); + final updatedIfItems = List>.from(state.ifItems) + ..add(event.item); emit(state.copyWith(ifItems: updatedIfItems)); } } - void _onAddToThenContainer(AddToThenContainer event, Emitter emit) { + void _onAddToThenContainer( + AddToThenContainer event, Emitter emit) { if (!_isDuplicate(state.thenItems, event.item)) { - final updatedThenItems = List>.from(state.thenItems)..add(event.item); + final updatedThenItems = List>.from(state.thenItems) + ..add(event.item); emit(state.copyWith(thenItems: updatedThenItems)); } } - bool _isDuplicate(List> items, Map newItem) { - return items.any((item) => item['imagePath'] == newItem['imagePath'] && item['title'] == newItem['title']); + void _onAddFunction(AddFunction event, Emitter emit) { + final functions = List.from(state.selectedFunctions); + functions.add(event.function); + emit(state.copyWith(selectedFunctions: functions)); } - Future _onLoadScenes(LoadScenes event, Emitter emit) async { + void _onRemoveFunction(RemoveFunction event, Emitter emit) { + final functions = List.from(state.selectedFunctions) + ..removeWhere((f) => + f.function == event.function.function && + f.value == event.function.value); + emit(state.copyWith(selectedFunctions: functions)); + } + + void _onClearFunctions(ClearFunctions event, Emitter emit) { + emit(state.copyWith(selectedFunctions: [])); + } + + bool _isDuplicate( + List> items, Map newItem) { + return items.any((item) => + item['imagePath'] == newItem['imagePath'] && + item['title'] == newItem['title']); + } + + Future _onLoadScenes( + LoadScenes event, Emitter emit) async { emit(state.copyWith(isLoading: true, errorMessage: null)); try { @@ -51,7 +80,8 @@ class RoutineBloc extends Bloc { } } - Future _onLoadAutomation(LoadAutomation event, Emitter emit) async { + Future _onLoadAutomation( + LoadAutomation event, Emitter emit) async { emit(state.copyWith(isLoading: true, errorMessage: null)); try { diff --git a/lib/pages/routiens/bloc/routine_event.dart b/lib/pages/routiens/bloc/routine_event.dart index aaee73c7..d3af8134 100644 --- a/lib/pages/routiens/bloc/routine_event.dart +++ b/lib/pages/routiens/bloc/routine_event.dart @@ -42,3 +42,19 @@ class LoadAutomation extends RoutineEvent { @override List get props => [unitId]; } + +class AddFunction extends RoutineEvent { + final DeviceFunctionData function; + const AddFunction(this.function); + @override + List get props => [function]; +} + +class RemoveFunction extends RoutineEvent { + final DeviceFunctionData function; + const RemoveFunction(this.function); + @override + List get props => [function]; +} + +class ClearFunctions extends RoutineEvent {} diff --git a/lib/pages/routiens/bloc/routine_state.dart b/lib/pages/routiens/bloc/routine_state.dart index 63d2e6f8..95aab557 100644 --- a/lib/pages/routiens/bloc/routine_state.dart +++ b/lib/pages/routiens/bloc/routine_state.dart @@ -6,6 +6,7 @@ class RoutineState extends Equatable { final List> availableCards; final List scenes; final List automations; + final List selectedFunctions; final bool isLoading; final String? errorMessage; @@ -15,6 +16,7 @@ class RoutineState extends Equatable { this.availableCards = const [], this.scenes = const [], this.automations = const [], + this.selectedFunctions = const [], this.isLoading = false, this.errorMessage, }); @@ -24,6 +26,7 @@ class RoutineState extends Equatable { List>? thenItems, List? scenes, List? automations, + List? selectedFunctions, bool? isLoading, String? errorMessage, }) { @@ -32,6 +35,7 @@ class RoutineState extends Equatable { thenItems: thenItems ?? this.thenItems, scenes: scenes ?? this.scenes, automations: automations ?? this.automations, + selectedFunctions: selectedFunctions ?? this.selectedFunctions, isLoading: isLoading ?? this.isLoading, errorMessage: errorMessage ?? this.errorMessage, ); @@ -43,6 +47,7 @@ class RoutineState extends Equatable { thenItems, scenes, automations, + selectedFunctions, isLoading, errorMessage, ]; diff --git a/lib/pages/routiens/helper/ac_helper.dart b/lib/pages/routiens/helper/ac_helper.dart index 4c216a89..b14acd3a 100644 --- a/lib/pages/routiens/helper/ac_helper.dart +++ b/lib/pages/routiens/helper/ac_helper.dart @@ -1,411 +1,219 @@ 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/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(); - String? selectedFunction; - dynamic selectedValue = 20; - String? selectedCondition = "=="; - List _selectedConditions = [false, true, false]; + // Track multiple selections using a map + Map selectedValues = {}; + List selectedFunctions = []; - return showDialog?>( + await showDialog( context: context, builder: (BuildContext context) { - 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, - ), - ); - }, + 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, + ), + ), + ), + ], + ), + ], + ), + ), ); }, ); } - /// Build dialog content for AC functions dialog - static Widget _buildDialogContent( + static Widget _buildValueSelector( BuildContext context, - StateSetter setState, - List acFunctions, - String? selectedFunction, + ACFunction function, dynamic selectedValue, - String? selectedCondition, - List selectedConditions, - Function(String?) onFunctionSelected, Function(dynamic) onValueSelected, - Function(String?) onConditionSelected, ) { + final values = function.getOperationalValues(); return Container( - 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: [ - _buildDialogHeader(context), - Flexible( - child: Row( - children: [ - _buildFunctionsList( - context, - setState, - acFunctions, - selectedFunction, - onFunctionSelected, - ), - if (selectedFunction != null) - _buildValueSelector( - context, - setState, - selectedFunction, - selectedValue, - selectedCondition, - selectedConditions, - onValueSelected, - onConditionSelected, - acFunctions, - ), - ], - ), - ), - _buildDialogFooter( - context, - selectedFunction, - selectedValue, - selectedCondition, - ), - ], - ), - ); - } - - /// Build header for AC functions dialog - static Widget _buildDialogHeader(BuildContext context) { - return Column( - children: [ - Text( - 'AC 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, - ), - ), - ], - ); - } - - /// 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, - ), + height: 200, + padding: const EdgeInsets.symmetric(horizontal: 20), + child: ListView.builder( + itemCount: values.length, itemBuilder: (context, index) { - 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)), + final value = values[index]; + return RadioListTile( + value: value.value, + groupValue: selectedValue, + onChanged: onValueSelected, + title: Text(value.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)); - }, - ), - ); - }, - ); - } - - static Widget _buildDialogFooter( - BuildContext context, - String? selectedFunction, - dynamic selectedValue, - String? selectedCondition, - ) { - return Container( - decoration: const BoxDecoration( - border: Border( - top: BorderSide( - color: ColorsManager.greyColor, - ), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _buildFooterButton( - context, - 'Cancel', - selectedFunction != null ? 299 : 179, - () => Navigator.pop(context), - ), - _buildFooterButton( - context, - 'Confirm', - selectedFunction != null ? 299 : 179, - selectedFunction != null && selectedValue != null - ? () => Navigator.pop(context, { - 'function': selectedFunction, - 'value': selectedValue, - 'condition': selectedCondition ?? "==", - }) - : null, - ), - ], - ), - ); - } - - static Widget _buildFooterButton( - BuildContext context, - String text, - double width, - VoidCallback? onTap, - ) { - return GestureDetector( - onTap: onTap, - child: SizedBox( - height: 50, - width: width, - child: Center( - child: Text( - text, - style: context.textTheme.bodyMedium!.copyWith( - color: onTap != null - ? ColorsManager.primaryColorWithOpacity - : ColorsManager.textGray, - ), - ), - ), - ), - ); + static String _getValueDescription(ACFunction function, dynamic value) { + final values = function.getOperationalValues(); + final selectedValue = values.firstWhere((v) => v.value == value); + return selectedValue.description; } } diff --git a/lib/pages/routiens/helper/dialog_helper/device_dialog_helper.dart b/lib/pages/routiens/helper/dialog_helper/device_dialog_helper.dart index 0046db8a..ea4743b7 100644 --- a/lib/pages/routiens/helper/dialog_helper/device_dialog_helper.dart +++ b/lib/pages/routiens/helper/dialog_helper/device_dialog_helper.dart @@ -13,15 +13,11 @@ class DeviceDialogHelper { final functions = data['functions'] as List; try { - final result = await _getDialogForDeviceType( + await _getDialogForDeviceType( context, data['productType'], functions, ); - - if (result != null) { - return {...data, ...result}; - } } catch (e) { debugPrint('Error: $e'); } @@ -29,25 +25,25 @@ class DeviceDialogHelper { return null; } - static Future?> _getDialogForDeviceType( + static Future _getDialogForDeviceType( BuildContext context, String productType, List functions, ) async { switch (productType) { case 'AC': - return ACHelper.showACFunctionsDialog(context, functions); + await ACHelper.showACFunctionsDialog(context, functions); + break; case '1G': - return OneGangSwitchHelper.showSwitchFunctionsDialog( - context, functions); + await OneGangSwitchHelper.showSwitchFunctionsDialog(context, functions); + break; case '2G': - return TwoGangSwitchHelper.showSwitchFunctionsDialog( - context, functions); + await TwoGangSwitchHelper.showSwitchFunctionsDialog(context, functions); + break; case '3G': - return ThreeGangSwitchHelper.showSwitchFunctionsDialog( + await ThreeGangSwitchHelper.showSwitchFunctionsDialog( context, functions); - default: - return null; + break; } } } diff --git a/lib/pages/routiens/helper/one_gang_switch_helper.dart b/lib/pages/routiens/helper/one_gang_switch_helper.dart index 30f25a96..c9a79377 100644 --- a/lib/pages/routiens/helper/one_gang_switch_helper.dart +++ b/lib/pages/routiens/helper/one_gang_switch_helper.dart @@ -1,5 +1,7 @@ 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'; @@ -7,16 +9,18 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class OneGangSwitchHelper { - static Future?> showSwitchFunctionsDialog( + static Future showSwitchFunctionsDialog( BuildContext context, List> functions) async { List> switchFunctions = functions .where( (f) => f is OneGangSwitchFunction || f is OneGangCountdownFunction) .toList(); - String? selectedFunction; - dynamic selectedValue; + Map selectedValues = {}; + List selectedFunctions = []; + String? selectedCondition = "<"; + List selectedConditions = [true, false, false]; - return showDialog?>( + await showDialog( context: context, builder: (BuildContext context) { return StatefulBuilder( @@ -24,7 +28,7 @@ class OneGangSwitchHelper { return AlertDialog( contentPadding: EdgeInsets.zero, content: Container( - width: selectedFunction != null ? 600 : 360, + width: 600, height: 450, decoration: BoxDecoration( color: Colors.white, @@ -50,21 +54,23 @@ class OneGangSwitchHelper { color: ColorsManager.greyColor, ), ), - Flexible( + Expanded( child: Row( children: [ + // Left side: Function list Expanded( child: ListView.separated( - shrinkWrap: false, - physics: const AlwaysScrollableScrollPhysics(), itemCount: switchFunctions.length, - separatorBuilder: (context, index) => - const Divider( + separatorBuilder: (_, __) => const Divider( color: ColorsManager.dividerColor, ), 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, @@ -74,62 +80,129 @@ class OneGangSwitchHelper { function.operationName, style: context.textTheme.bodyMedium, ), - trailing: const Icon( - Icons.arrow_forward_ios, - size: 16, - color: ColorsManager.textGray, - ), + trailing: isSelected + ? Icon( + Icons.check_circle, + color: ColorsManager + .primaryColorWithOpacity, + size: 20, + ) + : const Icon( + Icons.arrow_forward_ios, + size: 16, + color: ColorsManager.textGray, + ), onTap: () { - setState(() { - selectedFunction = function.code; - selectedValue = null; - }); + if (isSelected) { + selectedValues.remove(function.code); + selectedFunctions.removeWhere( + (f) => f.function == function.code); + } + (context as Element).markNeedsBuild(); }, ); }, ), ), - if (selectedFunction != null) - Expanded( - child: Builder( - builder: (context) { - final selectedFn = switchFunctions.firstWhere( - (f) => f.code == selectedFunction) - as BaseSwitchFunction; - final values = - selectedFn.getOperationalValues(); - 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(() { - selectedValue = newValue; - }); - }, - ), + // 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 ListTile( + leading: SvgPicture.asset( + value.icon, + width: 24, + height: 24, + ), + title: Text( + value.description, + 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(); + }, + ), + ); + }, + ); + }, ), + ), ], ), ), @@ -141,55 +214,35 @@ class OneGangSwitchHelper { Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - GestureDetector( - onTap: () { - Navigator.pop(context); - }, - child: Container( - height: 50, - width: selectedFunction != null ? 299 : 179, - decoration: const BoxDecoration( - border: Border( - right: - BorderSide(color: ColorsManager.greyColor), - ), - ), - child: Center( - child: Text( - 'Cancel', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: ColorsManager.greyColor), - ), - ), + TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + 'Cancel', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: ColorsManager.greyColor), ), ), - GestureDetector( - onTap: () { - if (selectedFunction != null && - selectedValue != null) { - Navigator.pop(context, { - 'function': selectedFunction, - 'value': selectedValue, - }); - } - }, - child: SizedBox( - height: 50, - width: selectedFunction != null ? 299 : 179, - child: Center( - child: Text( - 'Confirm', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith( - color: - ColorsManager.primaryColorWithOpacity, - ), - ), - ), + 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, + ), ), ), ], @@ -203,4 +256,78 @@ 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, + ) { + 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()); + }); + }, + ), + ], + ); + } + + /// 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(">")], + ); + } } diff --git a/lib/pages/routiens/helper/three_gang_switch_helper.dart b/lib/pages/routiens/helper/three_gang_switch_helper.dart index 89aa2d95..56d1f3bb 100644 --- a/lib/pages/routiens/helper/three_gang_switch_helper.dart +++ b/lib/pages/routiens/helper/three_gang_switch_helper.dart @@ -1,5 +1,7 @@ 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/three_gang_switch/three_gang_switch.dart'; @@ -7,7 +9,7 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class ThreeGangSwitchHelper { - static Future?> showSwitchFunctionsDialog( + static Future showSwitchFunctionsDialog( BuildContext context, List> functions) async { List> switchFunctions = functions .where((f) => @@ -18,10 +20,12 @@ class ThreeGangSwitchHelper { f is ThreeGangCountdown2Function || f is ThreeGangCountdown3Function) .toList(); - String? selectedFunction; - dynamic selectedValue; + Map selectedValues = {}; + List selectedFunctions = []; + String? selectedCondition = "<"; + List selectedConditions = [true, false, false]; - return showDialog?>( + await showDialog( context: context, builder: (BuildContext context) { return StatefulBuilder( @@ -29,7 +33,7 @@ class ThreeGangSwitchHelper { return AlertDialog( contentPadding: EdgeInsets.zero, content: Container( - width: selectedFunction != null ? 600 : 360, + width: 600, height: 450, decoration: BoxDecoration( color: Colors.white, @@ -55,21 +59,23 @@ class ThreeGangSwitchHelper { color: ColorsManager.greyColor, ), ), - Flexible( + Expanded( child: Row( children: [ + // Left side: Function list Expanded( child: ListView.separated( - shrinkWrap: false, - physics: const AlwaysScrollableScrollPhysics(), itemCount: switchFunctions.length, - separatorBuilder: (context, index) => - const Divider( + separatorBuilder: (_, __) => const Divider( color: ColorsManager.dividerColor, ), 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, @@ -79,62 +85,131 @@ class ThreeGangSwitchHelper { function.operationName, style: context.textTheme.bodyMedium, ), - trailing: const Icon( - Icons.arrow_forward_ios, - size: 16, - color: ColorsManager.textGray, - ), + trailing: isSelected + ? Icon( + Icons.check_circle, + color: ColorsManager + .primaryColorWithOpacity, + size: 20, + ) + : const Icon( + Icons.arrow_forward_ios, + size: 16, + color: ColorsManager.textGray, + ), onTap: () { - setState(() { - selectedFunction = function.code; - selectedValue = null; - }); + if (isSelected) { + selectedValues.remove(function.code); + selectedFunctions.removeWhere( + (f) => f.function == function.code); + } + (context as Element).markNeedsBuild(); }, ); }, ), ), - if (selectedFunction != null) - Expanded( - child: Builder( - builder: (context) { - final selectedFn = switchFunctions.firstWhere( - (f) => f.code == selectedFunction) - as BaseSwitchFunction; - final values = - selectedFn.getOperationalValues(); - 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(() { - selectedValue = newValue; - }); - }, - ), + // 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 ThreeGangCountdown1Function || + selectedFn is ThreeGangCountdown2Function || + selectedFn is ThreeGangCountdown3Function) { + 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 ListTile( + leading: SvgPicture.asset( + value.icon, + width: 24, + height: 24, + ), + title: Text( + value.description, + 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(); + }, + ), + ); + }, + ); + }, ), + ), ], ), ), @@ -146,55 +221,35 @@ class ThreeGangSwitchHelper { Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - GestureDetector( - onTap: () { - Navigator.pop(context); - }, - child: Container( - height: 50, - width: selectedFunction != null ? 299 : 179, - decoration: const BoxDecoration( - border: Border( - right: - BorderSide(color: ColorsManager.greyColor), - ), - ), - child: Center( - child: Text( - 'Cancel', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: ColorsManager.greyColor), - ), - ), + TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + 'Cancel', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: ColorsManager.greyColor), ), ), - GestureDetector( - onTap: () { - if (selectedFunction != null && - selectedValue != null) { - Navigator.pop(context, { - 'function': selectedFunction, - 'value': selectedValue, - }); - } - }, - child: SizedBox( - height: 50, - width: selectedFunction != null ? 299 : 179, - child: Center( - child: Text( - 'Confirm', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith( - color: - ColorsManager.primaryColorWithOpacity, - ), - ), - ), + 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, + ), ), ), ], @@ -208,4 +263,78 @@ class ThreeGangSwitchHelper { }, ); } + + /// 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, + ) { + 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()); + }); + }, + ), + ], + ); + } + + /// 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(">")], + ); + } } diff --git a/lib/pages/routiens/helper/two_gang_switch_helper.dart b/lib/pages/routiens/helper/two_gang_switch_helper.dart index 1d271ac7..6f2febc7 100644 --- a/lib/pages/routiens/helper/two_gang_switch_helper.dart +++ b/lib/pages/routiens/helper/two_gang_switch_helper.dart @@ -1,5 +1,7 @@ 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/two_gang_switch/two_gang_switch.dart'; @@ -7,7 +9,7 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class TwoGangSwitchHelper { - static Future?> showSwitchFunctionsDialog( + static Future showSwitchFunctionsDialog( BuildContext context, List> functions) async { List> switchFunctions = functions .where((f) => @@ -16,10 +18,12 @@ class TwoGangSwitchHelper { f is TwoGangCountdown1Function || f is TwoGangCountdown2Function) .toList(); - String? selectedFunction; - dynamic selectedValue; + Map selectedValues = {}; + List selectedFunctions = []; + String? selectedCondition = "<"; + List selectedConditions = [true, false, false]; - return showDialog?>( + await showDialog( context: context, builder: (BuildContext context) { return StatefulBuilder( @@ -27,7 +31,7 @@ class TwoGangSwitchHelper { return AlertDialog( contentPadding: EdgeInsets.zero, content: Container( - width: selectedFunction != null ? 600 : 360, + width: 600, height: 450, decoration: BoxDecoration( color: Colors.white, @@ -53,21 +57,23 @@ class TwoGangSwitchHelper { color: ColorsManager.greyColor, ), ), - Flexible( + Expanded( child: Row( children: [ + // Left side: Function list Expanded( child: ListView.separated( - shrinkWrap: false, - physics: const AlwaysScrollableScrollPhysics(), itemCount: switchFunctions.length, - separatorBuilder: (context, index) => - const Divider( + separatorBuilder: (_, __) => const Divider( color: ColorsManager.dividerColor, ), 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, @@ -77,62 +83,130 @@ class TwoGangSwitchHelper { function.operationName, style: context.textTheme.bodyMedium, ), - trailing: const Icon( - Icons.arrow_forward_ios, - size: 16, - color: ColorsManager.textGray, - ), + trailing: isSelected + ? Icon( + Icons.check_circle, + color: ColorsManager + .primaryColorWithOpacity, + size: 20, + ) + : const Icon( + Icons.arrow_forward_ios, + size: 16, + color: ColorsManager.textGray, + ), onTap: () { - setState(() { - selectedFunction = function.code; - selectedValue = null; - }); + if (isSelected) { + selectedValues.remove(function.code); + selectedFunctions.removeWhere( + (f) => f.function == function.code); + } + (context as Element).markNeedsBuild(); }, ); }, ), ), - if (selectedFunction != null) - Expanded( - child: Builder( - builder: (context) { - final selectedFn = switchFunctions.firstWhere( - (f) => f.code == selectedFunction) - as BaseSwitchFunction; - final values = - selectedFn.getOperationalValues(); - 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(() { - selectedValue = newValue; - }); - }, - ), + // 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 ListTile( + leading: SvgPicture.asset( + value.icon, + width: 24, + height: 24, + ), + title: Text( + value.description, + 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(); + }, + ), + ); + }, + ); + }, ), + ), ], ), ), @@ -144,55 +218,35 @@ class TwoGangSwitchHelper { Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - GestureDetector( - onTap: () { - Navigator.pop(context); - }, - child: Container( - height: 50, - width: selectedFunction != null ? 299 : 179, - decoration: const BoxDecoration( - border: Border( - right: - BorderSide(color: ColorsManager.greyColor), - ), - ), - child: Center( - child: Text( - 'Cancel', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: ColorsManager.greyColor), - ), - ), + TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + 'Cancel', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: ColorsManager.greyColor), ), ), - GestureDetector( - onTap: () { - if (selectedFunction != null && - selectedValue != null) { - Navigator.pop(context, { - 'function': selectedFunction, - 'value': selectedValue, - }); - } - }, - child: SizedBox( - height: 50, - width: selectedFunction != null ? 299 : 179, - child: Center( - child: Text( - 'Confirm', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith( - color: - ColorsManager.primaryColorWithOpacity, - ), - ), - ), + 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, + ), ), ), ], @@ -206,4 +260,78 @@ class TwoGangSwitchHelper { }, ); } + + /// 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, + ) { + 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()); + }); + }, + ), + ], + ); + } + + /// 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(">")], + ); + } } diff --git a/lib/pages/routiens/models/device_functions.dart b/lib/pages/routiens/models/device_functions.dart index fa6ac673..303fe48c 100644 --- a/lib/pages/routiens/models/device_functions.dart +++ b/lib/pages/routiens/models/device_functions.dart @@ -15,3 +15,47 @@ abstract class DeviceFunction { T execute(T currentStatus, dynamic newValue); } + +class DeviceFunctionData { + final String entityId; + final String actionExecutor; + final String function; + final String operationName; + final dynamic value; + final String? condition; + final String? valueDescription; + + DeviceFunctionData({ + required this.entityId, + this.actionExecutor = 'function', + required this.function, + required this.operationName, + required this.value, + this.condition, + this.valueDescription, + }); + + Map toJson() { + return { + 'entityId': entityId, + 'actionExecutor': actionExecutor, + 'function': function, + 'operationName': operationName, + 'value': value, + if (condition != null) 'condition': condition, + if (valueDescription != null) 'valueDescription': valueDescription, + }; + } + + factory DeviceFunctionData.fromJson(Map json) { + return DeviceFunctionData( + entityId: json['entityId'], + actionExecutor: json['actionExecutor'] ?? 'function', + function: json['function'], + operationName: json['operationName'], + value: json['value'], + condition: json['condition'], + valueDescription: json['valueDescription'], + ); + } +} diff --git a/lib/pages/routiens/widgets/dragable_card.dart b/lib/pages/routiens/widgets/dragable_card.dart index 8ed4afff..7ee078ff 100644 --- a/lib/pages/routiens/widgets/dragable_card.dart +++ b/lib/pages/routiens/widgets/dragable_card.dart @@ -1,5 +1,7 @@ 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/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; @@ -46,51 +48,110 @@ class DraggableCard extends StatelessWidget { } Widget _buildCardContent(BuildContext context) { - return Card( - color: ColorsManager.whiteColors, - child: SizedBox( - height: 123, - width: 90, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - height: 50, - width: 50, - decoration: BoxDecoration( - color: ColorsManager.CircleImageBackground, - borderRadius: BorderRadius.circular(90), - border: Border.all( - color: ColorsManager.graysColor, + return BlocBuilder( + builder: (context, state) { + // Filter functions for this device + final deviceFunctions = state.selectedFunctions + .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), + ), + 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: titleColor ?? ColorsManager.blackColor, + fontSize: 12, + ), + ), + ), + ], + ), ), - ), - padding: const EdgeInsets.all(8), - child: imagePath.contains('.svg') - ? SvgPicture.asset( - imagePath, - ) - : Image.network(imagePath), + 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, + ), + ), + ), + ], + ); + }, + ), + ], + ], ), - 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: titleColor ?? ColorsManager.blackColor, - fontSize: 12, - ), - ), - ), - ], - ), - ), + ), + ); + }, ); }