diff --git a/.github/workflows/azure-static-web-apps-mango-bush-01e607f10.yml b/.github/workflows/azure-static-web-apps-mango-bush-01e607f10.yml index 95e9346d..f0379c95 100644 --- a/.github/workflows/azure-static-web-apps-mango-bush-01e607f10.yml +++ b/.github/workflows/azure-static-web-apps-mango-bush-01e607f10.yml @@ -25,7 +25,7 @@ jobs: - name: Set up Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.22.2' # Specify the Flutter version you want to use + flutter-version: '3.27.3' # Specify the Flutter version you want to use - name: Install dependencies run: flutter pub get diff --git a/.github/workflows/azure-static-web-apps-polite-smoke-017c65c10.yml b/.github/workflows/azure-static-web-apps-polite-smoke-017c65c10.yml index e28d1bb2..28cf00a2 100644 --- a/.github/workflows/azure-static-web-apps-polite-smoke-017c65c10.yml +++ b/.github/workflows/azure-static-web-apps-polite-smoke-017c65c10.yml @@ -25,7 +25,7 @@ jobs: - name: Set up Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.22.2' # Specify the Flutter version you want to use + flutter-version: '3.27.3' # Specify the Flutter version you want to use - name: Install dependencies run: flutter pub get diff --git a/assets/icons/boundary.svg b/assets/icons/boundary.svg new file mode 100644 index 00000000..73b4ab24 --- /dev/null +++ b/assets/icons/boundary.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/close_to_motion.svg b/assets/icons/close_to_motion.svg new file mode 100644 index 00000000..8ba6c8c6 --- /dev/null +++ b/assets/icons/close_to_motion.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/communication_fault.svg b/assets/icons/communication_fault.svg new file mode 100644 index 00000000..e2ab1b40 --- /dev/null +++ b/assets/icons/communication_fault.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/cps_custom_mode.svg b/assets/icons/cps_custom_mode.svg new file mode 100644 index 00000000..4176c939 --- /dev/null +++ b/assets/icons/cps_custom_mode.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/icons/cps_mode1.svg b/assets/icons/cps_mode1.svg new file mode 100644 index 00000000..407105df --- /dev/null +++ b/assets/icons/cps_mode1.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/cps_mode2.svg b/assets/icons/cps_mode2.svg new file mode 100644 index 00000000..9464a7ea --- /dev/null +++ b/assets/icons/cps_mode2.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/cps_mode3.svg b/assets/icons/cps_mode3.svg new file mode 100644 index 00000000..998329fd --- /dev/null +++ b/assets/icons/cps_mode3.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/cps_mode4.svg b/assets/icons/cps_mode4.svg new file mode 100644 index 00000000..3136a806 --- /dev/null +++ b/assets/icons/cps_mode4.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/far_away_motion.svg b/assets/icons/far_away_motion.svg new file mode 100644 index 00000000..9458eb0d --- /dev/null +++ b/assets/icons/far_away_motion.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/icons/motion_meter.svg b/assets/icons/motion_meter.svg new file mode 100644 index 00000000..85469973 --- /dev/null +++ b/assets/icons/motion_meter.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/moving_speed.svg b/assets/icons/moving_speed.svg new file mode 100644 index 00000000..6db52050 --- /dev/null +++ b/assets/icons/moving_speed.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/icons/presence_judgement_threshold.svg b/assets/icons/presence_judgement_threshold.svg new file mode 100644 index 00000000..d5537da7 --- /dev/null +++ b/assets/icons/presence_judgement_threshold.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/icons/radar_fault.svg b/assets/icons/radar_fault.svg new file mode 100644 index 00000000..b2295d1a --- /dev/null +++ b/assets/icons/radar_fault.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/icons/self_testing_failure.svg b/assets/icons/self_testing_failure.svg new file mode 100644 index 00000000..c86c9ec2 --- /dev/null +++ b/assets/icons/self_testing_failure.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/self_testing_success.svg b/assets/icons/self_testing_success.svg new file mode 100644 index 00000000..1f8976b0 --- /dev/null +++ b/assets/icons/self_testing_success.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/self_testing_timeout.svg b/assets/icons/self_testing_timeout.svg new file mode 100644 index 00000000..55c1e632 --- /dev/null +++ b/assets/icons/self_testing_timeout.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/sensitivity_feature_1.svg b/assets/icons/sensitivity_feature_1.svg new file mode 100644 index 00000000..21bebd7a --- /dev/null +++ b/assets/icons/sensitivity_feature_1.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/sensitivity_feature_2.svg b/assets/icons/sensitivity_feature_2.svg new file mode 100644 index 00000000..7370ab6f --- /dev/null +++ b/assets/icons/sensitivity_feature_2.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/sensitivity_feature_3.svg b/assets/icons/sensitivity_feature_3.svg new file mode 100644 index 00000000..23b92c43 --- /dev/null +++ b/assets/icons/sensitivity_feature_3.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/sensitivity_feature_4.svg b/assets/icons/sensitivity_feature_4.svg new file mode 100644 index 00000000..7a92045f --- /dev/null +++ b/assets/icons/sensitivity_feature_4.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/sensitivity_feature_5.svg b/assets/icons/sensitivity_feature_5.svg new file mode 100644 index 00000000..5f056602 --- /dev/null +++ b/assets/icons/sensitivity_feature_5.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/sensitivity_feature_6.svg b/assets/icons/sensitivity_feature_6.svg new file mode 100644 index 00000000..288b172e --- /dev/null +++ b/assets/icons/sensitivity_feature_6.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/sensitivity_feature_7.svg b/assets/icons/sensitivity_feature_7.svg new file mode 100644 index 00000000..5779dfd2 --- /dev/null +++ b/assets/icons/sensitivity_feature_7.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/sensitivity_feature_8.svg b/assets/icons/sensitivity_feature_8.svg new file mode 100644 index 00000000..4816b1ba --- /dev/null +++ b/assets/icons/sensitivity_feature_8.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/sensitivity_feature_9.svg b/assets/icons/sensitivity_feature_9.svg new file mode 100644 index 00000000..978145b4 --- /dev/null +++ b/assets/icons/sensitivity_feature_9.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/space_type.svg b/assets/icons/space_type.svg new file mode 100644 index 00000000..af5f6845 --- /dev/null +++ b/assets/icons/space_type.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/spatial_motion_value.svg b/assets/icons/spatial_motion_value.svg new file mode 100644 index 00000000..018da674 --- /dev/null +++ b/assets/icons/spatial_motion_value.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/spatial_static_value.svg b/assets/icons/spatial_static_value.svg new file mode 100644 index 00000000..95ca6112 --- /dev/null +++ b/assets/icons/spatial_static_value.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/sports_para.svg b/assets/icons/sports_para.svg new file mode 100644 index 00000000..6f9d5ece --- /dev/null +++ b/assets/icons/sports_para.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/lib/pages/device_managment/all_devices/models/devices_model.dart b/lib/pages/device_managment/all_devices/models/devices_model.dart index 0ac3f776..d07efc9b 100644 --- a/lib/pages/device_managment/all_devices/models/devices_model.dart +++ b/lib/pages/device_managment/all_devices/models/devices_model.dart @@ -12,6 +12,7 @@ import 'package:syncrow_web/pages/routines/models/gang_switches/three_gang_switc import 'package:syncrow_web/pages/routines/models/gang_switches/two_gang_switch/two_gang_switch.dart'; import 'package:syncrow_web/pages/routines/models/gateway.dart'; import 'package:syncrow_web/pages/routines/models/wps/wps_functions.dart'; +import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ceiling_sensor/ceiling_sensor_helper.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/enum/device_types.dart'; @@ -317,6 +318,11 @@ SOS type: 'BOTH', ), ]; + case 'CPS': + return CeilingSensorHelper.getCeilingSensorFunctions( + uuid: uuid ?? '', + name: name ?? '', + ); default: return []; } diff --git a/lib/pages/routines/helper/dialog_helper/device_dialog_helper.dart b/lib/pages/routines/helper/dialog_helper/device_dialog_helper.dart index 8f7cd1c5..b1afbc12 100644 --- a/lib/pages/routines/helper/dialog_helper/device_dialog_helper.dart +++ b/lib/pages/routines/helper/dialog_helper/device_dialog_helper.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_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/widgets/routine_dialogs/ac_dialog.dart'; +import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ceiling_sensor/ceiling_sensor_helper.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/gateway/gateway_helper.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/one_gang_switch_dialog.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/three_gang_switch_dialog.dart'; @@ -98,6 +99,15 @@ class DeviceDialogHelper { deviceSelectedFunctions: deviceSelectedFunctions, uniqueCustomId: data['uniqueCustomId'], removeComparetors: removeComparetors); + case 'CPS': + return CeilingSensorHelper.showCeilingSensorDialog( + context: context, + functions: functions, + device: data['device'], + deviceSelectedFunctions: deviceSelectedFunctions, + uniqueCustomId: data['uniqueCustomId'], + dialogType: dialogType, + ); case 'GW': return GatewayHelper.showGatewayFunctionsDialog( context: context, diff --git a/lib/pages/routines/models/ceiling_presence_sensor_functions.dart b/lib/pages/routines/models/ceiling_presence_sensor_functions.dart new file mode 100644 index 00000000..6dbe5cf6 --- /dev/null +++ b/lib/pages/routines/models/ceiling_presence_sensor_functions.dart @@ -0,0 +1,889 @@ +import 'package:syncrow_web/pages/routines/models/device_functions.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class CpsOperationalValue { + final String icon; + final String description; + final dynamic value; + + CpsOperationalValue({ + required this.icon, + required this.description, + required this.value, + }); +} + +abstract class CpsFunctions extends DeviceFunction { + CpsFunctions({ + required super.deviceId, + required super.deviceName, + required super.code, + required super.operationName, + required super.icon, + required this.type, + }); + + final String type; + + List getOperationalValues(); +} + +final class CpsRadarSwitchFunction extends CpsFunctions { + CpsRadarSwitchFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'radar_switch', + operationName: 'Radar Switch', + icon: Assets.acPower, + ); + + @override + List getOperationalValues() => [ + CpsOperationalValue( + icon: Assets.assetsAcPower, + description: "ON", + value: true, + ), + CpsOperationalValue( + icon: Assets.assetsAcPowerOFF, + description: "OFF", + value: false, + ), + ]; +} + +final class CpsSpatialParameterSwitchFunction extends CpsFunctions { + CpsSpatialParameterSwitchFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'space_para_switch', + operationName: 'Spatial Parameter Switch', + icon: Assets.acPower, + ); + + @override + List getOperationalValues() => [ + CpsOperationalValue( + icon: Assets.assetsAcPower, + description: "ON", + value: true, + ), + CpsOperationalValue( + icon: Assets.assetsAcPowerOFF, + description: "OFF", + value: false, + ), + ]; +} + +final class CpsSensitivityFunction extends CpsFunctions { + CpsSensitivityFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : min = 1, + max = 10, + step = 1, + super( + code: 'sensitivity', + operationName: 'Sensitivity', + icon: Assets.sensitivity, + ); + + final int min; + final int max; + final int step; + + static const _images = [ + Assets.sensitivityFeature1, + Assets.sensitivityFeature1, + Assets.sensitivityFeature2, + Assets.sensitivityFeature3, + Assets.sensitivityFeature4, + Assets.sensitivityFeature5, + Assets.sensitivityFeature6, + Assets.sensitivityFeature7, + Assets.sensitivityFeature8, + Assets.sensitivityFeature9, + Assets.sensitivityFeature9, + ]; + + @override + List getOperationalValues() { + final values = []; + for (var value = min; value <= max; value += step) { + values.add( + CpsOperationalValue( + icon: _images[value], + description: '$value', + value: value, + ), + ); + } + return values; + } +} + +final class CpsMovingSpeedFunction extends CpsFunctions { + CpsMovingSpeedFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : min = 0, + max = 32, + step = 1, + super( + code: 'moving_speed', + operationName: 'Moving Speed', + icon: Assets.speedoMeter, + ); + + final int min; + final int max; + final int step; + + @override + List getOperationalValues() { + return List.generate( + (max - min) ~/ step + 1, + (index) => CpsOperationalValue( + icon: Assets.speedoMeter, + description: '${min + (index * step)}', + value: min + (index * step), + ), + ); + } +} + +final class CpsSpatialStaticValueFunction extends CpsFunctions { + CpsSpatialStaticValueFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : min = 0, + max = 255, + step = 1, + super( + code: 'space_static_val', + operationName: 'Spacial Static Value', + icon: Assets.spatialStaticValue, + ); + + final int min; + final int max; + final int step; + + @override + List getOperationalValues() { + return List.generate( + (max - min) ~/ step + 1, + (index) => CpsOperationalValue( + icon: Assets.spatialStaticValue, + description: '${min + (index * step)}', + value: min + (index * step), + ), + ); + } +} + +final class CpsSpatialMotionValueFunction extends CpsFunctions { + CpsSpatialMotionValueFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : min = 0, + max = 255, + step = 1, + super( + code: 'space_move_val', + operationName: 'Spatial Motion Value', + icon: Assets.spatialMotionValue, + ); + + final int min; + final int max; + final int step; + + @override + List getOperationalValues() { + return List.generate( + (max - min) ~/ step + 1, + (index) => CpsOperationalValue( + icon: Assets.spatialMotionValue, + description: '${min + (index * step)}', + value: min + (index * step), + ), + ); + } +} + +final class CpsMaxDistanceOfDetectionFunction extends CpsFunctions { + CpsMaxDistanceOfDetectionFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : min = 0.0, + max = 10.0, + step = 0.5, + super( + code: 'moving_max_dis', + operationName: 'Maximum Distance Of Detection', + icon: Assets.currentDistanceIcon, + ); + + final double min; + final double max; + final double step; + + @override + List getOperationalValues() { + final count = ((max - min) / step).round() + 1; + return List.generate( + count, + (index) { + final value = (min + (index * step)); + return CpsOperationalValue( + icon: Assets.currentDistanceIcon, + description: '${value.toStringAsFixed(1)} M', + value: value, + ); + }, + ); + } +} + +final class CpsMaxDistanceOfStaticDetectionFunction extends CpsFunctions { + CpsMaxDistanceOfStaticDetectionFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : min = 0.0, + max = 10.0, + step = 0.5, + super( + code: 'static_max_dis', + operationName: 'Maximum Distance Of Static Detection', + icon: Assets.currentDistanceIcon, + ); + + final double min; + final double max; + final double step; + + @override + List getOperationalValues() { + final count = ((max - min) / step).round() + 1; + return List.generate( + count, + (index) { + final value = (min + (index * step)); + return CpsOperationalValue( + icon: Assets.currentDistanceIcon, + description: '${value.toStringAsFixed(1)} M', + value: value, + ); + }, + ); + } +} + +final class CpsDetectionRangeFunction extends CpsFunctions { + CpsDetectionRangeFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : min = 0.0, + max = 25.5, + step = 0.1, + super( + code: 'moving_range', + operationName: 'Detection Range', + icon: Assets.farDetection, + ); + + final double min; + final double max; + final double step; + + @override + List getOperationalValues() { + final count = ((max - min) / step).round() + 1; + return List.generate( + count, + (index) { + final value = (min + (index * step)); + return CpsOperationalValue( + icon: Assets.farDetection, + description: '${value.toStringAsFixed(1)} M', + value: value, + ); + }, + ); + } +} + +final class CpsDistanceOfMovingObjectsFunction extends CpsFunctions { + CpsDistanceOfMovingObjectsFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : min = 0.0, + max = 25.5, + step = 0.1, + super( + code: 'presence_range', + operationName: 'Distance Of Moving Objects', + icon: Assets.currentDistanceIcon, + ); + + final double min; + final double max; + final double step; + + @override + List getOperationalValues() { + final count = ((max - min) / step).round() + 1; + return List.generate( + count, + (index) { + final value = (min + (index * step)); + return CpsOperationalValue( + icon: Assets.currentDistanceIcon, + description: '${value.toStringAsFixed(1)} M', + value: value, + ); + }, + ); + } +} + +final class CpsPresenceJudgementThrsholdFunction extends CpsFunctions { + CpsPresenceJudgementThrsholdFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : min = 0, + max = 255, + step = 5, + super( + code: 'presence_reference', + operationName: 'Presence Judgement Threshold', + icon: Assets.presenceJudgementThrshold, + ); + + final int min; + final int max; + final int step; + + @override + List getOperationalValues() { + return List.generate( + (max - min) ~/ step + 1, + (index) => CpsOperationalValue( + icon: Assets.presenceJudgementThrshold, + description: '${min + (index * step)}', + value: min + (index * step), + ), + ); + } +} + +final class CpsMotionAmplitudeTriggerThresholdFunction extends CpsFunctions { + CpsMotionAmplitudeTriggerThresholdFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : min = 0, + max = 255, + step = 5, + super( + code: 'moving_reference', + operationName: 'Motion Amplitude Trigger Threshold', + icon: Assets.presenceJudgementThrshold, + ); + + final int min; + final int max; + final int step; + + @override + List getOperationalValues() { + return List.generate( + (max - min) ~/ step + 1, + (index) => CpsOperationalValue( + icon: Assets.presenceJudgementThrshold, + description: '${min + (index * step)}', + value: min + (index * step), + ), + ); + } +} + +final class CpsPerpetualBoundaryFunction extends CpsFunctions { + CpsPerpetualBoundaryFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : min = 0.00, + max = 5.00, + step = 0.50, + super( + code: 'perceptual_boundary', + operationName: 'Perpetual Boundary', + icon: Assets.boundary, + ); + + final double min; + final double max; + final double step; + + @override + List getOperationalValues() { + final count = ((max - min) / step).round() + 1; + return List.generate( + count, + (index) { + final value = (min + (index * step)); + return CpsOperationalValue( + icon: Assets.boundary, + description: '${value.toStringAsFixed(1)}M', + value: value + 1200, + ); + }, + ); + } +} + +final class CpsMotionTriggerBoundaryFunction extends CpsFunctions { + CpsMotionTriggerBoundaryFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : min = 0.0, + max = 5.0, + step = 0.5, + super( + code: 'moving_boundary', + operationName: 'Motion Trigger Boundary', + icon: Assets.motionMeter, + ); + + final double min; + final double max; + final double step; + + @override + List getOperationalValues() { + final count = ((max - min) / step).round() + 1; + return List.generate( + count, + (index) { + final value = (min + (index * step)); + return CpsOperationalValue( + icon: Assets.motionMeter, + description: '${value.toStringAsFixed(1)} M', + value: value, + ); + }, + ); + } +} + +final class CpsMotionTriggerTimeFunction extends CpsFunctions { + CpsMotionTriggerTimeFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : min = 0.0, + max = 2.0, + step = 0.1, + super( + code: 'moving_rigger_time', + operationName: 'Motion Trigger Time', + icon: Assets.motionMeter, + ); + + final double min; + final double max; + final double step; + + @override + List getOperationalValues() { + final count = ((max - min) / step).round() + 1; + return List.generate( + count, + (index) { + final value = (min + (index * step)); + return CpsOperationalValue( + icon: Assets.motionMeter, + description: '${value.toStringAsFixed(3)} sec', + value: value, + ); + }, + ); + } +} + +final class CpsMotionToStaticTimeFunction extends CpsFunctions { + CpsMotionToStaticTimeFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : min = 0.0, + max = 50.0, + step = 1.0, + super( + code: 'moving_static_time', + operationName: 'Motion To Static Time', + icon: Assets.motionMeter, + ); + + final double min; + final double max; + final double step; + + @override + List getOperationalValues() { + final count = ((max - min) / step).round() + 1; + return List.generate( + count, + (index) { + final value = (min + (index * step)); + return CpsOperationalValue( + icon: Assets.motionMeter, + description: '${value.toStringAsFixed(0)} sec', + value: value, + ); + }, + ); + } +} + +final class CpsEnteringNoBodyStateTimeFunction extends CpsFunctions { + CpsEnteringNoBodyStateTimeFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : min = 0.0, + max = 300.0, + step = 5.0, + super( + code: 'none_body_time', + operationName: 'Entering Nobody State Time', + icon: Assets.motionMeter, + ); + + final double min; + final double max; + final double step; + + @override + List getOperationalValues() { + final count = ((max - min) / step).round() + 1; + return List.generate( + count, + (index) { + final value = (min + (index * step)); + return CpsOperationalValue( + icon: Assets.motionMeter, + description: '${value.toStringAsFixed(0)} sec', + value: value, + ); + }, + ); + } +} + +final class CpsSelfTestResultFunctions extends CpsFunctions { + CpsSelfTestResultFunctions({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'checking_result', + operationName: 'Self-Test Result', + icon: Assets.selfTestResult, + ); + @override + List getOperationalValues() { + return [ + CpsOperationalValue( + description: 'Self Testing', + icon: Assets.selfTestResult, + value: 'check', + ), + CpsOperationalValue( + description: 'Self Testing Success', + icon: Assets.selfTestingSuccess, + value: 'check_success', + ), + CpsOperationalValue( + description: 'Self Testing Failure', + icon: Assets.selfTestingFailure, + value: 'check_failure', + ), + CpsOperationalValue( + description: 'Self Testing Timeout', + icon: Assets.selfTestingTimeout, + value: 'check_timeout', + ), + CpsOperationalValue( + description: 'Communication Fault', + icon: Assets.communicationFault, + value: 'communication_fault', + ), + CpsOperationalValue( + description: 'Radar Fault', + icon: Assets.radarFault, + value: 'radar_fault', + ), + ]; + } +} + +final class CpsNobodyTimeFunction extends CpsFunctions { + CpsNobodyTimeFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'nobody_time', + operationName: 'Entering Nobody Time', + icon: Assets.assetsNobodyTime, + ); + + @override + List getOperationalValues() { + return [ + CpsOperationalValue( + icon: Assets.assetsNobodyTime, + description: 'None', + value: 'none', + ), + CpsOperationalValue( + icon: Assets.assetsNobodyTime, + description: '10sec', + value: '10s', + ), + CpsOperationalValue( + icon: Assets.assetsNobodyTime, + description: '30sec', + value: '30s', + ), + CpsOperationalValue( + icon: Assets.assetsNobodyTime, + description: '1min', + value: '1min', + ), + CpsOperationalValue( + icon: Assets.assetsNobodyTime, + description: '2min', + value: '2min', + ), + CpsOperationalValue( + icon: Assets.assetsNobodyTime, + description: '5min', + value: '5min', + ), + CpsOperationalValue( + icon: Assets.assetsNobodyTime, + description: '10min', + value: '10min', + ), + CpsOperationalValue( + icon: Assets.assetsNobodyTime, + description: '30min', + value: '30min', + ), + CpsOperationalValue( + icon: Assets.assetsNobodyTime, + description: '1hour', + value: '1hr', + ), + ]; + } +} + +final class CpsMovementFunctions extends CpsFunctions { + CpsMovementFunctions({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'body_movement', + operationName: 'Movement', + icon: Assets.motion, + ); + @override + List getOperationalValues() { + return [ + CpsOperationalValue( + description: 'None', + icon: Assets.nobodyTime, + value: 'none', + ), + CpsOperationalValue( + description: 'Close To', + icon: Assets.closeToMotion, + value: 'close_to', + ), + CpsOperationalValue( + description: 'Far Away', + icon: Assets.farAwayMotion, + value: 'far_away', + ), + ]; + } +} + +final class CpsCustomModeFunction extends CpsFunctions { + CpsCustomModeFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'custom_mode', + operationName: 'Custom Mode', + icon: Assets.cpsCustomMode, + ); + + @override + List getOperationalValues() { + return [ + CpsOperationalValue( + icon: Assets.cpsMode1, + description: 'Mode 1', + value: 'mode1', + ), + CpsOperationalValue( + icon: Assets.cpsMode2, + description: 'Mode 2', + value: 'mode2', + ), + CpsOperationalValue( + icon: Assets.cpsMode3, + description: 'Mode 3', + value: 'mode3', + ), + CpsOperationalValue( + icon: Assets.cpsMode4, + description: 'Mode 4', + value: 'mode4', + ), + ]; + } +} + +final class CpsSpaceTypeFunctions extends CpsFunctions { + CpsSpaceTypeFunctions({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'scene', + operationName: 'Space Type', + icon: Assets.spaceType, + ); + @override + List getOperationalValues() { + return [ + CpsOperationalValue( + description: 'Office', + icon: Assets.office, + value: 'office', + ), + CpsOperationalValue( + description: 'Parlour', + icon: Assets.parlour, + value: 'parlour', + ), + CpsOperationalValue( + description: 'Bathroom', + icon: Assets.bathroom, + value: 'bathroom', + ), + CpsOperationalValue( + description: 'Bedroom', + icon: Assets.bedroom, + value: 'bedroom', + ), + CpsOperationalValue( + description: 'DIY', + icon: Assets.dyi, + value: 'dyi', + ), + ]; + } +} + +class CpsPresenceStatusFunctions extends CpsFunctions { + CpsPresenceStatusFunctions({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'presence_state', + operationName: 'Presence Status', + icon: Assets.presenceSensor, + ); + + @override + List getOperationalValues() { + return [ + CpsOperationalValue( + icon: Assets.nobodyTime, + description: 'None', + value: 'none', + ), + CpsOperationalValue( + icon: Assets.presenceState, + description: 'Presence', + value: 'presence', + ), + CpsOperationalValue( + icon: Assets.motion, + description: 'Motion', + value: 'motion', + ), + ]; + } +} + +final class CpsSportsParaFunction extends CpsFunctions { + CpsSportsParaFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : min = 1, + max = 100, + step = 1, + super( + code: 'sports_para', + operationName: 'Sports Para', + icon: Assets.sportsPara, + ); + + final double min; + final double max; + final double step; + + @override + List getOperationalValues() { + final count = ((max - min) / step).round() + 1; + return List.generate( + count, + (index) { + final value = (min + (index * step)); + return CpsOperationalValue( + icon: Assets.motionMeter, + description: value.toStringAsFixed(0), + value: value, + ); + }, + ); + } +} 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/dialog_footer.dart b/lib/pages/routines/widgets/dialog_footer.dart index 15db9732..e5a548f7 100644 --- a/lib/pages/routines/widgets/dialog_footer.dart +++ b/lib/pages/routines/widgets/dialog_footer.dart @@ -8,12 +8,12 @@ class DialogFooter extends StatelessWidget { final int? dialogWidth; const DialogFooter({ - Key? key, + super.key, required this.onCancel, required this.onConfirm, required this.isConfirmEnabled, this.dialogWidth, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -28,21 +28,19 @@ class DialogFooter extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Expanded( - child: _buildFooterButton( - context, - 'Cancel', - onCancel, - ), + _buildFooterButton( + context: context, + text: 'Cancel', + onTap: onCancel, ), if (isConfirmEnabled) ...[ Container(width: 1, height: 50, color: ColorsManager.greyColor), - Expanded( - child: _buildFooterButton( - context, - 'Confirm', - onConfirm, - ), + _buildFooterButton( + context: context, + text: 'Confirm', + onTap: onConfirm, + textColor: + isConfirmEnabled ? ColorsManager.primaryColorWithOpacity : Colors.red, ), ], ], @@ -50,24 +48,24 @@ class DialogFooter extends StatelessWidget { ); } - Widget _buildFooterButton( - BuildContext context, - String text, - VoidCallback? onTap, - ) { - return GestureDetector( - onTap: onTap, - child: SizedBox( - height: 50, - child: Center( - child: Text( - text, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: text == 'Confirm' - ? ColorsManager.primaryColorWithOpacity - : ColorsManager.textGray, - ), - ), + Widget _buildFooterButton({ + required BuildContext context, + required String text, + required VoidCallback? onTap, + Color? textColor, + }) { + return Expanded( + child: TextButton( + style: TextButton.styleFrom( + foregroundColor: ColorsManager.primaryColorWithOpacity, + disabledForegroundColor: ColorsManager.primaryColor, + ), + onPressed: onTap, + child: Text( + text, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: textColor ?? ColorsManager.textGray, + ), ), ), ); diff --git a/lib/pages/routines/widgets/function_slider.dart b/lib/pages/routines/widgets/function_slider.dart new file mode 100644 index 00000000..50167a7b --- /dev/null +++ b/lib/pages/routines/widgets/function_slider.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; + +class FunctionSlider extends StatelessWidget { + final dynamic initialValue; + final (double min, double max) range; + final void Function(double value) onChanged; + final double dividendOfRange; + + const FunctionSlider({ + required this.onChanged, + required this.initialValue, + required this.range, + required this.dividendOfRange, + super.key, + }); + + @override + Widget build(BuildContext context) { + final (min, max) = range; + final bool isValidRange = max > min; + final double value = initialValue is int + ? (initialValue as int).toDouble() + : (initialValue as double); + + final int? divisions = isValidRange ? ((max - min) / dividendOfRange).round() : null; + + return Slider( + value: value.clamp(min, max), + min: min, + max: max, + divisions: divisions, + onChanged: isValidRange ? onChanged : null, + ); + } +} diff --git a/lib/pages/routines/widgets/if_container.dart b/lib/pages/routines/widgets/if_container.dart index 007e4dc5..f7a4ddc1 100644 --- a/lib/pages/routines/widgets/if_container.dart +++ b/lib/pages/routines/widgets/if_container.dart @@ -70,8 +70,9 @@ class IfContainer extends StatelessWidget { '1G', '2G', '3G', - 'WPS' - 'GW', + 'WPS', + 'GW', + 'CPS', ].contains(state.ifItems[index]['productType'])) { context .read() @@ -121,7 +122,7 @@ class IfContainer extends StatelessWidget { if (result != null) { context.read().add(AddToIfContainer(mutableData, false)); - } else if (!['AC', '1G', '2G', '3G', 'WPS', 'GW'] + } else if (!['AC', '1G', '2G', '3G', 'WPS', 'GW', 'CPS'] .contains(mutableData['productType'])) { context.read().add(AddToIfContainer(mutableData, false)); } diff --git a/lib/pages/routines/widgets/routine_devices.dart b/lib/pages/routines/widgets/routine_devices.dart index f22c8ae3..3294a73a 100644 --- a/lib/pages/routines/widgets/routine_devices.dart +++ b/lib/pages/routines/widgets/routine_devices.dart @@ -17,7 +17,7 @@ class _RoutineDevicesState extends State { context.read().add(FetchDevicesInRoutine()); } - static const _allowedProductTypes = {'AC', '1G', '2G', '3G', 'WPS', 'GW'}; + static const _allowedProductTypes = {'AC', '1G', '2G', '3G', 'WPS', 'GW', 'CPS'}; @override Widget build(BuildContext context) { diff --git a/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/ceiling_sensor_dialog.dart b/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/ceiling_sensor_dialog.dart new file mode 100644 index 00000000..c18706f0 --- /dev/null +++ b/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/ceiling_sensor_dialog.dart @@ -0,0 +1,219 @@ +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/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routines/models/ceiling_presence_sensor_functions.dart'; +import 'package:syncrow_web/pages/routines/models/device_functions.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/ceiling_sensor/ceiling_sensor_helper.dart'; +import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_dialog_slider_selector.dart'; +import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_dialog_value_selector.dart'; +import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_functions_list.dart'; +import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_slider_helpers.dart'; + +class CeilingSensorDialog extends StatefulWidget { + const CeilingSensorDialog({ + required this.uniqueCustomId, + required this.functions, + required this.deviceSelectedFunctions, + required this.device, + required this.dialogType, + super.key, + }); + + final String? uniqueCustomId; + final List functions; + final List deviceSelectedFunctions; + final AllDevicesModel? device; + final String dialogType; + + @override + State createState() => _CeilingSensorDialogState(); +} + +class _CeilingSensorDialogState extends State { + late final List _cpsFunctions; + late final String _dialogHeaderText; + + @override + void initState() { + super.initState(); + + _cpsFunctions = widget.functions.whereType().where((function) { + if (widget.dialogType == 'THEN') { + return function.type == 'THEN' || function.type == 'BOTH'; + } + return function.type == 'IF' || function.type == 'BOTH'; + }).toList(); + + final isIfDialog = widget.dialogType == 'IF'; + _dialogHeaderText = isIfDialog ? 'Conditions' : 'Functions'; + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: BlocBuilder( + builder: (context, state) { + final selectedFunction = state.selectedFunction; + + 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: [ + DialogHeader('Presence Sensor $_dialogHeaderText'), + Expanded(child: _buildMainContent(context, state)), + DialogFooter( + onCancel: () => Navigator.pop(context), + onConfirm: state.addedFunctions.isNotEmpty + ? () { + final functions = _updateValuesForAddedFunctions( + state.addedFunctions, + ); + context.read().add( + AddFunctionToRoutine( + functions, + '${widget.uniqueCustomId}', + ), + ); + + Navigator.pop(context, { + 'deviceId': widget.functions.first.deviceId, + }); + } + : null, + isConfirmEnabled: selectedFunction != null, + ), + ], + ), + ); + }, + ), + ); + } + + Widget _buildMainContent(BuildContext context, FunctionBlocState state) { + final selectedFunction = state.selectedFunction; + final selectedOperationName = state.selectedOperationName; + final selectedFunctionData = state.addedFunctions.firstWhere( + (f) => f.functionCode == selectedFunction, + orElse: () => DeviceFunctionData( + entityId: '', + functionCode: selectedFunction ?? '', + operationName: '', + value: null, + ), + ); + final selectedCpsFunctions = _cpsFunctions.firstWhere( + (f) => f.code == selectedFunction, + orElse: () => CpsMovementFunctions( + deviceId: '', + deviceName: '', + type: '', + ), + ); + final operations = selectedCpsFunctions.getOperationalValues(); + final isSensitivityFunction = selectedFunction == 'sensitivity'; + final isToggleFunction = isSensitivityFunction + ? widget.dialogType == 'THEN' + : CeilingSensorHelper.toggleCodes.contains(selectedFunction); + + return Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + CpsFunctionsList(cpsFunctions: _cpsFunctions), + if (state.selectedFunction != null) + Expanded( + child: isToggleFunction + ? CpsDialogValueSelector( + operations: operations, + selectedFunction: selectedFunction ?? '', + selectedFunctionData: selectedFunctionData, + cpsFunctions: _cpsFunctions, + operationName: selectedOperationName ?? '', + device: widget.device, + ) + : CpsDialogSliderSelector( + operations: operations, + selectedFunction: selectedFunction ?? '', + selectedFunctionData: selectedFunctionData, + cpsFunctions: _cpsFunctions, + operationName: selectedOperationName ?? '', + device: widget.device, + dialogType: widget.dialogType, + ), + ), + ], + ); + } + + static const _mappableSteppedFunctions = { + 'static_max_dis', + 'presence_reference', + 'moving_reference', + 'perceptual_boundary', + 'moving_boundary', + 'moving_rigger_time', + 'moving_static_time', + 'none_body_time', + 'moving_max_dis', + 'moving_range', + 'presence_range', + }; + + List _updateValuesForAddedFunctions( + List addedFunctions, + ) { + return addedFunctions.map((function) { + final shouldMapValue = _mappableSteppedFunctions.contains( + function.functionCode, + ); + if (shouldMapValue) { + final mappedValue = _mapSteppedValue( + value: function.value, + inputStep: CpsSliderHelpers.dividendOfRange(function.functionCode), + inputRange: CpsSliderHelpers.sliderRange(function.functionCode), + outputRange: CpsSliderHelpers.mappedRange(function.functionCode), + ); + return DeviceFunctionData( + value: mappedValue, + entityId: function.entityId, + functionCode: function.functionCode, + operationName: function.operationName, + condition: function.condition, + actionExecutor: function.actionExecutor, + valueDescription: function.valueDescription, + ); + } + return function; + }).toList(); + } + + int _mapSteppedValue({ + required (double min, double max) inputRange, + required double inputStep, + required (double min, double max, double dividend) outputRange, + required double value, + }) { + final (inputMin, inputMax) = inputRange; + final (outputMin, outputMax, outputStep) = outputRange; + + final clampedValue = value.clamp(inputMin, inputMax); + + final stepsFromMin = ((clampedValue - inputMin) / inputStep).round(); + + final mappedValue = outputMin + (stepsFromMin * outputStep); + + return mappedValue.clamp(outputMin, outputMax).round(); + } +} diff --git a/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/ceiling_sensor_helper.dart b/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/ceiling_sensor_helper.dart new file mode 100644 index 00000000..940daf23 --- /dev/null +++ b/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/ceiling_sensor_helper.dart @@ -0,0 +1,176 @@ +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/ceiling_presence_sensor_functions.dart'; +import 'package:syncrow_web/pages/routines/models/device_functions.dart'; +import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ceiling_sensor/ceiling_sensor_dialog.dart'; + +abstract final class CeilingSensorHelper { + const CeilingSensorHelper._(); + + static Future?> showCeilingSensorDialog({ + required BuildContext context, + required String? uniqueCustomId, + required List functions, + required List deviceSelectedFunctions, + required AllDevicesModel? device, + required String dialogType, + }) { + return showDialog( + context: context, + builder: (context) => BlocProvider( + create: (context) => FunctionBloc() + ..add( + InitializeFunctions(deviceSelectedFunctions), + ), + child: CeilingSensorDialog( + uniqueCustomId: uniqueCustomId, + functions: functions, + deviceSelectedFunctions: deviceSelectedFunctions, + device: device, + dialogType: dialogType, + ), + ), + ); + } + + static List> getCeilingSensorFunctions({ + required String uuid, + required String name, + }) { + return [ + CpsRadarSwitchFunction( + deviceName: name, + deviceId: uuid, + type: 'BOTH', + ), + CpsSpatialParameterSwitchFunction( + deviceName: name, + deviceId: uuid, + type: 'BOTH', + ), + CpsSensitivityFunction( + deviceName: name, + deviceId: uuid, + type: 'BOTH', + ), + CpsMovingSpeedFunction( + deviceName: name, + deviceId: uuid, + type: 'IF', + ), + CpsSpatialStaticValueFunction( + deviceName: name, + deviceId: uuid, + type: 'IF', + ), + CpsSpatialMotionValueFunction( + deviceName: name, + deviceId: uuid, + type: 'IF', + ), + CpsMaxDistanceOfDetectionFunction( + deviceName: name, + deviceId: uuid, + type: 'BOTH', + ), + CpsMaxDistanceOfStaticDetectionFunction( + deviceName: name, + deviceId: uuid, + type: 'BOTH', + ), + CpsDetectionRangeFunction( + deviceName: name, + deviceId: uuid, + type: 'IF', + ), + CpsDistanceOfMovingObjectsFunction( + deviceName: name, + deviceId: uuid, + type: 'IF', + ), + CpsPresenceJudgementThrsholdFunction( + deviceName: name, + deviceId: uuid, + type: 'BOTH', + ), + CpsMotionAmplitudeTriggerThresholdFunction( + deviceName: name, + deviceId: uuid, + type: 'BOTH', + ), + CpsPerpetualBoundaryFunction( + deviceName: name, + deviceId: uuid, + type: 'BOTH', + ), + CpsMotionTriggerBoundaryFunction( + deviceName: name, + deviceId: uuid, + type: 'BOTH', + ), + CpsMotionTriggerTimeFunction( + deviceName: name, + deviceId: uuid, + type: 'BOTH', + ), + CpsMotionToStaticTimeFunction( + deviceName: name, + deviceId: uuid, + type: 'BOTH', + ), + CpsEnteringNoBodyStateTimeFunction( + deviceName: name, + deviceId: uuid, + type: 'BOTH', + ), + CpsSelfTestResultFunctions( + deviceName: name, + deviceId: uuid, + type: 'IF', + ), + CpsNobodyTimeFunction( + deviceName: name, + deviceId: uuid, + type: 'BOTH', + ), + CpsMovementFunctions( + deviceName: name, + deviceId: uuid, + type: 'IF', + ), + CpsCustomModeFunction( + deviceName: name, + deviceId: uuid, + type: 'BOTH', + ), + CpsSpaceTypeFunctions( + deviceName: name, + deviceId: uuid, + type: 'BOTH', + ), + CpsPresenceStatusFunctions( + deviceName: name, + deviceId: uuid, + type: 'IF', + ), + CpsSportsParaFunction( + deviceName: name, + deviceId: uuid, + type: 'IF', + ), + ]; + } + + static const toggleCodes = { + 'radar_switch', + 'space_para_switch', + 'checking_result', + 'nobody_time', + 'body_movement', + 'custom_mode', + 'scene', + 'presence_state', + }; +} diff --git a/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_dialog_slider_selector.dart b/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_dialog_slider_selector.dart new file mode 100644 index 00000000..a2d11f79 --- /dev/null +++ b/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_dialog_slider_selector.dart @@ -0,0 +1,69 @@ +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/ceiling_presence_sensor_functions.dart'; +import 'package:syncrow_web/pages/routines/models/device_functions.dart'; +import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_slider_helpers.dart'; +import 'package:syncrow_web/pages/routines/widgets/slider_value_selector.dart'; + +class CpsDialogSliderSelector extends StatelessWidget { + const CpsDialogSliderSelector({ + required this.operations, + required this.selectedFunction, + required this.selectedFunctionData, + required this.cpsFunctions, + required this.device, + required this.operationName, + required this.dialogType, + super.key, + }); + + final List operations; + final String selectedFunction; + final DeviceFunctionData selectedFunctionData; + final List cpsFunctions; + final AllDevicesModel? device; + final String operationName; + final String dialogType; + + @override + Widget build(BuildContext context) { + return SliderValueSelector( + currentCondition: selectedFunctionData.condition, + dialogType: dialogType, + sliderRange: CpsSliderHelpers.sliderRange(selectedFunctionData.functionCode), + displayedValue: CpsSliderHelpers.displayText( + value: selectedFunctionData.value, + functionCode: selectedFunctionData.functionCode, + ), + initialValue: selectedFunctionData.value ?? 0, + unit: CpsSliderHelpers.unit(selectedFunctionData.functionCode), + onConditionChanged: (condition) => context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectedFunction, + operationName: operationName, + condition: condition, + value: selectedFunctionData.value, + ), + ), + ), + onSliderChanged: (value) => context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectedFunction, + operationName: operationName, + value: double.parse(value.toStringAsFixed(2)), + condition: selectedFunctionData.condition, + ), + ), + ), + dividendOfRange: CpsSliderHelpers.dividendOfRange( + selectedFunctionData.functionCode, + ), + ); + } +} diff --git a/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_dialog_value_selector.dart b/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_dialog_value_selector.dart new file mode 100644 index 00000000..504017a2 --- /dev/null +++ b/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_dialog_value_selector.dart @@ -0,0 +1,58 @@ +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/ceiling_presence_sensor_functions.dart'; +import 'package:syncrow_web/pages/routines/models/device_functions.dart'; +import 'package:syncrow_web/pages/routines/widgets/routine_dialog_selection_list_tile.dart'; + +class CpsDialogValueSelector extends StatelessWidget { + const CpsDialogValueSelector({ + required this.operations, + required this.selectedFunction, + required this.selectedFunctionData, + required this.cpsFunctions, + required this.device, + required this.operationName, + super.key, + }); + + final List operations; + final String selectedFunction; + final DeviceFunctionData? selectedFunctionData; + final List cpsFunctions; + final AllDevicesModel? device; + final String operationName; + + @override + Widget build(BuildContext context) { + return ListView.builder( + itemCount: operations.length, + itemBuilder: (context, index) { + final operation = operations[index]; + final isSelected = selectedFunctionData?.value == operation.value; + return RoutineDialogSelectionListTile( + iconPath: operation.icon, + description: operation.description, + isSelected: isSelected, + onTap: () { + if (!isSelected) { + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectedFunction, + operationName: operationName, + value: operation.value, + condition: selectedFunctionData?.condition, + valueDescription: selectedFunctionData?.valueDescription, + ), + ), + ); + } + }, + ); + }, + ); + } +} diff --git a/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_functions_list.dart b/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_functions_list.dart new file mode 100644 index 00000000..2a35428a --- /dev/null +++ b/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_functions_list.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart'; +import 'package:syncrow_web/pages/routines/models/ceiling_presence_sensor_functions.dart'; +import 'package:syncrow_web/pages/routines/widgets/routine_dialog_function_list_tile.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CpsFunctionsList extends StatelessWidget { + const CpsFunctionsList({required this.cpsFunctions, super.key}); + + final List cpsFunctions; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 360, + child: ListView.separated( + shrinkWrap: false, + itemCount: cpsFunctions.length, + separatorBuilder: (context, index) => const Padding( + padding: EdgeInsets.symmetric(horizontal: 40.0), + child: Divider(color: ColorsManager.dividerColor), + ), + itemBuilder: (context, index) { + final function = cpsFunctions[index]; + return RoutineDialogFunctionListTile( + iconPath: function.icon, + operationName: function.operationName, + onTap: () => context.read().add( + SelectFunction( + functionCode: function.code, + operationName: function.operationName, + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_slider_helpers.dart b/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_slider_helpers.dart new file mode 100644 index 00000000..fd637c28 --- /dev/null +++ b/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_slider_helpers.dart @@ -0,0 +1,90 @@ +abstract final class CpsSliderHelpers { + static (double min, double max, double step) mappedRange(String functionCode) { + final (defaultMin, defaultMax) = sliderRange(functionCode); + final defaultDivdidend = dividendOfRange(functionCode); + return switch (functionCode) { + 'static_max_dis' => (0, 500, 50), + 'presence_reference' => (0, 255, 5), + 'moving_reference' => (0, 255, 5), + 'perceptual_boundary' => (0, 500, 50), + 'moving_boundary' => (0, 500, 50), + 'moving_rigger_time' => (0, 2000, 100), + 'moving_static_time' => (0, 60000, 1000), + 'none_body_time' => (0, 300000, 5000), + 'moving_max_dis' => (0, 500, 50), + 'moving_range' => (0, 255, 5), + 'presence_range' => (0, 255, 5), + _ => (defaultMin, defaultMax, defaultDivdidend), + }; + } + + static (double min, double max) sliderRange(String functionCode) => + switch (functionCode) { + 'moving_speed' => (0, 32), + 'sensitivity' => (0, 10), + 'space_static_val' => (0, 255), + 'space_move_val' => (0, 255), + 'moving_max_dis' => (0, 10), + 'static_max_dis' => (0, 10), + 'moving_range' => (0, 25.5), + 'presence_range' => (0, 25.5), + 'presence_judgement_threshold' => (0, 255), + 'motion_amplitude_trigger_threshold' => (0, 255), + 'perceptual_boundary' => (0, 5), + 'moving_boundary' => (0, 5), + 'moving_rigger_time' => (0, 2), + 'moving_static_time' => (0, 50), + 'none_body_time' => (0, 300), + 'sports_para' => (0, 100), + _ => (0, 300), + }; + + static double dividendOfRange(String functionCode) => switch (functionCode) { + 'presence_reference' => 5, + 'moving_reference' => 5, + 'moving_max_dis' => 0.5, + 'static_max_dis' => 0.5, + 'moving_range' => 0.1, + 'presence_range' => 0.1, + 'perceptual_boundary' => 0.5, + 'moving_boundary' => 0.5, + 'moving_rigger_time' => 0.1, + 'moving_static_time' => 1.0, + 'none_body_time' => 5.0, + _ => 1, + }; + + static String unit(String functionCode) => switch (functionCode) { + 'moving_max_dis' || + 'static_max_dis' || + 'moving_range' || + 'presence_range' || + 'perceptual_boundary' || + 'moving_boundary' => + 'M', + 'moving_rigger_time' || 'moving_static_time' || 'none_body_time' => 'sec', + _ => '', + }; + + static String displayText({ + required dynamic value, + required String functionCode, + }) { + final parsedValue = double.tryParse('$value'); + + return switch (functionCode) { + 'moving_max_dis' || + 'static_max_dis' || + 'moving_range' || + 'presence_range' || + 'perceptual_boundary' || + 'moving_boundary' => + parsedValue?.toStringAsFixed(1) ?? '0', + 'moving_rigger_time' => parsedValue?.toStringAsFixed(2) ?? '0', + 'moving_static_time' || + 'none_body_time' => + parsedValue?.toStringAsFixed(2) ?? '0', + _ => '${parsedValue?.toStringAsFixed(0) ?? 0}', + }; + } +} 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..fcdc991a --- /dev/null +++ b/lib/pages/routines/widgets/routine_dialogs/wall_sensor/wps_value_selector_widget.dart @@ -0,0 +1,102 @@ +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( + currentCondition: functionData.condition, + 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, + ), + ), + ), + unit: _unit, + dividendOfRange: 1, + ); + } + + 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'}', + 'dis_current' => '${intValue ?? '250'}', + 'illuminance_value' => '${intValue ?? '0'}', + _ => '$intValue', + }; + } + + String get _unit => switch (functionData.functionCode) { + 'presence_time' => 'Min', + 'dis_current' => 'CM', + 'illuminance_value' => 'Lux', + _ => '', + }; +} 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..526e6fa0 --- /dev/null +++ b/lib/pages/routines/widgets/slider_value_selector.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.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? currentCondition; + 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; + final String unit; + final double dividendOfRange; + + const SliderValueSelector({ + required this.dialogType, + required this.sliderRange, + required this.displayedValue, + required this.initialValue, + required this.onConditionChanged, + required this.onSliderChanged, + required this.currentCondition, + required this.unit, + required this.dividendOfRange, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Column( + spacing: 16, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (dialogType == 'IF') + ConditionToggle( + currentCondition: currentCondition, + onChanged: onConditionChanged, + ), + ValueDisplay( + value: initialValue, + label: displayedValue, + unit: unit, + ), + FunctionSlider( + initialValue: initialValue, + range: sliderRange, + onChanged: onSliderChanged, + dividendOfRange: dividendOfRange, + ), + ], + ); + } +} + +class RangeInputFormatter extends TextInputFormatter { + const RangeInputFormatter({required this.min, required this.max}); + + final double min; + final double max; + + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + final text = newValue.text; + if (text.isEmpty) { + return newValue; + } + + final value = double.tryParse(text); + if (value == null || value < min || value > max) { + return oldValue; + } + + return newValue; + } +} diff --git a/lib/pages/routines/widgets/then_container.dart b/lib/pages/routines/widgets/then_container.dart index e0828721..3266d3b4 100644 --- a/lib/pages/routines/widgets/then_container.dart +++ b/lib/pages/routines/widgets/then_container.dart @@ -3,10 +3,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart'; -import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/automation_dialog.dart'; -import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/delay_dialog.dart'; import 'package:syncrow_web/pages/routines/helper/dialog_helper/device_dialog_helper.dart'; import 'package:syncrow_web/pages/routines/widgets/dragable_card.dart'; +import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/automation_dialog.dart'; +import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/delay_dialog.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:uuid/uuid.dart'; @@ -27,8 +27,7 @@ class ThenContainer extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('THEN', - style: TextStyle( - fontSize: 18, fontWeight: FontWeight.bold)), + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 16), state.isLoading && state.isUpdate == true ? const Center( @@ -41,12 +40,11 @@ class ThenContainer extends StatelessWidget { state.thenItems.length, (index) => GestureDetector( onTap: () async { - if (state.thenItems[index] - ['deviceId'] == + if (state.thenItems[index]['deviceId'] == 'delay') { final result = await DelayHelper - .showDelayPickerDialog(context, - state.thenItems[index]); + .showDelayPickerDialog( + context, state.thenItems[index]); if (result != null) { context @@ -66,17 +64,14 @@ class ThenContainer extends StatelessWidget { context: context, builder: (BuildContext context) => AutomationDialog( - automationName: - state.thenItems[index] - ['name'] ?? - 'Automation', - automationId: - state.thenItems[index] - ['deviceId'] ?? - '', - uniqueCustomId: - state.thenItems[index] - ['uniqueCustomId'], + automationName: state.thenItems[index] + ['name'] ?? + 'Automation', + automationId: state.thenItems[index] + ['deviceId'] ?? + '', + uniqueCustomId: state.thenItems[index] + ['uniqueCustomId'], ), ); @@ -85,13 +80,11 @@ class ThenContainer extends StatelessWidget { .read() .add(AddToThenContainer({ ...state.thenItems[index], - 'imagePath': - Assets.automation, - 'title': + 'imagePath': Assets.automation, + 'title': state.thenItems[index] + ['name'] ?? state.thenItems[index] - ['name'] ?? - state.thenItems[index] - ['title'], + ['title'], })); } return; @@ -114,9 +107,10 @@ class ThenContainer extends StatelessWidget { '2G', '3G', 'WPS', + 'CPS', "GW", - ].contains(state.thenItems[index] - ['productType'])) { + ].contains( + state.thenItems[index]['productType'])) { context.read().add( AddToThenContainer( state.thenItems[index])); @@ -126,9 +120,7 @@ class ThenContainer extends StatelessWidget { imagePath: state.thenItems[index] ['imagePath'] ?? '', - title: state.thenItems[index] - ['title'] ?? - '', + title: state.thenItems[index]['title'] ?? '', deviceData: state.thenItems[index], padding: const EdgeInsets.symmetric( horizontal: 4, vertical: 8), @@ -165,8 +157,8 @@ class ThenContainer extends StatelessWidget { } if (mutableData['type'] == 'automation') { - int index = state.thenItems.indexWhere( - (item) => item['deviceId'] == mutableData['deviceId']); + int index = state.thenItems + .indexWhere((item) => item['deviceId'] == mutableData['deviceId']); if (index != -1) { return; } @@ -191,8 +183,8 @@ class ThenContainer extends StatelessWidget { } if (mutableData['type'] == 'tap_to_run' && state.isAutomation) { - int index = state.thenItems.indexWhere( - (item) => item['deviceId'] == mutableData['deviceId']); + int index = state.thenItems + .indexWhere((item) => item['deviceId'] == mutableData['deviceId']); if (index != -1) { return; } @@ -230,7 +222,7 @@ class ThenContainer extends StatelessWidget { dialogType: "THEN"); if (result != null) { context.read().add(AddToThenContainer(mutableData)); - } else if (!['AC', '1G', '2G', '3G', 'WPS', 'GW'] + } else if (!['AC', '1G', '2G', '3G', 'WPS', 'GW', 'CPS'] .contains(mutableData['productType'])) { context.read().add(AddToThenContainer(mutableData)); } diff --git a/lib/pages/routines/widgets/value_display.dart b/lib/pages/routines/widgets/value_display.dart new file mode 100644 index 00000000..6a8bd949 --- /dev/null +++ b/lib/pages/routines/widgets/value_display.dart @@ -0,0 +1,33 @@ +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; + final String unit; + + const ValueDisplay({ + required this.value, + required this.label, + super.key, + required this.unit, + }); + + @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 $unit ', + style: context.textTheme.headlineMedium!.copyWith( + color: ColorsManager.primaryColorWithOpacity, + ), + ), + ); + } +} diff --git a/lib/pages/spaces_management/all_spaces/widgets/loaded_space_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/loaded_space_widget.dart index 66b2d6da..5ef9c79b 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/loaded_space_widget.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/loaded_space_widget.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; import 'package:syncrow_web/pages/space_tree/view/space_tree_view.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart'; @@ -107,6 +109,11 @@ class _LoadedSpaceViewState extends State { selectedSpaceUuid: widget.selectedSpace?.uuid ?? widget.selectedCommunity?.uuid ?? '', + onCreateCommunity: (name, description) { + context.read().add( + CreateCommunityEvent(name, description, context), + ); + }, ), CommunityStructureArea( selectedCommunity: widget.selectedCommunity, diff --git a/lib/pages/spaces_management/all_spaces/widgets/sidebar_add_community_button.dart b/lib/pages/spaces_management/all_spaces/widgets/sidebar_add_community_button.dart index 5c769d48..ae1eb2bf 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/sidebar_add_community_button.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/sidebar_add_community_button.dart @@ -1,19 +1,15 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart'; -import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart'; -import 'package:syncrow_web/pages/spaces_management/create_community/view/create_community_dialog.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; class SidebarAddCommunityButton extends StatelessWidget { const SidebarAddCommunityButton({ - required this.existingCommunityNames, + required this.onTap, super.key, }); - final List existingCommunityNames; + final void Function() onTap; @override Widget build(BuildContext context) { @@ -30,22 +26,9 @@ class SidebarAddCommunityButton extends StatelessWidget { ), ), ), - onPressed: () => _showCreateCommunityDialog(context), + onPressed: onTap, icon: SvgPicture.asset(Assets.addIcon), ), ); } - - void _showCreateCommunityDialog(BuildContext context) => showDialog( - context: context, - builder: (context) => CreateCommunityDialog( - isEditMode: false, - existingCommunityNames: existingCommunityNames, - onCreateCommunity: (name, description) { - context.read().add( - CreateCommunityEvent(name, description, context), - ); - }, - ), - ); } diff --git a/lib/pages/spaces_management/all_spaces/widgets/sidebar_header.dart b/lib/pages/spaces_management/all_spaces/widgets/sidebar_header.dart index 135be109..1706d51a 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/sidebar_header.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/sidebar_header.dart @@ -5,9 +5,12 @@ import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/style.dart'; class SidebarHeader extends StatelessWidget { - const SidebarHeader({required this.existingCommunityNames, super.key}); + const SidebarHeader({ + required this.onAddCommunity, + super.key, + }); - final List existingCommunityNames; + final void Function() onAddCommunity; @override Widget build(BuildContext context) { @@ -23,7 +26,9 @@ class SidebarHeader extends StatelessWidget { color: ColorsManager.blackColor, ), ), - SidebarAddCommunityButton(existingCommunityNames: existingCommunityNames), + SidebarAddCommunityButton( + onTap: onAddCommunity, + ), ], ), ); diff --git a/lib/pages/spaces_management/all_spaces/widgets/sidebar_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/sidebar_widget.dart index 35bb8ad2..3eb1c001 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/sidebar_widget.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/sidebar_widget.dart @@ -8,6 +8,7 @@ import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/community_tile.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/sidebar_header.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/space_tile_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/create_community/view/create_community_dialog.dart'; import 'package:syncrow_web/pages/spaces_management/structure_selector/bloc/center_body_bloc.dart'; import 'package:syncrow_web/pages/spaces_management/structure_selector/bloc/center_body_event.dart'; import 'package:syncrow_web/utils/style.dart'; @@ -15,9 +16,11 @@ import 'package:syncrow_web/utils/style.dart'; class SidebarWidget extends StatefulWidget { final List communities; final String? selectedSpaceUuid; + final void Function(String name, String description) onCreateCommunity; const SidebarWidget({ required this.communities, + required this.onCreateCommunity, this.selectedSpaceUuid, super.key, }); @@ -94,10 +97,7 @@ class _SidebarWidgetState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - SidebarHeader( - existingCommunityNames: - widget.communities.map((community) => community.name).toList(), - ), + SidebarHeader(onAddCommunity: _onAddCommunity), CustomSearchBar( onSearchChanged: (query) => setState(() => _searchQuery = query), ), @@ -179,4 +179,26 @@ class _SidebarWidgetState extends State { ), ); } + + void _onAddCommunity() => _selectedId?.isNotEmpty ?? true + ? _clearSelection() + : _showCreateCommunityDialog(); + + void _clearSelection() { + setState(() => _selectedId = ''); + context.read().add( + NewCommunityEvent(communities: widget.communities), + ); + } + + void _showCreateCommunityDialog() { + showDialog( + context: context, + builder: (context) => CreateCommunityDialog( + isEditMode: false, + existingCommunityNames: widget.communities.map((e) => e.name).toList(), + onCreateCommunity: widget.onCreateCommunity, + ), + ); + } } diff --git a/lib/pages/spaces_management/create_subspace_model/views/create_subspace_model_dialog.dart b/lib/pages/spaces_management/create_subspace_model/views/create_subspace_model_dialog.dart index 7a39891b..66acdf3d 100644 --- a/lib/pages/spaces_management/create_subspace_model/views/create_subspace_model_dialog.dart +++ b/lib/pages/spaces_management/create_subspace_model/views/create_subspace_model_dialog.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; -import 'package:syncrow_web/pages/common/buttons/default_button.dart'; import 'package:syncrow_web/pages/spaces_management/create_subspace_model/bloc/subspace_model_bloc.dart'; import 'package:syncrow_web/pages/spaces_management/create_subspace_model/bloc/subspace_model_event.dart'; import 'package:syncrow_web/pages/spaces_management/create_subspace_model/bloc/subspace_model_state.dart'; +import 'package:syncrow_web/pages/spaces_management/create_subspace_model/widgets/create_subspace_model_chips_box.dart'; +import 'package:syncrow_web/pages/spaces_management/create_subspace_model/widgets/create_subspace_model_footer_buttons.dart'; import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart'; import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; class CreateSubSpaceModelDialog extends StatelessWidget { final bool isEdit; @@ -14,211 +15,67 @@ class CreateSubSpaceModelDialog extends StatelessWidget { final List? existingSubSpaces; final void Function(List newSubspaces)? onUpdate; - const CreateSubSpaceModelDialog( - {Key? key, - required this.isEdit, - required this.dialogTitle, - this.existingSubSpaces, - this.onUpdate}) - : super(key: key); + const CreateSubSpaceModelDialog({ + required this.isEdit, + required this.dialogTitle, + this.existingSubSpaces, + this.onUpdate, + super.key, + }); @override Widget build(BuildContext context) { final screenWidth = MediaQuery.of(context).size.width; - final textController = TextEditingController(); return Dialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), child: BlocProvider( - create: (_) { + create: (context) { final bloc = SubSpaceModelBloc(); if (existingSubSpaces != null) { - for (var subSpace in existingSubSpaces!) { + for (final subSpace in existingSubSpaces ?? []) { bloc.add(AddSubSpaceModel(subSpace)); } } return bloc; }, child: BlocBuilder( - builder: (context, state) { - return Container( - color: ColorsManager.whiteColors, - child: SizedBox( - width: screenWidth * 0.3, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - dialogTitle, - style: Theme.of(context) - .textTheme - .headlineLarge - ?.copyWith(color: ColorsManager.blackColor), - ), - const SizedBox(height: 16), - Container( - width: screenWidth * 0.35, - padding: const EdgeInsets.symmetric( - vertical: 10.0, horizontal: 16.0), - decoration: BoxDecoration( - color: ColorsManager.boxColor, - borderRadius: BorderRadius.circular(10), - ), - child: Wrap( - spacing: 8.0, - runSpacing: 8.0, - children: [ - ...state.subSpaces.asMap().entries.map( - (entry) { - final index = entry.key; - final subSpace = entry.value; - - final lowerName = - subSpace.subspaceName.toLowerCase(); - - final duplicateIndices = state.subSpaces - .asMap() - .entries - .where((e) => - e.value.subspaceName.toLowerCase() == - lowerName) - .map((e) => e.key) - .toList(); - final isDuplicate = - duplicateIndices.length > 1 && - duplicateIndices.indexOf(index) != 0; - - return Chip( - label: Text(subSpace.subspaceName, - style: Theme.of(context) - .textTheme - .bodySmall - ?.copyWith( - color: ColorsManager.spaceColor, - )), - backgroundColor: ColorsManager.whiteColors, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - side: BorderSide( - color: isDuplicate - ? ColorsManager.red - : ColorsManager.transparentColor, - width: 0, - ), - ), - deleteIcon: Container( - width: 24, - height: 24, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: ColorsManager.lightGrayColor, - width: 1.5, - ), - ), - child: const Icon( - Icons.close, - size: 16, - color: ColorsManager.lightGrayColor, - ), - ), - onDeleted: () => context - .read() - .add(RemoveSubSpaceModel(subSpace)), - ); - }, - ), - SizedBox( - width: 200, - child: TextField( - controller: textController, - decoration: InputDecoration( - border: InputBorder.none, - hintText: state.subSpaces.isEmpty - ? 'Please enter the name' - : null, - hintStyle: Theme.of(context) - .textTheme - .bodySmall! - .copyWith( - color: ColorsManager - .lightGrayColor)), - onSubmitted: (value) { - if (value.trim().isNotEmpty) { - context.read().add( - AddSubSpaceModel( - SubspaceTemplateModel( - subspaceName: value.trim(), - disabled: false))); - textController.clear(); - } - }, - style: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith( - color: ColorsManager.blackColor)), - ), - ], - ), - ), - if (state.errorMessage.isNotEmpty) - Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: Text(state.errorMessage, - style: Theme.of(context) - .textTheme - .bodySmall - ?.copyWith( - color: ColorsManager.red, - )), - ), - const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: CancelButton( - label: 'Cancel', - onPressed: () async { - Navigator.of(context).pop(); - }, - ), - ), - const SizedBox(width: 10), - Expanded( - child: DefaultButton( - onPressed: (state.errorMessage.isNotEmpty) - ? null - : () async { - final subSpaces = context - .read() - .state - .subSpaces; - Navigator.of(context).pop(); - if (onUpdate != null) { - onUpdate!(subSpaces); - } - }, - backgroundColor: ColorsManager.secondaryColor, - borderRadius: 10, - foregroundColor: state.errorMessage.isNotEmpty - ? ColorsManager.whiteColorsWithOpacity - : ColorsManager.whiteColors, - child: const Text('OK'), - ), - ), - ], - ), - ], + builder: (context, state) => Container( + color: ColorsManager.whiteColors, + width: screenWidth * 0.3, + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + dialogTitle, + style: context.textTheme.headlineLarge?.copyWith( + color: ColorsManager.blackColor, + ), + ), + const SizedBox(height: 16), + CreateSubspaceModelChipsBox(subSpaces: state.subSpaces), + if (state.errorMessage.isNotEmpty) + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text( + state.errorMessage, + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.red, + ), ), ), - )); - }, + const SizedBox(height: 16), + CreateSubspaceModelFooterButtons( + onUpdate: onUpdate, + errorMessage: state.errorMessage, + ), + ], + ), + ), ), ), ); diff --git a/lib/pages/spaces_management/create_subspace_model/widgets/create_subspace_model_chips_box.dart b/lib/pages/spaces_management/create_subspace_model/widgets/create_subspace_model_chips_box.dart new file mode 100644 index 00000000..a18fc8d8 --- /dev/null +++ b/lib/pages/spaces_management/create_subspace_model/widgets/create_subspace_model_chips_box.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/spaces_management/create_subspace_model/widgets/subspace_chip.dart'; +import 'package:syncrow_web/pages/spaces_management/create_subspace_model/widgets/subspaces_textfield.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CreateSubspaceModelChipsBox extends StatelessWidget { + const CreateSubspaceModelChipsBox({ + required this.subSpaces, + super.key, + }); + + final List subSpaces; + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + + return Container( + width: screenWidth * 0.35, + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 16, + ), + decoration: BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.circular(10), + ), + child: Wrap( + spacing: 8, + runSpacing: 8, + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + ...subSpaces.asMap().entries.map( + (entry) { + final index = entry.key; + final subSpace = entry.value; + + final lowerName = subSpace.subspaceName.toLowerCase(); + + final duplicateIndices = subSpaces + .asMap() + .entries + .where((e) => e.value.subspaceName.toLowerCase() == lowerName) + .map((e) => e.key) + .toList(); + final isDuplicate = duplicateIndices.length > 1 && + duplicateIndices.indexOf(index) != 0; + + return SubspaceChip( + subSpace: subSpace, + isDuplicate: isDuplicate, + ); + }, + ), + SubspacesTextfield( + hintText: subSpaces.isEmpty ? 'Please enter the name' : null, + ), + ], + ), + ); + } +} diff --git a/lib/pages/spaces_management/create_subspace_model/widgets/create_subspace_model_footer_buttons.dart b/lib/pages/spaces_management/create_subspace_model/widgets/create_subspace_model_footer_buttons.dart new file mode 100644 index 00000000..a8dcf89c --- /dev/null +++ b/lib/pages/spaces_management/create_subspace_model/widgets/create_subspace_model_footer_buttons.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/spaces_management/create_subspace_model/bloc/subspace_model_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CreateSubspaceModelFooterButtons extends StatelessWidget { + const CreateSubspaceModelFooterButtons({ + required this.onUpdate, + required this.errorMessage, + super.key, + }); + + final void Function(List newSubspaces)? onUpdate; + final String errorMessage; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: CancelButton( + label: 'Cancel', + onPressed: () => Navigator.of(context).pop(), + ), + ), + const SizedBox(width: 10), + Expanded( + child: DefaultButton( + onPressed: errorMessage.isEmpty + ? () { + Navigator.of(context).pop(); + if (onUpdate != null) { + final subSpaces = + context.read().state.subSpaces; + onUpdate!(subSpaces); + } + } + : null, + backgroundColor: ColorsManager.secondaryColor, + borderRadius: 10, + foregroundColor: errorMessage.isNotEmpty + ? ColorsManager.whiteColorsWithOpacity + : ColorsManager.whiteColors, + child: const Text('OK'), + ), + ), + ], + ); + } +} diff --git a/lib/pages/spaces_management/create_subspace_model/widgets/subspace_chip.dart b/lib/pages/spaces_management/create_subspace_model/widgets/subspace_chip.dart new file mode 100644 index 00000000..b54e0712 --- /dev/null +++ b/lib/pages/spaces_management/create_subspace_model/widgets/subspace_chip.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/create_subspace_model/bloc/subspace_model_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/create_subspace_model/bloc/subspace_model_event.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SubspaceChip extends StatelessWidget { + const SubspaceChip({ + required this.subSpace, + required this.isDuplicate, + super.key, + }); + + final SubspaceTemplateModel subSpace; + final bool isDuplicate; + + @override + Widget build(BuildContext context) { + return Chip( + label: Text( + subSpace.subspaceName, + style: context.textTheme.bodySmall?.copyWith( + color: isDuplicate ? ColorsManager.red : ColorsManager.spaceColor, + ), + ), + backgroundColor: ColorsManager.whiteColors, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + side: BorderSide( + color: isDuplicate ? ColorsManager.red : ColorsManager.transparentColor, + width: 0, + ), + ), + deleteIcon: Container( + padding: const EdgeInsetsDirectional.all(1), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: ColorsManager.lightGrayColor, + width: 1.5, + ), + ), + child: const FittedBox( + fit: BoxFit.scaleDown, + child: Icon( + Icons.close, + color: ColorsManager.lightGrayColor, + ), + ), + ), + onDeleted: () => context.read().add( + RemoveSubSpaceModel(subSpace), + ), + ); + } +} diff --git a/lib/pages/spaces_management/create_subspace_model/widgets/subspaces_textfield.dart b/lib/pages/spaces_management/create_subspace_model/widgets/subspaces_textfield.dart new file mode 100644 index 00000000..d654b960 --- /dev/null +++ b/lib/pages/spaces_management/create_subspace_model/widgets/subspaces_textfield.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/create_subspace_model/bloc/subspace_model_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/create_subspace_model/bloc/subspace_model_event.dart'; +import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SubspacesTextfield extends StatefulWidget { + const SubspacesTextfield({ + required this.hintText, + super.key, + }); + + final String? hintText; + + @override + State createState() => _SubspacesTextfieldState(); +} + +class _SubspacesTextfieldState extends State { + late final TextEditingController _controller; + @override + void initState() { + _controller = TextEditingController(); + super.initState(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 100, + child: TextField( + controller: _controller, + decoration: InputDecoration( + border: InputBorder.none, + hintText: widget.hintText, + hintStyle: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.lightGrayColor, + ), + ), + onSubmitted: (value) { + final trimmedValue = value.trim(); + if (trimmedValue.isNotEmpty) { + context.read().add( + AddSubSpaceModel( + SubspaceTemplateModel( + subspaceName: trimmedValue, + disabled: false, + ), + ), + ); + _controller.clear(); + } + }, + style: context.textTheme.bodyMedium?.copyWith( + color: ColorsManager.blackColor, + ), + ), + ); + } +} diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index 12d699b4..dd933789 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -16,7 +16,8 @@ class Assets { static const String invisiblePassword = "assets/images/Password_invisible.svg"; static const String visiblePassword = "assets/images/password_visible.svg"; static const String accessIcon = "assets/images/access_icon.svg"; - static const String spaseManagementIcon = "assets/images/spase_management_icon.svg"; + static const String spaseManagementIcon = + "assets/images/spase_management_icon.svg"; static const String devicesIcon = "assets/images/devices_icon.svg"; static const String moveinIcon = "assets/images/movein_icon.svg"; static const String constructionIcon = "assets/images/construction_icon.svg"; @@ -35,7 +36,8 @@ class Assets { static const String acFanMiddle = "assets/icons/ac_fan_middle.svg"; static const String switchAlarmSound = "assets/icons/switch_alarm_sound.svg"; static const String resetOff = "assets/icons/reset_off.svg"; - static const String sensitivityOperationIcon = "assets/icons/sesitivity_operation_icon.svg"; + static const String sensitivityOperationIcon = + "assets/icons/sesitivity_operation_icon.svg"; static const String motionDetection = "assets/icons/motion_detection.svg"; static const String freezing = "assets/icons/freezing.svg"; static const String indicator = "assets/icons/indicator.svg"; @@ -56,7 +58,8 @@ class Assets { static const String celsiusDegrees = "assets/icons/celsius_degrees.svg"; static const String masterState = "assets/icons/master_state.svg"; static const String acPower = "assets/icons/ac_power.svg"; - static const String farDetectionFunction = "assets/icons/far_detection_function.svg"; + static const String farDetectionFunction = + "assets/icons/far_detection_function.svg"; static const String nobodyTime = "assets/icons/nobody_time.svg"; // Automation functions @@ -67,19 +70,26 @@ class Assets { static const String doorbell = "assets/icons/automation_functions/doorbell.svg"; static const String remoteUnlockViaApp = "assets/icons/automation_functions/remote_unlock_via_app.svg"; - static const String doubleLock = "assets/icons/automation_functions/double_lock.svg"; - static const String selfTestResult = "assets/icons/automation_functions/self_test_result.svg"; + static const String doubleLock = + "assets/icons/automation_functions/double_lock.svg"; + static const String selfTestResult = + "assets/icons/automation_functions/self_test_result.svg"; static const String lockAlarm = "assets/icons/automation_functions/lock_alarm.svg"; - static const String presenceState = "assets/icons/automation_functions/presence_state.svg"; - static const String currentTemp = "assets/icons/automation_functions/current_temp.svg"; + static const String presenceState = + "assets/icons/automation_functions/presence_state.svg"; + static const String currentTemp = + "assets/icons/automation_functions/current_temp.svg"; static const String presence = "assets/icons/automation_functions/presence.svg"; static const String residualElectricity = "assets/icons/automation_functions/residual_electricity.svg"; - static const String hijackAlarm = "assets/icons/automation_functions/hijack_alarm.svg"; - static const String passwordUnlock = "assets/icons/automation_functions/password_unlock.svg"; + static const String hijackAlarm = + "assets/icons/automation_functions/hijack_alarm.svg"; + static const String passwordUnlock = + "assets/icons/automation_functions/password_unlock.svg"; static const String remoteUnlockRequest = "assets/icons/automation_functions/remote_unlock_req.svg"; - static const String cardUnlock = "assets/icons/automation_functions/card_unlock.svg"; + static const String cardUnlock = + "assets/icons/automation_functions/card_unlock.svg"; static const String motion = "assets/icons/automation_functions/motion.svg"; static const String fingerprintUnlock = "assets/icons/automation_functions/fingerprint_unlock.svg"; @@ -88,7 +98,8 @@ class Assets { static const String sensorMotionIcon = "assets/icons/sensor_motion_ic.svg"; static const String sensorPresenceIcon = "assets/icons/sensor_presence_ic.svg"; static const String sensorVacantIcon = "assets/icons/sensor_vacant_ic.svg"; - static const String illuminanceRecordIcon = "assets/icons/illuminance_record_ic.svg"; + static const String illuminanceRecordIcon = + "assets/icons/illuminance_record_ic.svg"; static const String presenceRecordIcon = "assets/icons/presence_record_ic.svg"; static const String helpDescriptionIcon = "assets/icons/help_description_ic.svg"; @@ -258,37 +269,55 @@ class Assets { static const String delay = 'assets/icons/routine/delay.svg'; // Assets for functions_icons - static const String assetsSensitivityFunction = "assets/icons/functions_icons/sensitivity.svg"; + static const String assetsSensitivityFunction = + "assets/icons/functions_icons/sensitivity.svg"; static const String assetsSensitivityOperationIcon = "assets/icons/functions_icons/sesitivity_operation_icon.svg"; static const String assetsAcPower = "assets/icons/functions_icons/ac_power.svg"; - static const String assetsAcPowerOFF = "assets/icons/functions_icons/ac_power_off.svg"; - static const String assetsChildLock = "assets/icons/functions_icons/child_lock.svg"; + static const String assetsAcPowerOFF = + "assets/icons/functions_icons/ac_power_off.svg"; + static const String assetsChildLock = + "assets/icons/functions_icons/child_lock.svg"; static const String assetsFreezing = "assets/icons/functions_icons/freezing.svg"; static const String assetsFanSpeed = "assets/icons/functions_icons/fan_speed.svg"; - static const String assetsAcCooling = "assets/icons/functions_icons/ac_cooling.svg"; - static const String assetsAcHeating = "assets/icons/functions_icons/ac_heating.svg"; - static const String assetsCelsiusDegrees = "assets/icons/functions_icons/celsius_degrees.svg"; - static const String assetsTempreture = "assets/icons/functions_icons/tempreture.svg"; + static const String assetsAcCooling = + "assets/icons/functions_icons/ac_cooling.svg"; + static const String assetsAcHeating = + "assets/icons/functions_icons/ac_heating.svg"; + static const String assetsCelsiusDegrees = + "assets/icons/functions_icons/celsius_degrees.svg"; + static const String assetsTempreture = + "assets/icons/functions_icons/tempreture.svg"; static const String assetsAcFanLow = "assets/icons/functions_icons/ac_fan_low.svg"; - static const String assetsAcFanMiddle = "assets/icons/functions_icons/ac_fan_middle.svg"; - static const String assetsAcFanHigh = "assets/icons/functions_icons/ac_fan_high.svg"; - static const String assetsAcFanAuto = "assets/icons/functions_icons/ac_fan_auto.svg"; - static const String assetsSceneChildLock = "assets/icons/functions_icons/scene_child_lock.svg"; + static const String assetsAcFanMiddle = + "assets/icons/functions_icons/ac_fan_middle.svg"; + static const String assetsAcFanHigh = + "assets/icons/functions_icons/ac_fan_high.svg"; + static const String assetsAcFanAuto = + "assets/icons/functions_icons/ac_fan_auto.svg"; + static const String assetsSceneChildLock = + "assets/icons/functions_icons/scene_child_lock.svg"; static const String assetsSceneChildUnlock = "assets/icons/functions_icons/scene_child_unlock.svg"; - static const String assetsSceneRefresh = "assets/icons/functions_icons/scene_refresh.svg"; - static const String assetsLightCountdown = "assets/icons/functions_icons/light_countdown.svg"; - static const String assetsFarDetection = "assets/icons/functions_icons/far_detection.svg"; + static const String assetsSceneRefresh = + "assets/icons/functions_icons/scene_refresh.svg"; + static const String assetsLightCountdown = + "assets/icons/functions_icons/light_countdown.svg"; + static const String assetsFarDetection = + "assets/icons/functions_icons/far_detection.svg"; static const String assetsFarDetectionFunction = "assets/icons/functions_icons/far_detection_function.svg"; static const String assetsIndicator = "assets/icons/functions_icons/indicator.svg"; - static const String assetsMotionDetection = "assets/icons/functions_icons/motion_detection.svg"; + static const String assetsMotionDetection = + "assets/icons/functions_icons/motion_detection.svg"; static const String assetsMotionlessDetection = "assets/icons/functions_icons/motionless_detection.svg"; - static const String assetsNobodyTime = "assets/icons/functions_icons/nobody_time.svg"; - static const String assetsFactoryReset = "assets/icons/functions_icons/factory_reset.svg"; - static const String assetsMasterState = "assets/icons/functions_icons/master_state.svg"; + static const String assetsNobodyTime = + "assets/icons/functions_icons/nobody_time.svg"; + static const String assetsFactoryReset = + "assets/icons/functions_icons/factory_reset.svg"; + static const String assetsMasterState = + "assets/icons/functions_icons/master_state.svg"; static const String assetsSwitchAlarmSound = "assets/icons/functions_icons/switch_alarm_sound.svg"; static const String assetsResetOff = "assets/icons/functions_icons/reset_off.svg"; @@ -322,7 +351,8 @@ class Assets { "assets/icons/functions_icons/automation_functions/self_test_result.svg"; static const String assetsPresence = "assets/icons/functions_icons/automation_functions/presence.svg"; - static const String assetsMotion = "assets/icons/functions_icons/automation_functions/motion.svg"; + static const String assetsMotion = + "assets/icons/functions_icons/automation_functions/motion.svg"; static const String assetsCurrentTemp = "assets/icons/functions_icons/automation_functions/current_temp.svg"; static const String assetsPresenceState = @@ -337,9 +367,11 @@ class Assets { static const String rectangleCheckBox = 'assets/icons/rectangle_check_box.png'; static const String CheckBoxChecked = 'assets/icons/box_checked.png'; static const String emptyBox = 'assets/icons/empty_box.png'; - static const String completeProcessIcon = 'assets/icons/compleate_process_icon.svg'; + static const String completeProcessIcon = + 'assets/icons/compleate_process_icon.svg'; static const String currentProcessIcon = 'assets/icons/current_process_icon.svg'; - static const String uncomplete_ProcessIcon = 'assets/icons/uncompleate_process_icon.svg'; + static const String uncomplete_ProcessIcon = + 'assets/icons/uncompleate_process_icon.svg'; static const String wrongProcessIcon = 'assets/icons/wrong_process_icon.svg'; static const String arrowForward = 'assets/icons/arrow_forward.svg'; static const String arrowDown = 'assets/icons/arrow_down.svg'; @@ -352,7 +384,8 @@ class Assets { static const String duplicate = 'assets/icons/duplicate.svg'; static const String spaceDelete = 'assets/icons/space_delete.svg'; - static const String deleteSpaceLinkIcon = 'assets/icons/delete_space_link_icon.svg'; + static const String deleteSpaceLinkIcon = + 'assets/icons/delete_space_link_icon.svg'; static const String spaceLinkIcon = 'assets/icons/space_link_icon.svg'; static const String successIcon = 'assets/icons/success_icon.svg'; static const String spaceLocationIcon = 'assets/icons/spaseLocationIcon.svg'; @@ -375,5 +408,35 @@ class Assets { static const String IlluminanceIcon = 'assets/icons/Illuminance_icon.svg'; static const String gear = 'assets/icons/gear.svg'; static const String activeBell = 'assets/icons/active_bell.svg'; + static const String cpsCustomMode = 'assets/icons/cps_custom_mode.svg'; + static const String cpsMode1 = 'assets/icons/cps_mode1.svg'; + static const String cpsMode2 = 'assets/icons/cps_mode2.svg'; + static const String cpsMode3 = 'assets/icons/cps_mode3.svg'; + static const String cpsMode4 = 'assets/icons/cps_mode4.svg'; + static const String closeToMotion = 'assets/icons/close_to_motion.svg'; + static const String farAwayMotion = 'assets/icons/far_away_motion.svg'; + static const String communicationFault = 'assets/icons/communication_fault.svg'; + static const String radarFault = 'assets/icons/radar_fault.svg'; + static const String selfTestingSuccess = 'assets/icons/self_testing_success.svg'; + static const String selfTestingFailure = 'assets/icons/self_testing_failure.svg'; + static const String selfTestingTimeout = 'assets/icons/self_testing_timeout.svg'; + static const String movingSpeed = 'assets/icons/moving_speed.svg'; + static const String boundary = 'assets/icons/boundary.svg'; + static const String motionMeter = 'assets/icons/motion_meter.svg'; + static const String spatialStaticValue = 'assets/icons/spatial_static_value.svg'; + static const String spatialMotionValue = 'assets/icons/spatial_motion_value.svg'; + static const String presenceJudgementThrshold = + 'assets/icons/presence_judgement_threshold.svg'; + static const String spaceType = 'assets/icons/space_type.svg'; + static const String sportsPara = 'assets/icons/sports_para.svg'; + static const String sensitivityFeature1 = 'assets/icons/sensitivity_feature_1.svg'; + static const String sensitivityFeature2 = 'assets/icons/sensitivity_feature_2.svg'; + static const String sensitivityFeature3 = 'assets/icons/sensitivity_feature_3.svg'; + static const String sensitivityFeature4 = 'assets/icons/sensitivity_feature_4.svg'; + static const String sensitivityFeature5 = 'assets/icons/sensitivity_feature_5.svg'; + static const String sensitivityFeature6 = 'assets/icons/sensitivity_feature_6.svg'; + static const String sensitivityFeature7 = 'assets/icons/sensitivity_feature_7.svg'; + static const String sensitivityFeature8 = 'assets/icons/sensitivity_feature_8.svg'; + static const String sensitivityFeature9 = 'assets/icons/sensitivity_feature_9.svg'; static const String deviceTagIcon = 'assets/icons/device_tag_ic.svg'; }