Merge pull request #168 from SyncrowIOT/flush-presence-sensor-routines

fix real time garage door and add flush sensor to routines
This commit is contained in:
mohammadnemer1
2025-04-29 10:25:17 +03:00
committed by GitHub
21 changed files with 1435 additions and 140 deletions

View File

@ -4,6 +4,7 @@ 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/flush_presence_sensor/flush_presence_sensor.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';
@ -116,6 +117,15 @@ class DeviceDialogHelper {
deviceSelectedFunctions: deviceSelectedFunctions,
device: data['device'],
);
case 'NCPS':
return FlushPresenceSensor.showFlushFunctionsDialog(
context: context,
functions: functions,
uniqueCustomId: data['uniqueCustomId'],
deviceSelectedFunctions: deviceSelectedFunctions,
dialogType: dialogType,
device: data['device'],
);
default:
return null;

View File

@ -0,0 +1,407 @@
import 'package:syncrow_web/pages/device_managment/flush_mounted_presence_sensor/models/flush_mounted_presence_sensor_model.dart';
import 'package:syncrow_web/pages/routines/models/device_functions.dart';
import 'package:syncrow_web/pages/routines/models/flush/flush_operational_value.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
abstract class FlushFunctions
extends DeviceFunction<FlushMountedPresenceSensorModel> {
final String type;
FlushFunctions({
required super.deviceId,
required super.deviceName,
required super.code,
required super.operationName,
required super.icon,
required this.type,
});
List<FlushOperationalValue> getOperationalValues();
}
class FlushPresenceDelayFunction extends FlushFunctions {
final int min;
FlushPresenceDelayFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : min = 0,
super(
code: FlushMountedPresenceSensorModel.codePresenceState,
operationName: 'Presence State',
icon: Assets.presenceStateIcon,
);
@override
List<FlushOperationalValue> getOperationalValues() {
return [
FlushOperationalValue(
icon: Assets.nobodyTime,
description: 'None',
value: "none",
),
FlushOperationalValue(
icon: Assets.presenceStateIcon,
description: 'Presence',
value: 'presence',
),
];
}
}
class FlushSensiReduceFunction extends FlushFunctions {
final int min;
final int max;
final int step;
FlushSensiReduceFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : min = 1,
max = 5,
step = 1,
super(
code: FlushMountedPresenceSensorModel.codeSensiReduce,
operationName: 'Sensitivity Reduction',
icon: Assets.motionlessDetectionSensitivityIcon,
);
@override
List<FlushOperationalValue> getOperationalValues() {
return List.generate(
(max - min) ~/ step + 1,
(index) => FlushOperationalValue(
icon: Assets.currentDistanceIcon,
description: '${min + (index * step)}',
value: min + (index * step),
));
}
}
class FlushNoneDelayFunction extends FlushFunctions {
final int min;
final int max;
final String unit;
FlushNoneDelayFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : min = 10,
max = 10000,
unit = '',
super(
code: FlushMountedPresenceSensorModel.codeNoneDelay,
operationName: 'None Delay',
icon: Assets.nobodyTime,
);
@override
List<FlushOperationalValue> getOperationalValues() {
return [
FlushOperationalValue(
icon: icon,
description: 'Custom $unit',
value: null,
)
];
}
}
class FlushIlluminanceFunction extends FlushFunctions {
final int min;
final int max;
final int step;
FlushIlluminanceFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : min = 0,
max = 2000,
step = 0,
super(
code: FlushMountedPresenceSensorModel.codeIlluminance,
operationName: 'Illuminance',
icon: Assets.IlluminanceIcon,
);
@override
List<FlushOperationalValue> getOperationalValues() {
List<FlushOperationalValue> values = [];
for (int lux = min; lux <= max; lux += step) {
values.add(FlushOperationalValue(
icon: Assets.IlluminanceIcon,
description: "$lux Lux",
value: lux,
));
}
return values;
}
}
class FlushOccurDistReduceFunction extends FlushFunctions {
final int min;
final int max;
final int step;
FlushOccurDistReduceFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : min = 0,
max = 100,
step = 1,
super(
code: FlushMountedPresenceSensorModel.codeOccurDistReduce,
operationName: 'Occurrence Distance Reduction',
icon: Assets.assetsTempreture,
);
@override
List<FlushOperationalValue> getOperationalValues() {
return List.generate(
(max - min) ~/ step + 1,
(index) => FlushOperationalValue(
icon: Assets.assetsTempreture,
description: '${min + (index * step)}',
value: min + (index * step),
));
}
}
// ==== then functions ====
class FlushSensitivityFunction extends FlushFunctions {
final int min;
final int max;
final int step;
FlushSensitivityFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : min = 1,
max = 9,
step = 1,
super(
code: FlushMountedPresenceSensorModel.codeSensitivity,
operationName: 'Sensitivity',
icon: Assets.sensitivity,
);
@override
List<FlushOperationalValue> getOperationalValues() {
return List.generate(
(max - min) ~/ step + 1,
(index) => FlushOperationalValue(
icon: Assets.motionDetectionSensitivityValueIcon,
description: '${min + (index * step)}',
value: min + (index * step),
));
}
}
class FlushNearDetectionFunction extends FlushFunctions {
final int min;
final double max;
final int step;
final String unit;
FlushNearDetectionFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : min = 0,
max = 9.5,
step = 1,
unit = 'm',
super(
code: FlushMountedPresenceSensorModel.codeNearDetection,
operationName: 'Nearest Detect Dist',
icon: Assets.currentDistanceIcon,
);
@override
List<FlushOperationalValue> getOperationalValues() {
final values = <FlushOperationalValue>[];
for (var value = min; value <= max; value += step) {
values.add(FlushOperationalValue(
icon: Assets.nobodyTime,
description: '$value $unit',
value: value * 10,
));
}
return values;
}
}
class FlushMaxDetectDistFunction extends FlushFunctions {
final int min;
final int max;
final int step;
final String unit;
FlushMaxDetectDistFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : min = 75,
max = 600,
step = 75,
unit = 'cm',
super(
code: FlushMountedPresenceSensorModel.codeFarDetection,
operationName: 'Max Detect Dist',
icon: Assets.currentDistanceIcon,
);
@override
List<FlushOperationalValue> getOperationalValues() {
final values = <FlushOperationalValue>[];
for (var value = min; value <= max; value += step) {
values.add(FlushOperationalValue(
icon: Assets.nobodyTime,
description: '$value $unit',
value: value,
));
}
return values;
}
}
class FlushTargetConfirmTimeFunction extends FlushFunctions {
final int min;
final int max;
final int step;
final String unit;
FlushTargetConfirmTimeFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : min = 75,
max = 600,
step = 75,
unit = 'cm',
super(
code: FlushMountedPresenceSensorModel.codePresenceDelay,
operationName: 'Target Confirm Time',
icon: Assets.targetConfirmTimeIcon,
);
@override
List<FlushOperationalValue> getOperationalValues() {
final values = <FlushOperationalValue>[];
for (var value = min; value <= max; value += step) {
values.add(FlushOperationalValue(
icon: Assets.nobodyTime,
description: '$value $unit',
value: value,
));
}
return values;
}
}
class FlushDisappeDelayFunction extends FlushFunctions {
final int min;
final int max;
final int step;
final String unit;
FlushDisappeDelayFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : min = 75,
max = 600,
step = 75,
unit = 'cm',
super(
code: FlushMountedPresenceSensorModel.codeNoneDelay,
operationName: 'Disappe Delay',
icon: Assets.DisappeDelayIcon,
);
@override
List<FlushOperationalValue> getOperationalValues() {
final values = <FlushOperationalValue>[];
for (var value = min; value <= max; value += step) {
values.add(FlushOperationalValue(
icon: Assets.nobodyTime,
description: '$value $unit',
value: value,
));
}
return values;
}
}
class FlushIndentLevelFunction extends FlushFunctions {
final int min;
final int max;
final int step;
final String unit;
FlushIndentLevelFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : min = 75,
max = 600,
step = 75,
unit = 'cm',
super(
code: FlushMountedPresenceSensorModel.codeOccurDistReduce,
operationName: 'Indent Level',
icon: Assets.indentLevelIcon,
);
@override
List<FlushOperationalValue> getOperationalValues() {
final values = <FlushOperationalValue>[];
for (var value = min; value <= max; value += step) {
values.add(FlushOperationalValue(
icon: Assets.nobodyTime,
description: '$value $unit',
value: value,
));
}
return values;
}
}
class FlushTriggerLevelFunction extends FlushFunctions {
final int min;
final int max;
final int step;
final String unit;
FlushTriggerLevelFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : min = 75,
max = 600,
step = 75,
unit = 'cm',
super(
code: FlushMountedPresenceSensorModel.codeSensiReduce,
operationName: 'Trigger Level',
icon: Assets.triggerLevelIcon,
);
@override
List<FlushOperationalValue> getOperationalValues() {
final values = <FlushOperationalValue>[];
for (var value = min; value <= max; value += step) {
values.add(FlushOperationalValue(
icon: Assets.nobodyTime,
description: '$value $unit',
value: value,
));
}
return values;
}
}

View File

@ -0,0 +1,11 @@
class FlushOperationalValue {
final String icon;
final String description;
final dynamic value;
FlushOperationalValue({
required this.icon,
required this.description,
required this.value,
});
}

View File

@ -28,10 +28,12 @@ class IfContainer extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('IF',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
style: TextStyle(
fontSize: 18, fontWeight: FontWeight.bold)),
if (state.isAutomation && state.ifItems.isNotEmpty)
AutomationOperatorSelector(
selectedOperator: state.selectedAutomationOperator),
selectedOperator:
state.selectedAutomationOperator),
],
),
const SizedBox(height: 16),
@ -55,16 +57,17 @@ class IfContainer extends StatelessWidget {
(index) => GestureDetector(
onTap: () async {
if (!state.isTabToRun) {
final result = await DeviceDialogHelper.showDeviceDialog(
context: context,
data: state.ifItems[index],
removeComparetors: false,
dialogType: "IF");
final result = await DeviceDialogHelper
.showDeviceDialog(
context: context,
data: state.ifItems[index],
removeComparetors: false,
dialogType: "IF");
if (result != null) {
context
.read<RoutineBloc>()
.add(AddToIfContainer(state.ifItems[index], false));
context.read<RoutineBloc>().add(
AddToIfContainer(
state.ifItems[index], false));
} else if (![
'AC',
'1G',
@ -73,25 +76,32 @@ class IfContainer extends StatelessWidget {
'WPS',
'GW',
'CPS',
].contains(state.ifItems[index]['productType'])) {
context
.read<RoutineBloc>()
.add(AddToIfContainer(state.ifItems[index], false));
'NCPS'
].contains(state.ifItems[index]
['productType'])) {
context.read<RoutineBloc>().add(
AddToIfContainer(
state.ifItems[index], false));
}
}
},
child: DraggableCard(
imagePath: state.ifItems[index]['imagePath'] ?? '',
imagePath:
state.ifItems[index]['imagePath'] ?? '',
title: state.ifItems[index]['title'] ?? '',
deviceData: state.ifItems[index],
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
padding: const EdgeInsets.symmetric(
horizontal: 4, vertical: 8),
isFromThen: false,
isFromIf: true,
onRemove: () {
context.read<RoutineBloc>().add(RemoveDragCard(
index: index,
isFromThen: false,
key: state.ifItems[index]['uniqueCustomId']));
context.read<RoutineBloc>().add(
RemoveDragCard(
index: index,
isFromThen: false,
key: state.ifItems[index]
['uniqueCustomId']));
},
),
)),
@ -112,7 +122,9 @@ class IfContainer extends StatelessWidget {
if (!state.isTabToRun) {
if (mutableData['deviceId'] == 'tab_to_run') {
context.read<RoutineBloc>().add(AddToIfContainer(mutableData, true));
context
.read<RoutineBloc>()
.add(AddToIfContainer(mutableData, true));
} else {
final result = await DeviceDialogHelper.showDeviceDialog(
dialogType: 'IF',
@ -121,10 +133,14 @@ class IfContainer extends StatelessWidget {
removeComparetors: false);
if (result != null) {
context.read<RoutineBloc>().add(AddToIfContainer(mutableData, false));
} else if (!['AC', '1G', '2G', '3G', 'WPS', 'GW', 'CPS']
context
.read<RoutineBloc>()
.add(AddToIfContainer(mutableData, false));
} else if (!['AC', '1G', '2G', '3G', 'WPS', 'GW', 'CPS', 'NCPS']
.contains(mutableData['productType'])) {
context.read<RoutineBloc>().add(AddToIfContainer(mutableData, false));
context
.read<RoutineBloc>()
.add(AddToIfContainer(mutableData, false));
}
}
}
@ -170,7 +186,9 @@ class AutomationOperatorSelector extends StatelessWidget {
),
),
onPressed: () {
context.read<RoutineBloc>().add(const ChangeAutomationOperator(operator: 'or'));
context
.read<RoutineBloc>()
.add(const ChangeAutomationOperator(operator: 'or'));
},
),
Container(
@ -196,7 +214,9 @@ class AutomationOperatorSelector extends StatelessWidget {
),
),
onPressed: () {
context.read<RoutineBloc>().add(const ChangeAutomationOperator(operator: 'and'));
context
.read<RoutineBloc>()
.add(const ChangeAutomationOperator(operator: 'and'));
},
),
],

View File

@ -17,7 +17,16 @@ class _RoutineDevicesState extends State<RoutineDevices> {
context.read<RoutineBloc>().add(FetchDevicesInRoutine());
}
static const _allowedProductTypes = {'AC', '1G', '2G', '3G', 'WPS', 'GW', 'CPS'};
static const _allowedProductTypes = {
'AC',
'1G',
'2G',
'3G',
'WPS',
'GW',
'CPS',
'NCPS'
};
@override
Widget build(BuildContext context) {
@ -34,7 +43,8 @@ class _RoutineDevicesState extends State<RoutineDevices> {
});
final deviceList = state.devices
.where((device) => _allowedProductTypes.contains(device.productType))
.where(
(device) => _allowedProductTypes.contains(device.productType))
.toList();
return Wrap(
@ -51,12 +61,16 @@ class _RoutineDevicesState extends State<RoutineDevices> {
'productType': device.productType,
'functions': device.functions,
'uniqueCustomId': '',
'tag': device.deviceTags!.isNotEmpty ? device.deviceTags![0].name : '',
'tag': device.deviceTags!.isNotEmpty
? device.deviceTags![0].name
: '',
'subSpace': device.deviceSubSpace?.subspaceName ?? '',
};
if (state.searchText != null && state.searchText!.isNotEmpty) {
return device.name!.toLowerCase().contains(state.searchText!.toLowerCase())
return device.name!
.toLowerCase()
.contains(state.searchText!.toLowerCase())
? DraggableCard(
imagePath: deviceData['imagePath'] as String,
title: deviceData['title'] as String,

View File

@ -0,0 +1,70 @@
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/flush/flush_operational_value.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/wall_sensor/time_wheel.dart';
class FlushOperationalValuesList extends StatelessWidget {
final List<FlushOperationalValue> values;
final dynamic selectedValue;
final AllDevicesModel? device;
final String operationName;
final String selectCode;
final ValueChanged<FlushOperationalValue> onSelect;
const FlushOperationalValuesList({
required this.values,
required this.selectedValue,
required this.device,
required this.operationName,
required this.selectCode,
required this.onSelect,
super.key,
});
@override
Widget build(BuildContext context) {
return ListView.builder(
padding: const EdgeInsets.all(20),
itemCount: values.length,
itemBuilder: (context, index) =>
_buildValueItem(context, values[index]),
);
}
Widget _buildValueItem(BuildContext context, FlushOperationalValue value) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: _buildValueDescription(value)),
_buildValueRadio(context, value),
],
),
);
}
Widget _buildValueDescription(FlushOperationalValue value) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(value.description),
);
}
Widget _buildValueRadio(context, FlushOperationalValue value) {
return Radio<dynamic>(
value: value.value,
groupValue: selectedValue,
onChanged: (_) => onSelect(value));
}
}

View File

@ -0,0 +1,208 @@
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/flush/flush_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/flush_presence_sensor/flush_value_selector_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class FlushPresenceSensor extends StatefulWidget {
final List<DeviceFunction> functions;
final AllDevicesModel? device;
final List<DeviceFunctionData>? deviceSelectedFunctions;
final String? uniqueCustomId;
final String dialogType;
final bool removeComparetors;
const FlushPresenceSensor({
super.key,
required this.functions,
this.device,
this.deviceSelectedFunctions,
this.uniqueCustomId,
required this.dialogType,
this.removeComparetors = false,
});
static Future<Map<String, dynamic>?> showFlushFunctionsDialog({
required BuildContext context,
required List<DeviceFunction> functions,
AllDevicesModel? device,
List<DeviceFunctionData>? deviceSelectedFunctions,
String? uniqueCustomId,
required String dialogType,
bool removeComparetors = false,
}) async {
return showDialog<Map<String, dynamic>?>(
context: context,
builder: (context) => FlushPresenceSensor(
functions: functions,
device: device,
deviceSelectedFunctions: deviceSelectedFunctions,
uniqueCustomId: uniqueCustomId,
removeComparetors: removeComparetors,
dialogType: dialogType,
),
);
}
@override
State<FlushPresenceSensor> createState() => _WallPresenceSensorState();
}
class _WallPresenceSensorState extends State<FlushPresenceSensor> {
late final List<FlushFunctions> _flushFunctions;
@override
void initState() {
super.initState();
_flushFunctions =
widget.functions.whereType<FlushFunctions>().where((function) {
if (widget.dialogType == 'THEN') {
return function.type == 'THEN' || function.type == 'BOTH';
}
return function.type == 'IF' || function.type == 'BOTH';
}).toList();
}
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => FunctionBloc()
..add(InitializeFunctions(widget.deviceSelectedFunctions ?? [])),
child: _buildDialogContent(),
);
}
Widget _buildDialogContent() {
return AlertDialog(
contentPadding: EdgeInsets.zero,
content: BlocBuilder<FunctionBloc, FunctionBlocState>(
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: [
const DialogHeader('Presence Sensor Condition'),
Expanded(child: _buildMainContent(context, state)),
_buildDialogFooter(context, state),
],
),
);
},
),
);
}
Widget _buildMainContent(BuildContext context, FunctionBlocState state) {
return Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildFunctionList(context),
if (state.selectedFunction != null) _buildValueSelector(context, state),
],
);
}
Widget _buildFunctionList(BuildContext context) {
return SizedBox(
width: 360,
child: ListView.separated(
shrinkWrap: false,
physics: const AlwaysScrollableScrollPhysics(),
itemCount: _flushFunctions.length,
separatorBuilder: (context, index) => const Padding(
padding: EdgeInsets.symmetric(horizontal: 40.0),
child: Divider(color: ColorsManager.dividerColor),
),
itemBuilder: (context, index) {
final function = _flushFunctions[index];
return ListTile(
leading: SvgPicture.asset(
function.icon,
width: 24,
height: 24,
placeholderBuilder: (context) => const SizedBox(
width: 24,
height: 24,
),
),
title: Text(
function.operationName,
style: context.textTheme.bodyMedium,
),
trailing: const Icon(
Icons.arrow_forward_ios,
size: 16,
color: ColorsManager.textGray,
),
onTap: () => context.read<FunctionBloc>().add(
SelectFunction(
functionCode: function.code,
operationName: function.operationName,
),
),
);
},
),
);
}
Widget _buildValueSelector(BuildContext context, FunctionBlocState state) {
final selectedFunction = state.selectedFunction ?? '';
final functionData = state.addedFunctions.firstWhere(
(f) => f.functionCode == selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: selectedFunction,
operationName: state.selectedOperationName ?? '',
value: null,
),
);
return Expanded(
child: FlushValueSelectorWidget(
selectedFunction: selectedFunction,
functionData: functionData,
flushFunctions: _flushFunctions,
device: widget.device,
dialogType: widget.dialogType,
removeComparators: widget.removeComparetors,
),
);
}
Widget _buildDialogFooter(BuildContext context, FunctionBlocState state) {
return DialogFooter(
onCancel: () => Navigator.pop(context),
onConfirm: state.addedFunctions.isNotEmpty
? () {
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
widget.uniqueCustomId!,
),
);
Navigator.pop(
context,
{'deviceId': widget.functions.first.deviceId},
);
}
: null,
isConfirmEnabled: state.selectedFunction != null,
);
}
}

View File

@ -0,0 +1,178 @@
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/device_managment/flush_mounted_presence_sensor/models/flush_mounted_presence_sensor_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/flush/flush_functions.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/flush_presence_sensor/flush_operational_values_list.dart';
import 'package:syncrow_web/pages/routines/widgets/slider_value_selector.dart';
class FlushValueSelectorWidget extends StatelessWidget {
final String selectedFunction;
final DeviceFunctionData functionData;
final List<FlushFunctions> flushFunctions;
final AllDevicesModel? device;
final String dialogType;
final bool removeComparators;
const FlushValueSelectorWidget({
required this.selectedFunction,
required this.functionData,
required this.flushFunctions,
required this.device,
required this.dialogType,
required this.removeComparators,
super.key,
});
(double, double) get sliderRange => switch (functionData.functionCode) {
FlushMountedPresenceSensorModel.codeOccurDistReduce => (0, 3),
FlushMountedPresenceSensorModel.codeNoneDelay => (200, 3000),
FlushMountedPresenceSensorModel.codeSensiReduce => (0, 3),
FlushMountedPresenceSensorModel.codePresenceDelay => (0, 5),
FlushMountedPresenceSensorModel.codeIlluminance => (0.0, 2000.0),
FlushMountedPresenceSensorModel.codeFarDetection => (0.0, 9.5),
FlushMountedPresenceSensorModel.codeNearDetection => (0.0, 9.5),
_ => (0.0, 100.0),
};
double get stepSize => switch (functionData.functionCode) {
FlushMountedPresenceSensorModel.codeNoneDelay => 10.0,
FlushMountedPresenceSensorModel.codeFarDetection => 0.5,
FlushMountedPresenceSensorModel.codeNearDetection => 0.5,
FlushMountedPresenceSensorModel.codePresenceDelay => 1.0,
FlushMountedPresenceSensorModel.codeOccurDistReduce => 1.0,
FlushMountedPresenceSensorModel.codeSensiReduce => 1.0,
_ => 1.0,
};
@override
Widget build(BuildContext context) {
final selectedFn = flushFunctions.firstWhere(
(f) => f.code == selectedFunction,
orElse: () => throw Exception('Function $selectedFunction not found'),
);
if (_isSliderFunction(selectedFunction)) {
final isNearDetection =
selectedFunction == FlushMountedPresenceSensorModel.codeNearDetection;
final isFarDetection =
selectedFunction == FlushMountedPresenceSensorModel.codeFarDetection;
final isDistanceDetection = isNearDetection || isFarDetection;
double initialValue = (functionData.value as num?)?.toDouble() ?? 0.0;
if (isDistanceDetection) {
initialValue = initialValue / 100;
}
return SliderValueSelector(
currentCondition: functionData.condition,
dialogType: dialogType,
sliderRange: sliderRange,
displayedValue: getDisplayText,
initialValue: initialValue,
onConditionChanged: (condition) => context.read<FunctionBloc>().add(
AddFunction(
functionData: DeviceFunctionData(
entityId: device?.uuid ?? '',
functionCode: selectedFunction,
operationName: functionData.operationName,
condition: condition,
value: functionData.value ?? 0,
),
),
),
onSliderChanged: (value) {
final roundedValue = _roundToStep(value, stepSize);
final finalValue =
isDistanceDetection ? (roundedValue * 100).toInt() : roundedValue;
context.read<FunctionBloc>().add(
AddFunction(
functionData: DeviceFunctionData(
entityId: device?.uuid ?? '',
functionCode: selectedFunction,
operationName: functionData.operationName,
value: finalValue,
condition: functionData.condition,
),
),
);
},
unit: _unit,
dividendOfRange: stepSize,
);
}
return FlushOperationalValuesList(
values: selectedFn.getOperationalValues(),
selectedValue: functionData.value,
device: device,
operationName: selectedFn.operationName,
selectCode: selectedFunction,
onSelect: (selectedValue) async {
context.read<FunctionBloc>().add(
AddFunction(
functionData: DeviceFunctionData(
entityId: device?.uuid ?? '',
functionCode: selectedFunction,
operationName: functionData.operationName,
value: selectedValue.value,
condition: functionData.condition,
),
),
);
},
);
}
double _roundToStep(double value, double step) {
return (value / step).roundToDouble() * step;
}
bool _isSliderFunction(String function) => [
FlushMountedPresenceSensorModel.codeOccurDistReduce,
FlushMountedPresenceSensorModel.codeSensiReduce,
FlushMountedPresenceSensorModel.codeNoneDelay,
FlushMountedPresenceSensorModel.codeIlluminance,
FlushMountedPresenceSensorModel.codePresenceDelay,
FlushMountedPresenceSensorModel.codeFarDetection,
FlushMountedPresenceSensorModel.codeNearDetection,
].contains(function);
String get _unit => switch (functionData.functionCode) {
FlushMountedPresenceSensorModel.codeOccurDistReduce => 'Min',
FlushMountedPresenceSensorModel.codeSensiReduce => 'Sec',
FlushMountedPresenceSensorModel.codeNoneDelay => 'Sec',
FlushMountedPresenceSensorModel.codePresenceDelay => 'Sec',
FlushMountedPresenceSensorModel.codeIlluminance => 'Lux',
FlushMountedPresenceSensorModel.codeFarDetection => 'm',
FlushMountedPresenceSensorModel.codeNearDetection => 'm',
_ => '',
};
String get getDisplayText {
final num? value = functionData.value;
double displayValue = value?.toDouble() ?? 0.0;
if (functionData.functionCode ==
FlushMountedPresenceSensorModel.codeNearDetection ||
functionData.functionCode ==
FlushMountedPresenceSensorModel.codeFarDetection) {
displayValue = displayValue / 100;
}
switch (functionData.functionCode) {
case FlushMountedPresenceSensorModel.codeFarDetection:
case FlushMountedPresenceSensorModel.codeNearDetection:
return displayValue.toStringAsFixed(1);
case FlushMountedPresenceSensorModel.codeOccurDistReduce:
case FlushMountedPresenceSensorModel.codeSensiReduce:
case FlushMountedPresenceSensorModel.codePresenceDelay:
return displayValue.toStringAsFixed(0);
default:
return displayValue.toStringAsFixed(0);
}
}
}

View File

@ -0,0 +1,169 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class TimeWheelPicker extends StatefulWidget {
final int initialHours;
final int initialMinutes;
final int initialSeconds;
final Function(int, int, int) onTimeChanged;
const TimeWheelPicker({
super.key,
required this.initialHours,
required this.initialMinutes,
required this.initialSeconds,
required this.onTimeChanged,
});
@override
State<TimeWheelPicker> createState() => _TimeWheelPickerState();
}
class _TimeWheelPickerState extends State<TimeWheelPicker> {
late FixedExtentScrollController _hoursController;
late FixedExtentScrollController _minutesController;
late FixedExtentScrollController _secondsController;
@override
void initState() {
super.initState();
_hoursController =
FixedExtentScrollController(initialItem: widget.initialHours);
_minutesController =
FixedExtentScrollController(initialItem: widget.initialMinutes);
_secondsController =
FixedExtentScrollController(initialItem: widget.initialSeconds);
}
@override
void didUpdateWidget(TimeWheelPicker oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.initialHours != widget.initialHours) {
_hoursController.jumpToItem(widget.initialHours);
}
if (oldWidget.initialMinutes != widget.initialMinutes) {
_minutesController.jumpToItem(widget.initialMinutes);
}
if (oldWidget.initialSeconds != widget.initialSeconds) {
_secondsController.jumpToItem(widget.initialSeconds);
}
}
@override
void dispose() {
_hoursController.dispose();
_minutesController.dispose();
_secondsController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildPickerColumn(
label: 'h',
controller: _hoursController,
itemCount: 3,
onChanged: (value) {
_handleTimeChange(
value,
_minutesController.selectedItem,
_secondsController.selectedItem,
);
}),
const SizedBox(width: 5),
_buildPickerColumn(
label: 'm',
controller: _minutesController,
itemCount: 60,
onChanged: (value) {
_handleTimeChange(
_hoursController.selectedItem,
value,
_secondsController.selectedItem,
);
}),
const SizedBox(width: 5),
_buildPickerColumn(
label: 's',
controller: _secondsController,
itemCount: 60,
onChanged: (value) => _handleTimeChange(
_hoursController.selectedItem,
_minutesController.selectedItem,
value,
),
),
],
);
}
void _handleTimeChange(int hours, int minutes, int seconds) {
int total = hours * 3600 + minutes * 60 + seconds;
if (total > 10000) {
hours = 2;
minutes = 46;
seconds = 40;
total = 10000;
WidgetsBinding.instance.addPostFrameCallback((_) {
_hoursController.jumpToItem(hours);
_minutesController.jumpToItem(minutes);
_secondsController.jumpToItem(seconds);
});
}
widget.onTimeChanged(hours, minutes, seconds);
}
Widget _buildPickerColumn({
required String label,
required FixedExtentScrollController controller,
required int itemCount,
required Function(int) onChanged,
}) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
height: 40,
width: 40,
padding: const EdgeInsets.symmetric(horizontal: 5),
decoration: BoxDecoration(
color: ColorsManager.boxColor,
borderRadius: BorderRadius.circular(8),
),
child: ListWheelScrollView.useDelegate(
controller: controller,
itemExtent: 40.0,
physics: const FixedExtentScrollPhysics(),
onSelectedItemChanged: onChanged,
childDelegate: ListWheelChildBuilderDelegate(
builder: (context, index) => Center(
child: Text(
index.toString().padLeft(2),
style: const TextStyle(
fontSize: 18,
color: ColorsManager.blue1,
),
),
),
childCount: itemCount,
),
),
),
const SizedBox(width: 5),
Text(
label,
style: const TextStyle(
color: ColorsManager.blackColor,
fontSize: 18,
),
),
],
);
}
}

View File

@ -27,7 +27,8 @@ 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(
@ -40,11 +41,12 @@ 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
@ -64,14 +66,17 @@ 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'],
),
);
@ -80,11 +85,13 @@ class ThenContainer extends StatelessWidget {
.read<RoutineBloc>()
.add(AddToThenContainer({
...state.thenItems[index],
'imagePath': Assets.automation,
'title': state.thenItems[index]
['name'] ??
'imagePath':
Assets.automation,
'title':
state.thenItems[index]
['title'],
['name'] ??
state.thenItems[index]
['title'],
}));
}
return;
@ -109,8 +116,9 @@ class ThenContainer extends StatelessWidget {
'WPS',
'CPS',
"GW",
].contains(
state.thenItems[index]['productType'])) {
"NCPS"
].contains(state.thenItems[index]
['productType'])) {
context.read<RoutineBloc>().add(
AddToThenContainer(
state.thenItems[index]));
@ -120,7 +128,9 @@ 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),
@ -157,8 +167,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;
}
@ -183,8 +193,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;
}
@ -222,7 +232,7 @@ class ThenContainer extends StatelessWidget {
dialogType: "THEN");
if (result != null) {
context.read<RoutineBloc>().add(AddToThenContainer(mutableData));
} else if (!['AC', '1G', '2G', '3G', 'WPS', 'GW', 'CPS']
} else if (!['AC', '1G', '2G', '3G', 'WPS', 'GW', 'CPS', "NCPS"]
.contains(mutableData['productType'])) {
context.read<RoutineBloc>().add(AddToThenContainer(mutableData));
}