add WallPresenceSensor and add new icons for presence state and selected value

This commit is contained in:
mohammad
2025-04-06 09:57:12 +03:00
parent b77dc860cb
commit 4fc259491e
21 changed files with 1517 additions and 115 deletions

View File

@ -22,21 +22,25 @@ class ThreeGangSwitchHelper {
String uniqueCustomId,
bool removeComparetors,
) async {
List<BaseSwitchFunction> switchFunctions = functions.whereType<BaseSwitchFunction>().toList();
List<BaseSwitchFunction> switchFunctions =
functions.whereType<BaseSwitchFunction>().toList();
return showDialog<Map<String, dynamic>?>(
context: context,
builder: (BuildContext context) {
print('functions: $functions');
return BlocProvider(
create: (_) => FunctionBloc()..add(InitializeFunctions(deviceSelectedFunctions ?? [])),
create: (_) => FunctionBloc()
..add(InitializeFunctions(deviceSelectedFunctions ?? [])),
child: AlertDialog(
contentPadding: EdgeInsets.zero,
content: BlocBuilder<FunctionBloc, FunctionBlocState>(
builder: (context, state) {
final selectedFunction = state.selectedFunction;
final selectedOperationName = state.selectedOperationName;
final selectedFunctionData =
state.addedFunctions.firstWhere((f) => f.functionCode == selectedFunction,
final selectedFunctionData = state.addedFunctions
.firstWhere((f) => f.functionCode == selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: selectedFunction ?? '',
@ -83,9 +87,12 @@ class ThreeGangSwitchHelper {
color: ColorsManager.textGray,
),
onTap: () {
context.read<FunctionBloc>().add(SelectFunction(
context
.read<FunctionBloc>()
.add(SelectFunction(
functionCode: function.code,
operationName: function.operationName,
operationName:
function.operationName,
));
},
);
@ -177,7 +184,8 @@ class ThreeGangSwitchHelper {
);
}
final selectedFn = switchFunctions.firstWhere((f) => f.code == selectedFunction);
final selectedFn =
switchFunctions.firstWhere((f) => f.code == selectedFunction);
final values = selectedFn.getOperationalValues();
return _buildOperationalValuesList(
@ -214,11 +222,11 @@ class ThreeGangSwitchHelper {
selectedFunctionData,
),
const SizedBox(height: 20),
_buildCountDownDisplay(
context, initialValue, device, operationName, selectedFunctionData, selectCode),
_buildCountDownDisplay(context, initialValue, device, operationName,
selectedFunctionData, selectCode),
const SizedBox(height: 20),
_buildCountDownSlider(
context, initialValue, device, operationName, selectedFunctionData, selectCode),
_buildCountDownSlider(context, initialValue, device, operationName,
selectedFunctionData, selectCode),
],
);
}
@ -259,7 +267,8 @@ class ThreeGangSwitchHelper {
minHeight: 40.0,
minWidth: 40.0,
),
isSelected: conditions.map((c) => c == (currentCondition ?? "==")).toList(),
isSelected:
conditions.map((c) => c == (currentCondition ?? "==")).toList(),
children: conditions.map((c) => Text(c)).toList(),
);
}
@ -307,7 +316,8 @@ class ThreeGangSwitchHelper {
value: (initialValue ?? 0).toDouble(),
min: operationalValues.minValue?.toDouble() ?? 0.0,
max: operationalValues.maxValue?.toDouble() ?? 0.0,
divisions: (((operationalValues.maxValue ?? 0) - (operationalValues.minValue ?? 0)) /
divisions: (((operationalValues.maxValue ?? 0) -
(operationalValues.minValue ?? 0)) /
(operationalValues.stepValue ?? 1))
.round(),
onChanged: (value) {
@ -359,9 +369,13 @@ class ThreeGangSwitchHelper {
style: context.textTheme.bodyMedium,
),
trailing: Icon(
isSelected ? Icons.radio_button_checked : Icons.radio_button_unchecked,
isSelected
? Icons.radio_button_checked
: Icons.radio_button_unchecked,
size: 24,
color: isSelected ? ColorsManager.primaryColorWithOpacity : ColorsManager.textGray,
color: isSelected
? ColorsManager.primaryColorWithOpacity
: ColorsManager.textGray,
),
onTap: () {
if (!isSelected) {
@ -373,7 +387,8 @@ class ThreeGangSwitchHelper {
operationName: operationName,
value: value.value,
condition: selectedFunctionData?.condition,
valueDescription: selectedFunctionData?.valueDescription,
valueDescription:
selectedFunctionData?.valueDescription,
),
),
);

View File

@ -0,0 +1,150 @@
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: 24,
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) {
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

@ -0,0 +1,553 @@
import 'package:flutter/material.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/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/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<DeviceFunction> functions;
final AllDevicesModel? device;
final List<DeviceFunctionData>? deviceSelectedFunctions;
final String? uniqueCustomId;
final String? dialogType;
final bool removeComparetors;
const WallPresenceSensor({
super.key,
required this.functions,
this.device,
this.deviceSelectedFunctions,
this.uniqueCustomId,
this.dialogType,
this.removeComparetors = false,
});
static Future<Map<String, dynamic>?> showWPSFunctionsDialog({
required BuildContext context,
required List<DeviceFunction> functions,
AllDevicesModel? device,
List<DeviceFunctionData>? deviceSelectedFunctions,
String? uniqueCustomId,
String? dialogType,
bool removeComparetors = false,
}) async {
return showDialog<Map<String, dynamic>?>(
context: context,
builder: (context) => WallPresenceSensor(
functions: functions,
device: device,
deviceSelectedFunctions: deviceSelectedFunctions,
uniqueCustomId: uniqueCustomId,
removeComparetors: removeComparetors,
dialogType: dialogType,
),
);
}
@override
State<WallPresenceSensor> createState() => _WallPresenceSensorState();
}
class _WallPresenceSensorState extends State<WallPresenceSensor> {
late final List<WpsFunctions> _wpsFunctions;
@override
void initState() {
super.initState();
_wpsFunctions =
widget.functions.whereType<WpsFunctions>().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: _wpsFunctions.length,
separatorBuilder: (context, index) => const Padding(
padding: EdgeInsets.symmetric(horizontal: 40.0),
child: Divider(color: ColorsManager.dividerColor),
),
itemBuilder: (context, index) {
final function = _wpsFunctions[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: '',
value: null,
),
);
return Expanded(
child: _ValueSelector(
selectedFunction: selectedFunction,
functionData: functionData,
acFunctions: _wpsFunctions,
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,
);
}
}
class _ValueSelector extends StatelessWidget {
final String selectedFunction;
final DeviceFunctionData functionData;
final List<WpsFunctions> 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) => [
'current_distance',
'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<FunctionBloc>().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 'current_distance':
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 'current_distance':
return (1, 600);
case 'illuminance_value':
return (0, 10000);
default:
return (200, 300);
}
}
void _updateValue(BuildContext context, int value) {
context.read<FunctionBloc>().add(
AddFunction(
functionData: DeviceFunctionData(
entityId: device?.uuid ?? '',
functionCode: functionCode,
operationName: functionData.operationName,
value: value,
condition: functionData.condition,
),
),
);
}
}
class _OperationalValuesList extends StatelessWidget {
final List<WpsOperationalValue> 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<dynamic>(
value: value.value,
groupValue: selectedValue,
onChanged: (_) => _selectValue(context, value.value),
);
}
void _selectValue(BuildContext context, dynamic value) {
context.read<FunctionBloc>().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<FunctionBloc>().add(
AddFunction(
functionData: DeviceFunctionData(
entityId: device?.uuid ?? '',
functionCode: selectCode,
operationName: operationName,
value: h * 3600 + m * 60 + s,
),
),
);
}
}