diff --git a/assets/icons/detection_distance_icon.svg b/assets/icons/detection_distance_icon.svg new file mode 100644 index 0000000..9b0faa1 --- /dev/null +++ b/assets/icons/detection_distance_icon.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/assets/icons/disappe_delay_icon.svg b/assets/icons/disappe_delay_icon.svg new file mode 100644 index 0000000..c65fc5a --- /dev/null +++ b/assets/icons/disappe_delay_icon.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/assets/icons/indent_level_icon.svg b/assets/icons/indent_level_icon.svg new file mode 100644 index 0000000..837cdac --- /dev/null +++ b/assets/icons/indent_level_icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/icons/target_confirm_time_icon.svg b/assets/icons/target_confirm_time_icon.svg new file mode 100644 index 0000000..6114165 --- /dev/null +++ b/assets/icons/target_confirm_time_icon.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/trigger_level_icon.svg b/assets/icons/trigger_level_icon.svg new file mode 100644 index 0000000..56e343e --- /dev/null +++ b/assets/icons/trigger_level_icon.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/features/devices/bloc/flush_sensor_bloc/flush_sensor_bloc.dart b/lib/features/devices/bloc/flush_sensor_bloc/flush_sensor_bloc.dart new file mode 100644 index 0000000..28fafce --- /dev/null +++ b/lib/features/devices/bloc/flush_sensor_bloc/flush_sensor_bloc.dart @@ -0,0 +1,129 @@ +import 'dart:async'; +import 'dart:developer'; +import 'package:firebase_database/firebase_database.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_app/features/devices/bloc/flush_sensor_bloc/flush_sensor_event.dart'; +import 'package:syncrow_app/features/devices/bloc/flush_sensor_bloc/flush_sensor_state.dart'; +import 'package:syncrow_app/features/devices/model/device_control_model.dart'; +import 'package:syncrow_app/features/devices/model/device_model.dart'; +import 'package:syncrow_app/features/devices/model/flush_sensor_model.dart'; +import 'package:syncrow_app/features/devices/model/status_model.dart'; +import 'package:syncrow_app/services/api/devices_api.dart'; + +class FlushSensorBloc extends Bloc { + final String deviceId; + late DeviceModel deviceModel; + late FlushSensorModel deviceStatus; + + FlushSensorBloc({required this.deviceId}) : super(InitialState()) { + on(_fetchFlushSensorStatus); + on(_changeIndicator); + on(_changeValue); + on(_flushSensorUpdated); + on(_getDeviceReports); + } + + void _fetchFlushSensorStatus( + InitialEvent event, Emitter emit) async { + emit(LoadingInitialState()); + try { + var response = await DevicesAPI.getDeviceStatus(deviceId); + List statusModelList = []; + for (var status in response['status']) { + statusModelList.add(StatusModel.fromJson(status)); + } + deviceStatus = FlushSensorModel.fromJson(statusModelList); + emit(UpdateState(flushSensorModel: deviceStatus)); + _listenToChanges(); + } catch (e) { + emit(FailedState(error: e.toString())); + return; + } + } + + StreamSubscription? _streamSubscription; + + void _listenToChanges() { + try { + _streamSubscription?.cancel(); + DatabaseReference ref = + FirebaseDatabase.instance.ref('device-status/$deviceId'); + Stream stream = ref.onValue; + + _streamSubscription = stream.listen((DatabaseEvent event) async { + Map usersMap = + event.snapshot.value as Map; + List statusList = []; + usersMap['status'].forEach((element) { + statusList + .add(StatusModel(code: element['code'], value: element['value'])); + }); + deviceStatus = FlushSensorModel.fromJson(statusList); + if (!isClosed) { + add(WallSensorUpdatedEvent()); + } + }); + } catch (_) { + log( + 'Error listening to changes', + name: 'FlushMountedPresenceSensorBloc._listenToChanges', + ); + } + } + + @override + Future close() async { + _streamSubscription?.cancel(); + _streamSubscription = null; + return super.close(); + } + + _flushSensorUpdated( + WallSensorUpdatedEvent event, Emitter emit) { + emit(UpdateState(flushSensorModel: deviceStatus)); + } + + void _changeIndicator( + ChangeIndicatorEvent event, Emitter emit) async { + emit(LoadingNewSate(flushSensorModel: deviceStatus)); + try { + final response = await DevicesAPI.controlDevice( + DeviceControlModel( + deviceId: deviceId, code: 'indicator', value: !event.value), + deviceId); + + } catch (e) { + emit(FailedState(error: e.toString())); + return; + } + emit(UpdateState(flushSensorModel: deviceStatus)); + } + + void _changeValue( + ChangeValueEvent event, Emitter emit) async { + emit(LoadingNewSate(flushSensorModel: deviceStatus)); + try { + final response = await DevicesAPI.controlDevice( + DeviceControlModel( + deviceId: deviceId, code: event.code, value: event.value), + deviceId); + } catch (e) { + emit(FailedState(error: e.toString())); + return; + } + emit(UpdateState(flushSensorModel: deviceStatus)); + } + + void _getDeviceReports( + GetDeviceReportsEvent event, Emitter emit) async { + emit(LoadingInitialState()); + try { + await DevicesAPI.getDeviceReports(deviceId, event.code).then((value) { + emit(DeviceReportsState(deviceReport: value, code: event.code)); + }); + } catch (e) { + emit(FailedState(error: e.toString())); + return; + } + } +} diff --git a/lib/features/devices/bloc/flush_sensor_bloc/flush_sensor_event.dart b/lib/features/devices/bloc/flush_sensor_bloc/flush_sensor_event.dart new file mode 100644 index 0000000..99c5aa5 --- /dev/null +++ b/lib/features/devices/bloc/flush_sensor_bloc/flush_sensor_event.dart @@ -0,0 +1,43 @@ +import 'package:equatable/equatable.dart'; + +abstract class FlushSensorEvent extends Equatable { + const FlushSensorEvent(); + + @override + List get props => []; +} + +class LoadingEvent extends FlushSensorEvent {} + +class InitialEvent extends FlushSensorEvent {} + +class WallSensorUpdatedEvent extends FlushSensorEvent {} + +class ChangeIndicatorEvent extends FlushSensorEvent { + final bool value; + const ChangeIndicatorEvent({required this.value}); + + @override + List get props => [value]; +} + +class ChangeValueEvent extends FlushSensorEvent { + final dynamic value; + final String code; + const ChangeValueEvent({required this.value, required this.code}); + + @override + List get props => [value, code]; +} + +class GetDeviceReportsEvent extends FlushSensorEvent { + final String deviceUuid; + final String code; + const GetDeviceReportsEvent({ + required this.deviceUuid, + required this.code, + }); + + @override + List get props => [deviceUuid, code]; +} diff --git a/lib/features/devices/bloc/flush_sensor_bloc/flush_sensor_state.dart b/lib/features/devices/bloc/flush_sensor_bloc/flush_sensor_state.dart new file mode 100644 index 0000000..5bbac2c --- /dev/null +++ b/lib/features/devices/bloc/flush_sensor_bloc/flush_sensor_state.dart @@ -0,0 +1,45 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_app/features/devices/model/device_report_model.dart'; +import 'package:syncrow_app/features/devices/model/flush_sensor_model.dart'; + +class FlushSensorState extends Equatable { + const FlushSensorState(); + + @override + List get props => []; +} + +class InitialState extends FlushSensorState {} + +class LoadingInitialState extends FlushSensorState {} + +class UpdateState extends FlushSensorState { + final FlushSensorModel flushSensorModel; + const UpdateState({required this.flushSensorModel}); + + @override + List get props => [flushSensorModel]; +} + +class LoadingNewSate extends FlushSensorState { + final FlushSensorModel flushSensorModel; + const LoadingNewSate({required this.flushSensorModel}); + + @override + List get props => [flushSensorModel]; +} + +class FailedState extends FlushSensorState { + final String error; + + const FailedState({required this.error}); + + @override + List get props => [error]; +} + +class DeviceReportsState extends FlushSensorState { + final DeviceReport deviceReport; + final String code; + const DeviceReportsState({required this.deviceReport, required this.code}); +} diff --git a/lib/features/devices/model/device_model.dart b/lib/features/devices/model/device_model.dart index 9824468..7abeb9e 100644 --- a/lib/features/devices/model/device_model.dart +++ b/lib/features/devices/model/device_model.dart @@ -92,6 +92,8 @@ class DeviceModel { tempIcon = Assets.sixSceneHomeIcon; } else if (type == DeviceType.SOS) { tempIcon = Assets.sosHomeIcon; + } else if (type == DeviceType.FlushMountedSensor) { + tempIcon = Assets.sosHomeIcon; } else { tempIcon = Assets.assetsIconsLogo; } diff --git a/lib/features/devices/model/flush_sensor_model.dart b/lib/features/devices/model/flush_sensor_model.dart new file mode 100644 index 0000000..3640c61 --- /dev/null +++ b/lib/features/devices/model/flush_sensor_model.dart @@ -0,0 +1,100 @@ + +import 'package:syncrow_app/features/devices/model/status_model.dart'; + +class FlushSensorModel { + FlushSensorModel({ + required this.presenceState, + required this.farDetection, + required this.illuminance, + required this.sensitivity, + required this.occurDistReduce, + required this.noneDelay, + required this.presenceDelay, + required this.nearDetection, + required this.sensiReduce, + required this.checkingResult, + }); + + static const String codePresenceState = 'presence_state'; + static const String codeSensitivity = 'sensitivity'; + static const String codeNearDetection = 'near_detection'; + static const String codeFarDetection = 'far_detection'; + static const String codeCheckingResult = 'checking_result'; + static const String codePresenceDelay = 'presence_delay'; + static const String codeNoneDelay = 'none_delay'; + static const String codeOccurDistReduce = 'occur_dist_reduce'; + static const String codeIlluminance = 'illum_value'; + static const String codeSensiReduce = 'sensi_reduce'; + + String presenceState; + int sensitivity; + int nearDetection; + int farDetection; + String checkingResult; + int presenceDelay; + int noneDelay; + int occurDistReduce; + int illuminance; + int sensiReduce; + + factory FlushSensorModel.fromJson(List jsonList) { + String presenceState = 'none'; + int sensitivity = 0; + int nearDetection = 0; + int farDetection = 0; + String checkingResult = 'none'; + int presenceDelay = 0; + int noneDelay = 0; + int occurDistReduce = 0; + int illuminance = 0; + int sensiReduce = 0; + + for (var status in jsonList) { + switch (status.code) { + case codePresenceState: + presenceState = status.value ?? 'presence'; + break; + case codeSensitivity: + sensitivity = status.value ?? 0; + break; + case codeNearDetection: + nearDetection = status.value ?? 0; + break; + case codeFarDetection: + farDetection = status.value ?? 0; + break; + case codeCheckingResult: + checkingResult = status.value ?? 'check_success'; + break; + case codePresenceDelay: + presenceDelay = status.value ?? 0; + break; + case codeNoneDelay: + noneDelay = status.value ?? 0; + break; + case codeOccurDistReduce: + occurDistReduce = status.value ?? 0; + break; + case codeIlluminance: + illuminance = status.value ?? 0; + break; + case codeSensiReduce: + sensiReduce = status.value ?? 0; + break; + } + } + + return FlushSensorModel( + presenceState: presenceState, + sensitivity: sensitivity, + nearDetection: nearDetection, + farDetection: farDetection, + checkingResult: checkingResult, + presenceDelay: presenceDelay, + noneDelay: noneDelay, + occurDistReduce: occurDistReduce, + illuminance: illuminance, + sensiReduce: sensiReduce, + ); + } +} diff --git a/lib/features/devices/view/widgets/flush_sensor/flush_persence_records.dart b/lib/features/devices/view/widgets/flush_sensor/flush_persence_records.dart new file mode 100644 index 0000000..a68535f --- /dev/null +++ b/lib/features/devices/view/widgets/flush_sensor/flush_persence_records.dart @@ -0,0 +1,120 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:intl/intl.dart'; +import 'package:syncrow_app/features/devices/bloc/flush_sensor_bloc/flush_sensor_bloc.dart'; +import 'package:syncrow_app/features/devices/bloc/flush_sensor_bloc/flush_sensor_event.dart'; +import 'package:syncrow_app/features/devices/bloc/flush_sensor_bloc/flush_sensor_state.dart'; +import 'package:syncrow_app/features/devices/model/device_report_model.dart'; +import 'package:syncrow_app/features/shared_widgets/default_container.dart'; +import 'package:syncrow_app/features/shared_widgets/default_scaffold.dart'; +import 'package:syncrow_app/utils/resource_manager/color_manager.dart'; + +class FlushPresenceRecords extends StatelessWidget { + final String deviceId; + final String code; + final String title; + const FlushPresenceRecords( + {super.key, required this.deviceId, required this.code, required this.title}); + + @override + Widget build(BuildContext context) { + return DefaultScaffold( + title: title, + child: BlocProvider( + create: (context) => FlushSensorBloc(deviceId: deviceId) + ..add(GetDeviceReportsEvent(deviceUuid: deviceId, code: code)), + child: BlocBuilder(builder: (context, state) { + final Map> groupedRecords = {}; + + if (state is LoadingInitialState) { + return const Center( + child: DefaultContainer(width: 50, height: 50, child: CircularProgressIndicator()), + ); + } else if (state is DeviceReportsState) { + for (var record in state.deviceReport.data ?? []) { + final DateTime eventDateTime = DateTime.fromMillisecondsSinceEpoch(record.eventTime!); + final String formattedDate = DateFormat('EEEE, dd/MM/yyyy').format(eventDateTime); + + // Group by formatted date + if (groupedRecords.containsKey(formattedDate)) { + groupedRecords[formattedDate]!.add(record); + } else { + groupedRecords[formattedDate] = [record]; + } + } + } + return groupedRecords.isEmpty + ? const Center( + child: Text('No records found'), + ) + : ListView.builder( + itemCount: groupedRecords.length, + itemBuilder: (context, index) { + final String date = groupedRecords.keys.elementAt(index); + final List recordsForDate = groupedRecords[date]!; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 5, top: 10), + child: Text( + date, + style: const TextStyle( + color: ColorsManager.grayColor, + fontSize: 13, + fontWeight: FontWeight.w700, + ), + ), + ), + DefaultContainer( + child: Column( + children: [ + ...recordsForDate.asMap().entries.map((entry) { + final int idx = entry.key; + final DeviceEvent record = entry.value; + final DateTime eventDateTime = + DateTime.fromMillisecondsSinceEpoch(record.eventTime!); + final String formattedTime = + DateFormat('HH:mm:ss').format(eventDateTime); + + return Column( + children: [ + SizedBox( + child: ListTile( + leading: Icon( + record.value == 'true' + ? Icons.radio_button_checked + : Icons.radio_button_unchecked, + color: record.value == 'true' ? Colors.blue : Colors.grey, + ), + title: Text( + record.value == 'true' ? "Opened" : "Closed", + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + subtitle: Text('$formattedTime'), + ), + ), + if (idx != recordsForDate.length - 1) + const Divider( + color: ColorsManager.graysColor, + ), + ], + ); + }).toList(), + ], + ), + ), + ], + ); + }, + ); + }), + ), + ); + } +} diff --git a/lib/features/devices/view/widgets/flush_sensor/flush_sensor_interface.dart b/lib/features/devices/view/widgets/flush_sensor/flush_sensor_interface.dart new file mode 100644 index 0000000..085f173 --- /dev/null +++ b/lib/features/devices/view/widgets/flush_sensor/flush_sensor_interface.dart @@ -0,0 +1,113 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_app/features/devices/bloc/flush_sensor_bloc/flush_sensor_bloc.dart'; +import 'package:syncrow_app/features/devices/bloc/flush_sensor_bloc/flush_sensor_event.dart'; +import 'package:syncrow_app/features/devices/bloc/flush_sensor_bloc/flush_sensor_state.dart'; +import 'package:syncrow_app/features/devices/model/device_model.dart'; +import 'package:syncrow_app/features/devices/model/flush_sensor_model.dart'; +import 'package:syncrow_app/features/devices/view/widgets/device_appbar.dart'; +import 'package:syncrow_app/features/devices/view/widgets/flush_sensor/flush_persence_records.dart'; +import 'package:syncrow_app/features/devices/view/widgets/flush_sensor/flush_sensor_option.dart'; +import 'package:syncrow_app/features/shared_widgets/text_widgets/body_medium.dart'; +import 'package:syncrow_app/features/shared_widgets/text_widgets/title_medium.dart'; +import 'package:syncrow_app/generated/assets.dart'; +import 'package:syncrow_app/utils/context_extension.dart'; +import 'package:syncrow_app/utils/helpers/misc_string_helpers.dart'; +import 'package:syncrow_app/utils/helpers/snack_bar.dart'; +import 'package:syncrow_app/utils/resource_manager/color_manager.dart'; +import 'package:syncrow_app/utils/resource_manager/constants.dart'; +import 'package:syncrow_app/utils/resource_manager/font_manager.dart'; +part "presence_indicator.dart"; +part "flush_sensor_options_list.dart"; +part 'flush_sensor_parameter_control_dialog.dart'; + +class FlushMountedInterface extends StatelessWidget { + const FlushMountedInterface({super.key, required this.deviceModel}); + final DeviceModel deviceModel; + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => FlushSensorBloc(deviceId: deviceModel.uuid ?? '') + ..add(InitialEvent()), + child: BlocBuilder( + builder: (context, state) { + final bloc = BlocProvider.of(context); + FlushSensorModel flushSensorModel = FlushSensorModel( + presenceState: 'none', + farDetection: 0, + illuminance: 0, + checkingResult: '', + nearDetection: 0, + noneDelay: 0, + occurDistReduce: 0, + presenceDelay: 0, + sensiReduce: 0, + sensitivity: 0); + + if (state is UpdateState) { + flushSensorModel = state.flushSensorModel; + } else if (state is LoadingNewSate) { + flushSensorModel = state.flushSensorModel; + } + return AnnotatedRegion( + value: SystemUiOverlayStyle( + statusBarColor: ColorsManager.primaryColor.withOpacity(0.5), + statusBarIconBrightness: Brightness.light, + ), + child: Scaffold( + backgroundColor: ColorsManager.backgroundColor, + extendBodyBehindAppBar: true, + extendBody: true, + appBar: DeviceAppbar( + deviceName: deviceModel.name!, + deviceUuid: deviceModel.uuid!, + ), + body: Container( + width: MediaQuery.sizeOf(context).width, + height: MediaQuery.sizeOf(context).height, + padding: const EdgeInsets.all(Constants.defaultPadding), + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage( + Assets.assetsImagesBackground, + ), + fit: BoxFit.cover, + opacity: 0.4, + ), + ), + child: state is LoadingInitialState + ? const Center( + child: RefreshProgressIndicator(), + ) + : SafeArea( + child: RefreshIndicator( + onRefresh: () async { + bloc.add(InitialEvent()); + }, + child: ListView(children: [ + SizedBox( + height: MediaQuery.of(context).size.height, + child: Column(children: [ + PresenceIndicator( + state: flushSensorModel.presenceState, + ), + Expanded( + flex: 2, + child: FlushSensorOptionsList( + bloc: bloc, + flushSensorModel: flushSensorModel, + deviceModel: deviceModel, + ), + ) + ]), + ), + ]), + ), + )))); + }), + ); + } +} diff --git a/lib/features/devices/view/widgets/flush_sensor/flush_sensor_option.dart b/lib/features/devices/view/widgets/flush_sensor/flush_sensor_option.dart new file mode 100644 index 0000000..7911bb0 --- /dev/null +++ b/lib/features/devices/view/widgets/flush_sensor/flush_sensor_option.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_app/features/shared_widgets/default_container.dart'; +import 'package:syncrow_app/utils/resource_manager/color_manager.dart'; + +class FlushSensorOption extends StatelessWidget { + final String icon; + final String label; + final VoidCallback onTap; + final String? sliderValue; + final bool withArrow; + + const FlushSensorOption({ + super.key, + required this.icon, + required this.label, + required this.onTap, + required this.withArrow, + this.sliderValue = '', + }); + + @override + Widget build(BuildContext context) { + return DefaultContainer( + margin: const EdgeInsets.only(bottom: 5), + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 20), + onTap: onTap, + child: Row( + children: [ + SvgPicture.asset(icon), + const SizedBox(width: 25), + Expanded( + child: + Text(label, style: Theme.of(context).textTheme.bodyMedium)), + Text(sliderValue!, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: ColorsManager.grayTextColor, + fontWeight: FontWeight.bold)), + withArrow == true + ? const Icon(Icons.arrow_forward_ios, + color: ColorsManager.grayTextColor, size: 15) + : SizedBox(), + ], + ), + ); + } +} diff --git a/lib/features/devices/view/widgets/flush_sensor/flush_sensor_options_list.dart b/lib/features/devices/view/widgets/flush_sensor/flush_sensor_options_list.dart new file mode 100644 index 0000000..38f8ffb --- /dev/null +++ b/lib/features/devices/view/widgets/flush_sensor/flush_sensor_options_list.dart @@ -0,0 +1,193 @@ +part of "flush_sensor_interface.dart"; + +class FlushSensorOptionsList extends StatelessWidget { + final FlushSensorModel flushSensorModel; + final DeviceModel deviceModel; + final FlushSensorBloc bloc; + const FlushSensorOptionsList( + {super.key, + required this.flushSensorModel, + required this.bloc, + required this.deviceModel}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlushSensorOption( + withArrow: false, + sliderValue: '${flushSensorModel.illuminance.toString()} Lux', + icon: Assets.assetsIconsPresenceSensorAssetsIlluminanceValue, + label: 'Illuminance Value', + onTap: () {}, + ), + FlushSensorOption( + withArrow: false, + icon: Assets.assetsIconsPresenceSensorAssetsIlluminanceRecord, + label: 'Presence Record', + onTap: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (_) => FlushPresenceRecords( + deviceId: deviceModel.uuid!, + code: FlushSensorModel.codePresenceState, + title: 'Illuminance Record'), + )); + }), + Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Text( + 'Configuration', + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(fontWeight: FontWeight.bold), + ), + ), + FlushSensorOption( + withArrow: true, + sliderValue: flushSensorModel.sensitivity.toString(), + icon: Assets.assetsSensitivityFunction, + label: 'Sensitivity', + onTap: () => _showParameterDialog( + min: 0, + max: 9, + value: flushSensorModel.sensitivity.toDouble(), + context: context, + title: 'Sensitivity', + controlCode: FlushSensorModel.codeSensitivity, + step: 1.0, + ), + ), + FlushSensorOption( + withArrow: true, + sliderValue: "${flushSensorModel.nearDetection / 100} m", + icon: Assets.detectionDistanceIcon, + label: 'Min Detection Distance', + onTap: () => _showParameterDialog( + min: 0.0, + max: 9.5, + value: flushSensorModel.nearDetection.toDouble() / 100, + context: context, + title: 'Min Detection Distance', + controlCode: FlushSensorModel.codeNearDetection, + step: 0.1, + description: 'm', + ), + ), + FlushSensorOption( + withArrow: true, + sliderValue: '${flushSensorModel.farDetection / 100} m', + icon: Assets.detectionDistanceIcon, + label: 'Max Detection Distance', + onTap: () => _showParameterDialog( + description: 'm', + min: 0.0, + max: 9.5, + value: flushSensorModel.farDetection / 100, + context: context, + title: 'Max Detection Distance', + controlCode: FlushSensorModel.codeFarDetection, + step: 0.1), + ), + FlushSensorOption( + sliderValue: flushSensorModel.sensiReduce.toString(), + withArrow: true, + icon: Assets.triggerLevelIcon, + label: 'Trigger Level', + onTap: () => _showParameterDialog( + min: 0, + max: 3, + value: flushSensorModel.sensiReduce.toDouble(), + context: context, + title: 'Trigger Level', + controlCode: FlushSensorModel.codeSensiReduce, + step: 1), + ), + FlushSensorOption( + sliderValue: flushSensorModel.occurDistReduce.toString(), + withArrow: true, + icon: Assets.indentLevelIcon, + label: 'Indent Level', + onTap: () => _showParameterDialog( + min: 0, + max: 3, + value: flushSensorModel.occurDistReduce.toDouble(), + context: context, + title: 'Indent Level', + controlCode: FlushSensorModel.codeOccurDistReduce, + step: 1), + ), + FlushSensorOption( + sliderValue: '${flushSensorModel.presenceDelay / 10}', + withArrow: true, + icon: Assets.targetConfirmTimeIcon, + label: 'Target Confirm Time', + onTap: () => _showParameterDialog( + min: 0.0, + max: 0.5, + value: flushSensorModel.presenceDelay.toDouble() / 10, + context: context, + title: 'Target Confirm Time', + controlCode: FlushSensorModel.codePresenceDelay, + step: 0.1), + ), + FlushSensorOption( + sliderValue: '${flushSensorModel.noneDelay.toDouble() / 10}', + withArrow: true, + icon: Assets.disappeDelayIcon, + label: 'Disappe Delay', + onTap: () => _showParameterDialog( + min: 20, + max: 300, + value: flushSensorModel.noneDelay.toDouble() / 10, + context: context, + title: 'Disappe Delay', + controlCode: FlushSensorModel.codeNoneDelay, + step: 1), + ), + ], + ); + } + + void _showParameterDialog({ + required double step, + required BuildContext context, + required String title, + required String controlCode, + required double min, + required double max, + required double value, + String? description = '', + }) async { + var result = await showDialog( + context: context, + builder: (context) { + return FlushParameterControlDialog( + title: title, + sensor: deviceModel, + value: value, + min: min, + max: max, + step: step, + description: description); + }, + ); + + if (result != null) { + if (controlCode == FlushSensorModel.codeNearDetection || + controlCode == FlushSensorModel.codeFarDetection) { + result = (result * 100).toInt(); + } + if (controlCode == FlushSensorModel.codeNoneDelay) { + result = (result * 10).toInt(); + } + if (controlCode == FlushSensorModel.codePresenceDelay) { + result = (result * 10).toInt(); + } + bloc.add( + ChangeValueEvent(value: result, code: controlCode), + ); + } + } +} diff --git a/lib/features/devices/view/widgets/flush_sensor/flush_sensor_parameter_control_dialog.dart b/lib/features/devices/view/widgets/flush_sensor/flush_sensor_parameter_control_dialog.dart new file mode 100644 index 0000000..1b4a052 --- /dev/null +++ b/lib/features/devices/view/widgets/flush_sensor/flush_sensor_parameter_control_dialog.dart @@ -0,0 +1,181 @@ +part of 'flush_sensor_interface.dart'; + +class FlushParameterControlDialog extends StatefulWidget { + final String title; + final String? description; + final DeviceModel sensor; + final double value; + final double min; + final double max; + final double step; + + const FlushParameterControlDialog({ + super.key, + required this.title, + required this.sensor, + required this.value, + required this.min, + required this.max, + required this.step, + this.description = '', + }); + + @override + FlushParameterControlDialogState createState() => + FlushParameterControlDialogState(); +} + +class FlushParameterControlDialogState + extends State { + late double _value; + + @override + void initState() { + super.initState(); + _value = widget.value; + } + + @override + Widget build(BuildContext context) { + final divisions = ((widget.max - widget.min) / widget.step).toInt(); + final decimalPlaces = widget.step % 1 == 0 ? 0 : 1; + return Dialog( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.only(top: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + BodyMedium( + text: widget.title, + style: context.bodyMedium.copyWith( + color: ColorsManager.primaryColorWithOpacity, + fontWeight: FontsManager.extraBold, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 15, + horizontal: 50, + ), + child: Container( + height: 1, + width: double.infinity, + color: ColorsManager.greyColor, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 10, + ), + child: TitleMedium( + text: + "${_value.toStringAsFixed(decimalPlaces)} ${widget.description}", + style: context.titleMedium.copyWith( + color: Colors.black, + fontWeight: FontsManager.bold, + ), + ), + ), + SizedBox( + width: MediaQuery.sizeOf(context).width, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + onPressed: () { + setState(() { + _value = (_value - widget.step) + .clamp(widget.min, widget.max); + _value = + double.parse(_value.toStringAsFixed(decimalPlaces)); + }); + }, + icon: const Icon(Icons.remove, color: Colors.grey), + ), + Flexible( + child: Slider( + value: _value, + onChanged: (value) { + final newValue = + (value / widget.step).round() * widget.step; + setState(() { + _value = newValue.clamp(widget.min, widget.max); + }); + }, + min: widget.min, + max: widget.max, + divisions: divisions, + label: _value.toStringAsFixed(decimalPlaces), + ), + ), + IconButton( + onPressed: () { + setState(() { + _value = (_value + widget.step) + .clamp(widget.min, widget.max); + _value = + double.parse(_value.toStringAsFixed(decimalPlaces)); + }); + }, + icon: const Icon(Icons.add, color: Colors.grey), + ), + ], + ), + ), + Container( + height: 1, + width: double.infinity, + color: ColorsManager.greyColor, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + InkWell( + onTap: () { + Navigator.pop(context); + }, + child: Center( + child: BodyMedium( + text: 'Cancel', + style: context.bodyMedium + .copyWith(color: ColorsManager.greyColor), + ), + ), + ), + Container( + height: 50, + width: 1, + color: ColorsManager.greyColor, + ), + InkWell( + onTap: () { + if (widget.sensor.isOnline == null) { + CustomSnackBar.displaySnackBar('The device is offline'); + return; + } + if (!widget.sensor.isOnline!) { + CustomSnackBar.displaySnackBar('The device is offline'); + return; + } + Navigator.pop(context, _value); + }, + child: Center( + child: BodyMedium( + text: 'Confirm', + style: context.bodyMedium.copyWith( + color: ColorsManager.primaryColorWithOpacity), + ), + ), + ), + ], + ) + ], + ), + ), + ); + } +} diff --git a/lib/features/devices/view/widgets/flush_sensor/presence_indicator.dart b/lib/features/devices/view/widgets/flush_sensor/presence_indicator.dart new file mode 100644 index 0000000..746e08f --- /dev/null +++ b/lib/features/devices/view/widgets/flush_sensor/presence_indicator.dart @@ -0,0 +1,31 @@ +part of "flush_sensor_interface.dart"; + +class PresenceIndicator extends StatelessWidget { + const PresenceIndicator({super.key, required this.state}); + final String state; + @override + Widget build(BuildContext context) { + return Expanded( + flex: 1, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + state.toLowerCase() == 'motion' + ? Assets.assetsIconsPresenceSensorAssetsPresenceSensorMotion + : Assets.assetsIconsPresenceSensorAssetsPresence, + width: 100, + height: 100, + ), + const SizedBox( + height: 10, + ), + BodyMedium( + text: StringHelpers.toTitleCase(state), + style: context.bodyMedium.copyWith(fontWeight: FontsManager.bold), + ), + ], + ), + ); + } +} diff --git a/lib/features/devices/view/widgets/room_page_switch.dart b/lib/features/devices/view/widgets/room_page_switch.dart index 9c95700..074a047 100644 --- a/lib/features/devices/view/widgets/room_page_switch.dart +++ b/lib/features/devices/view/widgets/room_page_switch.dart @@ -13,6 +13,7 @@ import 'package:syncrow_app/features/devices/view/widgets/ACs/acs_view.dart'; import 'package:syncrow_app/features/devices/view/widgets/ceiling_sensor/ceiling_sensor_interface.dart'; import 'package:syncrow_app/features/devices/view/widgets/curtains/curtain_view.dart'; import 'package:syncrow_app/features/devices/view/widgets/door_sensor/door_sensor_screen.dart'; +import 'package:syncrow_app/features/devices/view/widgets/flush_sensor/flush_sensor_interface.dart'; import 'package:syncrow_app/features/devices/view/widgets/four_scene_switch/four_scene_screen.dart'; import 'package:syncrow_app/features/devices/view/widgets/garage_door/garage_door_screen.dart'; import 'package:syncrow_app/features/devices/view/widgets/gateway/gateway_view.dart'; @@ -300,6 +301,13 @@ Future showDeviceInterface( pageBuilder: (context, animation1, animation2) => SosScreen(device: device))); + case DeviceType.FlushMountedSensor: + Navigator.push( + context, + PageRouteBuilder( + pageBuilder: (context, animation1, animation2) => + FlushMountedInterface(deviceModel: device))); + break; default: } diff --git a/lib/generated/assets.dart b/lib/generated/assets.dart index 29aef40..53cd6ae 100644 --- a/lib/generated/assets.dart +++ b/lib/generated/assets.dart @@ -1148,4 +1148,11 @@ class Assets { static const String refreshStatusIcon = 'assets/icons/refresh_status_icon.svg'; + static const String detectionDistanceIcon = + 'assets/icons/detection_distance_icon.svg'; + static const String triggerLevelIcon = 'assets/icons/trigger_level_icon.svg'; + static const String indentLevelIcon = 'assets/icons/indent_level_icon.svg'; + static const String targetConfirmTimeIcon = + 'assets/icons/target_confirm_time_icon.svg'; + static const String disappeDelayIcon = 'assets/icons/disappe_delay_icon.svg'; } diff --git a/lib/services/api/devices_api.dart b/lib/services/api/devices_api.dart index 4743fe0..35b390e 100644 --- a/lib/services/api/devices_api.dart +++ b/lib/services/api/devices_api.dart @@ -595,10 +595,7 @@ class DevicesAPI { final result = []; for (final device in data) { final mappedDevice = DeviceModel.fromJson(device); - if (mappedDevice.productType?.name != - DeviceType.FlushMountedSensor.name) { - result.add(mappedDevice); - } + result.add(mappedDevice); } return result; }, diff --git a/lib/utils/resource_manager/color_manager.dart b/lib/utils/resource_manager/color_manager.dart index fc792a8..8146d5e 100644 --- a/lib/utils/resource_manager/color_manager.dart +++ b/lib/utils/resource_manager/color_manager.dart @@ -36,7 +36,5 @@ abstract class ColorsManager { static const Color grayButtonColors = Color(0xffCCCCCC); static const Color blueButton = Color(0xff85BDFF); static const Color backgroundGrey = Color(0xFFF3F3F3); - + static const Color grayTextColor = Color(0xFFC4C4C4); } -//background: #F5F5F5;background: #85BDFF; -