diff --git a/lib/pages/routines/widgets/condition_toggle.dart b/lib/pages/routines/widgets/condition_toggle.dart new file mode 100644 index 00000000..99ea2f04 --- /dev/null +++ b/lib/pages/routines/widgets/condition_toggle.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class ConditionToggle extends StatelessWidget { + final String? currentCondition; + final void Function(String condition) onChanged; + + const ConditionToggle({ + required this.onChanged, + this.currentCondition, + super.key, + }); + + static const _conditions = ["<", "==", ">"]; + + @override + Widget build(BuildContext context) { + return ToggleButtons( + onPressed: (index) => onChanged(_conditions[index]), + 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: _conditions.map((c) => c == (currentCondition ?? "==")).toList(), + children: _conditions.map((c) => Text(c)).toList(), + ); + } +} diff --git a/lib/pages/routines/widgets/function_slider.dart b/lib/pages/routines/widgets/function_slider.dart new file mode 100644 index 00000000..b7fb0d53 --- /dev/null +++ b/lib/pages/routines/widgets/function_slider.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +class FunctionSlider extends StatelessWidget { + final dynamic initialValue; + + final (double min, double max) range; + final void Function(double value) onChanged; + + const FunctionSlider({ + required this.onChanged, + required this.initialValue, + required this.range, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Slider( + value: initialValue is int ? initialValue.toDouble() : range.$1, + min: range.$1, + max: range.$2, + divisions: (range.$2 - range.$1).toInt(), + onChanged: onChanged, + ); + } +} diff --git a/lib/pages/routines/widgets/routine_dialogs/wall_sensor/wall_presence_sensor.dart b/lib/pages/routines/widgets/routine_dialogs/wall_sensor/wall_presence_sensor.dart index b7733511..91abb34e 100644 --- a/lib/pages/routines/widgets/routine_dialogs/wall_sensor/wall_presence_sensor.dart +++ b/lib/pages/routines/widgets/routine_dialogs/wall_sensor/wall_presence_sensor.dart @@ -1,17 +1,16 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart'; import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routines/models/device_functions.dart'; import 'package:syncrow_web/pages/routines/models/wps/wps_functions.dart'; -import 'package:syncrow_web/pages/routines/models/wps/wps_operational_value.dart'; import 'package:syncrow_web/pages/routines/widgets/dialog_footer.dart'; import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart'; -import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/wall_sensor/time_wheel.dart'; +import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/wall_sensor/wps_value_selector_widget.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart'; class WallPresenceSensor extends StatefulWidget { final List functions; @@ -63,8 +62,7 @@ class _WallPresenceSensorState extends State { @override void initState() { super.initState(); - _wpsFunctions = - widget.functions.whereType().where((function) { + _wpsFunctions = widget.functions.whereType().where((function) { if (widget.dialogType == 'THEN') { return function.type == 'THEN' || function.type == 'BOTH'; } @@ -176,10 +174,10 @@ class _WallPresenceSensorState extends State { ); return Expanded( - child: _ValueSelector( + child: WpsValueSelectorWidget( selectedFunction: selectedFunction, functionData: functionData, - acFunctions: _wpsFunctions, + wpsFunctions: _wpsFunctions, device: widget.device, dialogType: widget.dialogType!, removeComparators: widget.removeComparetors, @@ -208,342 +206,3 @@ class _WallPresenceSensorState extends State { ); } } - -class _ValueSelector extends StatelessWidget { - final String selectedFunction; - final DeviceFunctionData functionData; - final List acFunctions; - final AllDevicesModel? device; - final String dialogType; - final bool removeComparators; - - const _ValueSelector({ - required this.selectedFunction, - required this.functionData, - required this.acFunctions, - required this.device, - required this.dialogType, - required this.removeComparators, - }); - - @override - Widget build(BuildContext context) { - final selectedFn = - acFunctions.firstWhere((f) => f.code == selectedFunction); - final values = selectedFn.getOperationalValues(); - - if (_isSliderFunction(selectedFunction)) { - return _SliderValueSelector( - selectedFunction: selectedFunction, - functionData: functionData, - device: device, - dialogType: dialogType, - ); - } - - return _OperationalValuesList( - values: values, - selectedValue: functionData.value, - device: device, - operationName: selectedFn.operationName, - selectCode: selectedFunction, - ); - } - - bool _isSliderFunction(String function) => - ['dis_current', 'presence_time', 'illuminance_value'].contains(function); -} - -class _SliderValueSelector extends StatelessWidget { - final String selectedFunction; - final DeviceFunctionData functionData; - final AllDevicesModel? device; - final String dialogType; - - const _SliderValueSelector({ - required this.selectedFunction, - required this.functionData, - required this.device, - required this.dialogType, - }); - - @override - Widget build(BuildContext context) { - final initialValue = functionData.value ?? 250; - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(height: 20), - _ConditionToggle( - currentCondition: functionData.condition, - selectCode: selectedFunction, - device: device, - operationName: functionData.operationName, - selectedValue: functionData.value, - ), - _ValueDisplay( - value: initialValue, - functionCode: selectedFunction, - ), - const SizedBox(height: 20), - _FunctionSlider( - initialValue: initialValue, - functionCode: selectedFunction, - functionData: functionData, - device: device, - ), - ], - ); - } -} - -class _ConditionToggle extends StatelessWidget { - final String? currentCondition; - final String selectCode; - final AllDevicesModel? device; - final String operationName; - final dynamic selectedValue; - - const _ConditionToggle({ - this.currentCondition, - required this.selectCode, - this.device, - required this.operationName, - this.selectedValue, - }); - - @override - Widget build(BuildContext context) { - const conditions = ["<", "==", ">"]; - return ToggleButtons( - onPressed: (index) => _updateCondition(context, conditions[index]), - 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: - conditions.map((c) => c == (currentCondition ?? "==")).toList(), - children: conditions.map((c) => Text(c)).toList(), - ); - } - - void _updateCondition(BuildContext context, String condition) { - context.read().add( - AddFunction( - functionData: DeviceFunctionData( - entityId: device?.uuid ?? '', - functionCode: selectCode, - operationName: operationName, - condition: condition, - value: selectedValue, - ), - ), - ); - } -} - -class _ValueDisplay extends StatelessWidget { - final dynamic value; - final String functionCode; - - const _ValueDisplay({ - required this.value, - required this.functionCode, - }); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - decoration: BoxDecoration( - color: ColorsManager.primaryColorWithOpacity.withOpacity(0.1), - borderRadius: BorderRadius.circular(10), - ), - child: Text( - _getDisplayText(), - style: context.textTheme.headlineMedium!.copyWith( - color: ColorsManager.primaryColorWithOpacity, - ), - ), - ); - } - - String _getDisplayText() { - final intValue = (value as num?)?.toInt() ?? 0; - switch (functionCode) { - case 'presence_time': - return '$intValue Min'; - case 'dis_current': - return '$intValue CM'; - case 'illuminance_value': - return '$intValue Lux'; - default: - return '$intValue'; - } - } -} - -class _FunctionSlider extends StatelessWidget { - final dynamic initialValue; - final String functionCode; - final DeviceFunctionData functionData; - final AllDevicesModel? device; - - const _FunctionSlider({ - required this.initialValue, - required this.functionCode, - required this.functionData, - required this.device, - }); - - @override - Widget build(BuildContext context) { - final (min, max) = _getSliderRange(); - return Slider( - value: initialValue is int ? initialValue.toDouble() : min, - min: min, - max: max, - divisions: (max - min).toInt(), - onChanged: (value) => _updateValue(context, value.toInt()), - ); - } - - (double, double) _getSliderRange() { - switch (functionCode) { - case 'presence_time': - return (0, 65535); - case 'dis_current': - return (1, 600); - case 'illuminance_value': - return (0, 10000); - default: - return (200, 300); - } - } - - void _updateValue(BuildContext context, int value) { - context.read().add( - AddFunction( - functionData: DeviceFunctionData( - entityId: device?.uuid ?? '', - functionCode: functionCode, - operationName: functionData.operationName, - value: value, - condition: functionData.condition, - ), - ), - ); - } -} - -class _OperationalValuesList extends StatelessWidget { - final List values; - final dynamic selectedValue; - final AllDevicesModel? device; - final String operationName; - final String selectCode; - - const _OperationalValuesList({ - required this.values, - required this.selectedValue, - required this.device, - required this.operationName, - required this.selectCode, - }); - - @override - Widget build(BuildContext context) { - return operationName == 'Nobody Time' - ? _buildTimeWheel(context) - : ListView.builder( - padding: const EdgeInsets.all(20), - itemCount: values.length, - itemBuilder: (context, index) => - _buildValueItem(context, values[index]), - ); - } - - Widget _buildTimeWheel(BuildContext context) { - final currentTotalSeconds = selectedValue as int? ?? 0; - return TimeWheelPicker( - initialHours: currentTotalSeconds ~/ 3600, - initialMinutes: (currentTotalSeconds % 3600) ~/ 60, - initialSeconds: currentTotalSeconds % 60, - onTimeChanged: (h, m, s) => _updateTotalSeconds(context, h, m, s), - ); - } - - Widget _buildValueItem(BuildContext context, WpsOperationalValue value) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _buildValueIcon(context, value), - Expanded(child: _buildValueDescription(value)), - _buildValueRadio(context, value), - ], - ), - ); - } - - Widget _buildValueIcon(context, WpsOperationalValue value) { - return Column( - children: [ - if (_shouldShowTextDescription) - Text(value.description.replaceAll("cm", '')), - SvgPicture.asset(value.icon, width: 25, height: 25), - ], - ); - } - - bool get _shouldShowTextDescription => - operationName == 'Far Detection' || - operationName == 'Motionless Detection Sensitivity'; - - Widget _buildValueDescription(WpsOperationalValue value) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Text(value.description), - ); - } - - Widget _buildValueRadio(context, WpsOperationalValue value) { - return Radio( - value: value.value, - groupValue: selectedValue, - onChanged: (_) => _selectValue(context, value.value), - ); - } - - void _selectValue(BuildContext context, dynamic value) { - context.read().add( - AddFunction( - functionData: DeviceFunctionData( - entityId: device?.uuid ?? '', - functionCode: selectCode, - operationName: operationName, - value: value, - ), - ), - ); - } - - void _updateTotalSeconds(BuildContext context, int h, int m, int s) { - context.read().add( - AddFunction( - functionData: DeviceFunctionData( - entityId: device?.uuid ?? '', - functionCode: selectCode, - operationName: operationName, - value: h * 3600 + m * 60 + s, - ), - ), - ); - } -} diff --git a/lib/pages/routines/widgets/routine_dialogs/wall_sensor/wps_operational_values_list.dart b/lib/pages/routines/widgets/routine_dialogs/wall_sensor/wps_operational_values_list.dart new file mode 100644 index 00000000..6c149cd3 --- /dev/null +++ b/lib/pages/routines/widgets/routine_dialogs/wall_sensor/wps_operational_values_list.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart'; +import 'package:syncrow_web/pages/routines/models/device_functions.dart'; +import 'package:syncrow_web/pages/routines/models/wps/wps_operational_value.dart'; +import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/wall_sensor/time_wheel.dart'; + +class WpsOperationalValuesList extends StatelessWidget { + final List values; + final dynamic selectedValue; + final AllDevicesModel? device; + final String operationName; + final String selectCode; + + const WpsOperationalValuesList({ + required this.values, + required this.selectedValue, + required this.device, + required this.operationName, + required this.selectCode, + super.key, + }); + + @override + Widget build(BuildContext context) { + return operationName == 'Nobody Time' + ? _buildTimeWheel(context) + : ListView.builder( + padding: const EdgeInsets.all(20), + itemCount: values.length, + itemBuilder: (context, index) => _buildValueItem(context, values[index]), + ); + } + + Widget _buildTimeWheel(BuildContext context) { + final currentTotalSeconds = selectedValue as int? ?? 0; + return TimeWheelPicker( + initialHours: currentTotalSeconds ~/ 3600, + initialMinutes: (currentTotalSeconds % 3600) ~/ 60, + initialSeconds: currentTotalSeconds % 60, + onTimeChanged: (h, m, s) => _updateTotalSeconds(context, h, m, s), + ); + } + + Widget _buildValueItem(BuildContext context, WpsOperationalValue value) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildValueIcon(context, value), + Expanded(child: _buildValueDescription(value)), + _buildValueRadio(context, value), + ], + ), + ); + } + + Widget _buildValueIcon(context, WpsOperationalValue value) { + return Column( + children: [ + if (_shouldShowTextDescription) Text(value.description.replaceAll("cm", '')), + SvgPicture.asset(value.icon, width: 25, height: 25), + ], + ); + } + + bool get _shouldShowTextDescription => + operationName == 'Far Detection' || + operationName == 'Motionless Detection Sensitivity'; + + Widget _buildValueDescription(WpsOperationalValue value) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text(value.description), + ); + } + + Widget _buildValueRadio(context, WpsOperationalValue value) { + return Radio( + value: value.value, + groupValue: selectedValue, + onChanged: (_) => _selectValue(context, value.value), + ); + } + + void _selectValue(BuildContext context, dynamic value) { + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + value: value, + ), + ), + ); + } + + void _updateTotalSeconds(BuildContext context, int h, int m, int s) { + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + value: h * 3600 + m * 60 + s, + ), + ), + ); + } +} diff --git a/lib/pages/routines/widgets/routine_dialogs/wall_sensor/wps_value_selector_widget.dart b/lib/pages/routines/widgets/routine_dialogs/wall_sensor/wps_value_selector_widget.dart new file mode 100644 index 00000000..5f38b240 --- /dev/null +++ b/lib/pages/routines/widgets/routine_dialogs/wall_sensor/wps_value_selector_widget.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart'; +import 'package:syncrow_web/pages/routines/models/device_functions.dart'; +import 'package:syncrow_web/pages/routines/models/wps/wps_functions.dart'; +import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/wall_sensor/wps_operational_values_list.dart'; +import 'package:syncrow_web/pages/routines/widgets/slider_value_selector.dart'; + +class WpsValueSelectorWidget extends StatelessWidget { + final String selectedFunction; + final DeviceFunctionData functionData; + final List wpsFunctions; + final AllDevicesModel? device; + final String dialogType; + final bool removeComparators; + + const WpsValueSelectorWidget({ + required this.selectedFunction, + required this.functionData, + required this.wpsFunctions, + required this.device, + required this.dialogType, + required this.removeComparators, + super.key, + }); + + @override + Widget build(BuildContext context) { + final selectedFn = wpsFunctions.firstWhere((f) => f.code == selectedFunction); + final values = selectedFn.getOperationalValues(); + + if (_isSliderFunction(selectedFunction)) { + return SliderValueSelector( + selectedFunction: selectedFunction, + functionData: functionData, + device: device, + dialogType: dialogType, + sliderRange: sliderRange, + displayedValue: getDisplayText, + initialValue: functionData.value ?? 250, + onConditionChanged: (condition) => context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectedFunction, + operationName: functionData.operationName, + condition: condition, + value: functionData.value, + ), + ), + ), + onSliderChanged: (value) => context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectedFunction, + operationName: functionData.operationName, + value: value.toInt(), + condition: functionData.condition, + ), + ), + ), + ); + } + + return WpsOperationalValuesList( + values: values, + selectedValue: functionData.value, + device: device, + operationName: selectedFn.operationName, + selectCode: selectedFunction, + ); + } + + bool _isSliderFunction(String function) => + ['dis_current', 'presence_time', 'illuminance_value'].contains(function); + + (double, double) get sliderRange => switch (functionData.functionCode) { + 'presence_time' => (0, 65535), + 'dis_current' => (0, 600), + 'illuminance_value' => (0, 10000), + _ => (200, 300), + }; + + String get getDisplayText { + final intValue = int.tryParse('${functionData.value ?? ''}'); + return switch (functionData.functionCode) { + 'presence_time' => '${intValue ?? '0'} Min', + 'dis_current' => '${intValue ?? '250'} CM', + 'illuminance_value' => '${intValue ?? '0'} Lux', + _ => '$intValue', + }; + } +} diff --git a/lib/pages/routines/widgets/slider_value_selector.dart b/lib/pages/routines/widgets/slider_value_selector.dart new file mode 100644 index 00000000..6b408008 --- /dev/null +++ b/lib/pages/routines/widgets/slider_value_selector.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/routines/models/device_functions.dart'; +import 'package:syncrow_web/pages/routines/widgets/condition_toggle.dart'; +import 'package:syncrow_web/pages/routines/widgets/function_slider.dart'; +import 'package:syncrow_web/pages/routines/widgets/value_display.dart'; + +class SliderValueSelector extends StatelessWidget { + final String selectedFunction; + final DeviceFunctionData functionData; + final AllDevicesModel? device; + final String dialogType; + final (double, double) sliderRange; + final String displayedValue; + final Object? initialValue; + final void Function(String condition) onConditionChanged; + final void Function(double value) onSliderChanged; + + const SliderValueSelector({ + required this.selectedFunction, + required this.functionData, + required this.device, + required this.dialogType, + required this.sliderRange, + required this.displayedValue, + required this.initialValue, + required this.onConditionChanged, + required this.onSliderChanged, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 20), + ConditionToggle( + currentCondition: functionData.condition, + onChanged: onConditionChanged, + ), + ValueDisplay( + value: initialValue, + label: displayedValue, + ), + const SizedBox(height: 20), + FunctionSlider( + initialValue: initialValue, + range: sliderRange, onChanged: onSliderChanged, + // void _updateValue(BuildContext context, int value) { + // context.read().add( + // AddFunction( + // functionData: DeviceFunctionData( + // entityId: device?.uuid ?? '', + // functionCode: functionCode, + // operationName: functionData.operationName, + // value: value, + // condition: functionData.condition, + // ), + // ), + // ); + // } + ), + ], + ); + } +} diff --git a/lib/pages/routines/widgets/value_display.dart b/lib/pages/routines/widgets/value_display.dart new file mode 100644 index 00000000..e9b76325 --- /dev/null +++ b/lib/pages/routines/widgets/value_display.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class ValueDisplay extends StatelessWidget { + final dynamic value; + final String label; + + const ValueDisplay({ + required this.value, + required this.label, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + decoration: BoxDecoration( + color: ColorsManager.primaryColorWithOpacity.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + label, + style: context.textTheme.headlineMedium!.copyWith( + color: ColorsManager.primaryColorWithOpacity, + ), + ), + ); + } +}