From a6e2681b6a48e2ba9209216a6ad8ed7ccbb8d42e Mon Sep 17 00:00:00 2001 From: ashrafzarkanisala Date: Wed, 13 Nov 2024 12:09:31 +0300 Subject: [PATCH 01/23] adding tabs and create widget --- lib/pages/common/custom_table.dart | 2 +- .../device_managment_bloc.dart | 0 .../device_managment_event.dart | 0 .../device_managment_state.dart | 0 .../bloc/switch_tabs/switch_tabs_bloc.dart | 28 +++++ .../bloc/switch_tabs/switch_tabs_event.dart | 21 ++++ .../bloc/switch_tabs/switch_tabs_state.dart | 26 +++++ .../view/device_managment_page.dart | 102 ++++++++++++++---- .../widgets/device_managment_body.dart | 69 ++++-------- .../widgets/device_search_filters.dart | 14 +-- .../view/create_new_routine_view.dart | 10 ++ lib/pages/routiens/view/routines_view.dart | 61 +++++++++++ 12 files changed, 252 insertions(+), 81 deletions(-) rename lib/pages/device_managment/all_devices/bloc/{ => device_mgmt_bloc}/device_managment_bloc.dart (100%) rename lib/pages/device_managment/all_devices/bloc/{ => device_mgmt_bloc}/device_managment_event.dart (100%) rename lib/pages/device_managment/all_devices/bloc/{ => device_mgmt_bloc}/device_managment_state.dart (100%) create mode 100644 lib/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_bloc.dart create mode 100644 lib/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_event.dart create mode 100644 lib/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_state.dart create mode 100644 lib/pages/routiens/view/create_new_routine_view.dart create mode 100644 lib/pages/routiens/view/routines_view.dart diff --git a/lib/pages/common/custom_table.dart b/lib/pages/common/custom_table.dart index 22baba36..60abc0d2 100644 --- a/lib/pages/common/custom_table.dart +++ b/lib/pages/common/custom_table.dart @@ -1,7 +1,7 @@ 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/bloc/device_managment_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; diff --git a/lib/pages/device_managment/all_devices/bloc/device_managment_bloc.dart b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart similarity index 100% rename from lib/pages/device_managment/all_devices/bloc/device_managment_bloc.dart rename to lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart diff --git a/lib/pages/device_managment/all_devices/bloc/device_managment_event.dart b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_event.dart similarity index 100% rename from lib/pages/device_managment/all_devices/bloc/device_managment_event.dart rename to lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_event.dart diff --git a/lib/pages/device_managment/all_devices/bloc/device_managment_state.dart b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_state.dart similarity index 100% rename from lib/pages/device_managment/all_devices/bloc/device_managment_state.dart rename to lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_state.dart diff --git a/lib/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_bloc.dart b/lib/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_bloc.dart new file mode 100644 index 00000000..3eaccf70 --- /dev/null +++ b/lib/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_bloc.dart @@ -0,0 +1,28 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'switch_tabs_event.dart'; +part 'switch_tabs_state.dart'; + +class SwitchTabsBloc extends Bloc { + SwitchTabsBloc() : super(SwitchTabsInitial()) { + on(_switchTab); + on(_newRoutineView); + } + + FutureOr _switchTab( + TriggerSwitchTabsEvent event, + Emitter emit, + ) { + emit(SelectedTabState(event.isRoutineView)); + } + + FutureOr _newRoutineView( + CreateNewRoutineViewEvent event, + Emitter emit, + ) { + emit(ShowCreateRoutineState(event.showCreateNewRoutineView)); + } +} diff --git a/lib/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_event.dart b/lib/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_event.dart new file mode 100644 index 00000000..98cad361 --- /dev/null +++ b/lib/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_event.dart @@ -0,0 +1,21 @@ +part of 'switch_tabs_bloc.dart'; + +sealed class SwitchTabsEvent extends Equatable { + const SwitchTabsEvent(); +} + +class TriggerSwitchTabsEvent extends SwitchTabsEvent { + final bool isRoutineView; + const TriggerSwitchTabsEvent(this.isRoutineView); + + @override + List get props => [isRoutineView]; +} + +class CreateNewRoutineViewEvent extends SwitchTabsEvent { + final bool showCreateNewRoutineView; + const CreateNewRoutineViewEvent(this.showCreateNewRoutineView); + + @override + List get props => [showCreateNewRoutineView]; +} diff --git a/lib/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_state.dart b/lib/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_state.dart new file mode 100644 index 00000000..dd01aeaa --- /dev/null +++ b/lib/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_state.dart @@ -0,0 +1,26 @@ +part of 'switch_tabs_bloc.dart'; + +sealed class SwitchTabsState extends Equatable { + const SwitchTabsState(); +} + +final class SwitchTabsInitial extends SwitchTabsState { + @override + List get props => []; +} + +class SelectedTabState extends SwitchTabsState { + final bool selectedTab; + const SelectedTabState(this.selectedTab); + + @override + List get props => [selectedTab]; +} + +class ShowCreateRoutineState extends SwitchTabsState { + final bool showCreateRoutine; + const ShowCreateRoutineState(this.showCreateRoutine); + + @override + List get props => [showCreateRoutine]; +} diff --git a/lib/pages/device_managment/all_devices/view/device_managment_page.dart b/lib/pages/device_managment/all_devices/view/device_managment_page.dart index 8ed8c35e..d163a718 100644 --- a/lib/pages/device_managment/all_devices/view/device_managment_page.dart +++ b/lib/pages/device_managment/all_devices/view/device_managment_page.dart @@ -1,18 +1,30 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_managment_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/widgets/device_managment_body.dart'; import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart'; -import 'package:syncrow_web/web_layout/web_scaffold.dart'; +import 'package:syncrow_web/pages/routiens/view/create_new_routine_view.dart'; +import 'package:syncrow_web/pages/routiens/view/routines_view.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; +import 'package:syncrow_web/web_layout/web_scaffold.dart'; class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout { const DeviceManagementPage({super.key}); @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => DeviceManagementBloc()..add(FetchDevices()), + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => DeviceManagementBloc()..add(FetchDevices()), + ), + BlocProvider( + create: (context) => SwitchTabsBloc()..add(const TriggerSwitchTabsEvent(false)), + ), + ], child: WebScaffold( appBarTitle: FittedBox( child: Text( @@ -20,26 +32,74 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout { style: Theme.of(context).textTheme.headlineLarge, ), ), + centerBody: BlocBuilder(builder: (context, state) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + TextButton( + style: TextButton.styleFrom( + backgroundColor: null, + ), + onPressed: () { + context.read().add(const TriggerSwitchTabsEvent(false)); + }, + child: Text( + 'Devices', + style: context.textTheme.titleMedium?.copyWith( + color: state is SelectedTabState && state.selectedTab == false + ? ColorsManager.whiteColors + : ColorsManager.grayColor, + fontWeight: + (state is SelectedTabState) && state.selectedTab == false ? FontWeight.w700 : FontWeight.w400, + ), + ), + ), + TextButton( + style: TextButton.styleFrom( + backgroundColor: null, + ), + onPressed: () { + context.read().add(const TriggerSwitchTabsEvent(true)); + }, + child: Text( + 'Routines', + style: context.textTheme.titleMedium?.copyWith( + color: (state is SelectedTabState) && state.selectedTab == true + ? ColorsManager.whiteColors + : ColorsManager.grayColor, + fontWeight: + (state is SelectedTabState) && state.selectedTab == true ? FontWeight.w700 : FontWeight.w400, + ), + ), + ), + ], + ); + }), rightBody: const NavigateHomeGridView(), - scaffoldBody: BlocBuilder( - builder: (context, state) { - if (state is DeviceManagementLoading) { - return const Center(child: CircularProgressIndicator()); - } else if (state is DeviceManagementLoaded || state is DeviceManagementFiltered) { - final devices = state is DeviceManagementLoaded - ? state.devices - : (state as DeviceManagementFiltered).filteredDevices; + scaffoldBody: BlocBuilder(builder: (context, state) { + if (state is SelectedTabState && state.selectedTab) { + return const RoutinesView(); + } + if (state is ShowCreateRoutineState && state.showCreateRoutine) { + return const CreateNewRoutineView(); + } - return DeviceManagementBody(devices: devices); - } else { - return const Center(child: Text('Error fetching Devices')); - } - }, - ), + return BlocBuilder( + builder: (context, deviceState) { + if (deviceState is DeviceManagementLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (deviceState is DeviceManagementLoaded || deviceState is DeviceManagementFiltered) { + final devices = + (deviceState as dynamic).devices ?? (deviceState as DeviceManagementFiltered).filteredDevices; + + return DeviceManagementBody(devices: devices); + } else { + return const Center(child: Text('Error fetching Devices')); + } + }, + ); + }), ), ); } } - - - diff --git a/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart b/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart index 12c66403..0788e08d 100644 --- a/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart +++ b/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart @@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/common/buttons/default_button.dart'; import 'package:syncrow_web/pages/common/custom_table.dart'; import 'package:syncrow_web/pages/common/filter/filter_widget.dart'; -import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_managment_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/widgets/device_search_filters.dart'; import 'package:syncrow_web/pages/device_managment/shared/device_batch_control_dialog.dart'; @@ -57,15 +57,12 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { 'Low Battery ($lowBatteryCount)', ]; - final buttonLabel = - (selectedDevices.length > 1) ? 'Batch Control' : 'Control'; + final buttonLabel = (selectedDevices.length > 1) ? 'Batch Control' : 'Control'; return Column( children: [ Container( - padding: isLargeScreenSize(context) - ? const EdgeInsets.all(30) - : const EdgeInsets.all(15), + padding: isLargeScreenSize(context) ? const EdgeInsets.all(30) : const EdgeInsets.all(15), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -74,9 +71,7 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { tabs: tabs, selectedIndex: selectedIndex, onTabChanged: (index) { - context - .read() - .add(SelectedFilterChanged(index)); + context.read().add(SelectedFilterChanged(index)); }, ), const SizedBox(height: 20), @@ -98,14 +93,11 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { ), ); } else if (selectedDevices.length > 1) { - final productTypes = selectedDevices - .map((device) => device.productType) - .toSet(); + final productTypes = selectedDevices.map((device) => device.productType).toSet(); if (productTypes.length == 1) { showDialog( context: context, - builder: (context) => - DeviceBatchControlDialog( + builder: (context) => DeviceBatchControlDialog( devices: selectedDevices, ), ); @@ -119,9 +111,7 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { textAlign: TextAlign.center, style: TextStyle( fontSize: 12, - color: isControlButtonEnabled - ? Colors.white - : Colors.grey, + color: isControlButtonEnabled ? Colors.white : Colors.grey, ), ), ), @@ -132,17 +122,13 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { ), Expanded( child: Padding( - padding: isLargeScreenSize(context) - ? const EdgeInsets.all(30) - : const EdgeInsets.all(15), + padding: isLargeScreenSize(context) ? const EdgeInsets.all(30) : const EdgeInsets.all(15), child: DynamicTable( withSelectAll: true, cellDecoration: containerDecoration, onRowSelected: (index, isSelected, row) { final selectedDevice = devicesToShow[index]; - context - .read() - .add(SelectDevice(selectedDevice)); + context.read().add(SelectDevice(selectedDevice)); }, withCheckBox: true, size: MediaQuery.of(context).size, @@ -160,44 +146,27 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { ], data: devicesToShow.map((device) { final combinedSpaceNames = device.spaces != null - ? device.spaces! - .map((space) => space.spaceName) - .join(' > ') + - (device.community != null - ? ' > ${device.community!.name}' - : '') - : (device.community != null - ? device.community!.name - : ''); + ? device.spaces!.map((space) => space.spaceName).join(' > ') + + (device.community != null ? ' > ${device.community!.name}' : '') + : (device.community != null ? device.community!.name : ''); return [ device.name ?? '', device.productName ?? '', device.uuid ?? '', - (device.spaces != null && device.spaces!.isNotEmpty) - ? device.spaces![0].spaceName - : '', + (device.spaces != null && device.spaces!.isNotEmpty) ? device.spaces![0].spaceName : '', combinedSpaceNames, - device.batteryLevel != null - ? '${device.batteryLevel}%' - : '-', - formatDateTime(DateTime.fromMillisecondsSinceEpoch( - (device.createTime ?? 0) * 1000)), + device.batteryLevel != null ? '${device.batteryLevel}%' : '-', + formatDateTime(DateTime.fromMillisecondsSinceEpoch((device.createTime ?? 0) * 1000)), device.online == true ? 'Online' : 'Offline', - formatDateTime(DateTime.fromMillisecondsSinceEpoch( - (device.updateTime ?? 0) * 1000)), + formatDateTime(DateTime.fromMillisecondsSinceEpoch((device.updateTime ?? 0) * 1000)), ]; }).toList(), onSelectionChanged: (selectedRows) { - context - .read() - .add(UpdateSelection(selectedRows)); + context.read().add(UpdateSelection(selectedRows)); }, - initialSelectedIds: context - .read() - .selectedDevices - .map((device) => device.uuid!) - .toList(), + initialSelectedIds: + context.read().selectedDevices.map((device) => device.uuid!).toList(), isEmpty: devicesToShow.isEmpty, ), ), diff --git a/lib/pages/device_managment/all_devices/widgets/device_search_filters.dart b/lib/pages/device_managment/all_devices/widgets/device_search_filters.dart index 71974156..d9e47aa6 100644 --- a/lib/pages/device_managment/all_devices/widgets/device_search_filters.dart +++ b/lib/pages/device_managment/all_devices/widgets/device_search_filters.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/common/text_field/custom_text_field.dart'; -import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_managment_bloc.dart'; import 'package:syncrow_web/pages/common/buttons/search_reset_buttons.dart'; +import 'package:syncrow_web/pages/common/text_field/custom_text_field.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; -import 'package:syncrow_web/utils/style.dart'; class DeviceSearchFilters extends StatefulWidget { const DeviceSearchFilters({super.key}); @@ -13,8 +12,7 @@ class DeviceSearchFilters extends StatefulWidget { State createState() => _DeviceSearchFiltersState(); } -class _DeviceSearchFiltersState extends State - with HelperResponsiveLayout { +class _DeviceSearchFiltersState extends State with HelperResponsiveLayout { final TextEditingController communityController = TextEditingController(); final TextEditingController unitNameController = TextEditingController(); final TextEditingController productNameController = TextEditingController(); @@ -36,8 +34,7 @@ class _DeviceSearchFiltersState extends State const SizedBox(width: 20), _buildSearchField("Space Name", unitNameController, 200), const SizedBox(width: 20), - _buildSearchField( - "Device Name / Product Name", productNameController, 300), + _buildSearchField("Device Name / Product Name", productNameController, 300), const SizedBox(width: 20), _buildSearchResetButtons(), ], @@ -62,8 +59,7 @@ class _DeviceSearchFiltersState extends State ); } - Widget _buildSearchField( - String title, TextEditingController controller, double width) { + Widget _buildSearchField(String title, TextEditingController controller, double width) { return Container( child: StatefulTextField( title: title, diff --git a/lib/pages/routiens/view/create_new_routine_view.dart b/lib/pages/routiens/view/create_new_routine_view.dart new file mode 100644 index 00000000..dc770946 --- /dev/null +++ b/lib/pages/routiens/view/create_new_routine_view.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class CreateNewRoutineView extends StatelessWidget { + const CreateNewRoutineView({super.key}); + + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} diff --git a/lib/pages/routiens/view/routines_view.dart b/lib/pages/routiens/view/routines_view.dart new file mode 100644 index 00000000..5a19db08 --- /dev/null +++ b/lib/pages/routiens/view/routines_view.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class RoutinesView extends StatelessWidget { + const RoutinesView({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text("Create New Routines", + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: ColorsManager.grayColor, + )), + SizedBox( + height: 200, + width: 150, + child: GestureDetector( + onTap: () { + BlocProvider.of(context).add( + const CreateNewRoutineViewEvent(true), + ); + }, + child: Card( + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + color: ColorsManager.whiteColors, + child: Center( + child: Container( + decoration: BoxDecoration( + color: ColorsManager.graysColor, + borderRadius: BorderRadius.circular(120), + border: Border.all(color: ColorsManager.greyColor, width: 2.0), + ), + height: 70, + width: 70, + child: Icon( + Icons.add, + color: ColorsManager.dialogBlueTitle, + size: 40, + ), + )), + ), + ), + ), + const Spacer(), + ], + ), + ); + } +} From 16dd95c8d1654300fadcb799b511c61008abcb16 Mon Sep 17 00:00:00 2001 From: ashrafzarkanisala Date: Sat, 16 Nov 2024 23:58:33 +0300 Subject: [PATCH 02/23] added devices --- assets/icons/routine/delay.svg | 24 ++ assets/icons/routine/map.svg | 17 ++ assets/icons/routine/notification.svg | 10 + assets/icons/routine/schedule.svg | 17 ++ assets/icons/routine/tab_to_run.svg | 9 + assets/icons/routine/weather.svg | 242 ++++++++++++++++++ .../common/text_field/custom_text_field.dart | 109 +++++--- .../all_devices/models/devices_model.dart | 136 +++++++--- .../view/device_managment_page.dart | 49 ++-- lib/pages/routiens/bloc/routine_bloc.dart | 13 + lib/pages/routiens/bloc/routine_event.dart | 4 + lib/pages/routiens/bloc/routine_state.dart | 6 + .../view/create_new_routine_view.dart | 74 +++++- .../conditions_routines_devices_view.dart | 106 ++++++++ lib/pages/routiens/widgets/dragable_card.dart | 63 +++++ .../widgets/routine_search_and_buttons.dart | 193 ++++++++++++++ .../widgets/routines_title_widget.dart | 38 +++ .../widgets/search_bar_condition_title.dart | 62 +++++ lib/utils/color_manager.dart | 2 +- lib/utils/constants/assets.dart | 18 ++ lib/utils/enum/device_types.dart | 6 +- lib/utils/style.dart | 32 ++- pubspec.yaml | 1 + 23 files changed, 1117 insertions(+), 114 deletions(-) create mode 100644 assets/icons/routine/delay.svg create mode 100644 assets/icons/routine/map.svg create mode 100644 assets/icons/routine/notification.svg create mode 100644 assets/icons/routine/schedule.svg create mode 100644 assets/icons/routine/tab_to_run.svg create mode 100644 assets/icons/routine/weather.svg create mode 100644 lib/pages/routiens/bloc/routine_bloc.dart create mode 100644 lib/pages/routiens/bloc/routine_event.dart create mode 100644 lib/pages/routiens/bloc/routine_state.dart create mode 100644 lib/pages/routiens/widgets/conditions_routines_devices_view.dart create mode 100644 lib/pages/routiens/widgets/dragable_card.dart create mode 100644 lib/pages/routiens/widgets/routine_search_and_buttons.dart create mode 100644 lib/pages/routiens/widgets/routines_title_widget.dart create mode 100644 lib/pages/routiens/widgets/search_bar_condition_title.dart diff --git a/assets/icons/routine/delay.svg b/assets/icons/routine/delay.svg new file mode 100644 index 00000000..49a8d31c --- /dev/null +++ b/assets/icons/routine/delay.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/routine/map.svg b/assets/icons/routine/map.svg new file mode 100644 index 00000000..595a0d8c --- /dev/null +++ b/assets/icons/routine/map.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/assets/icons/routine/notification.svg b/assets/icons/routine/notification.svg new file mode 100644 index 00000000..f196f466 --- /dev/null +++ b/assets/icons/routine/notification.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/routine/schedule.svg b/assets/icons/routine/schedule.svg new file mode 100644 index 00000000..423eb577 --- /dev/null +++ b/assets/icons/routine/schedule.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/assets/icons/routine/tab_to_run.svg b/assets/icons/routine/tab_to_run.svg new file mode 100644 index 00000000..c8660bb8 --- /dev/null +++ b/assets/icons/routine/tab_to_run.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/icons/routine/weather.svg b/assets/icons/routine/weather.svg new file mode 100644 index 00000000..49ed9408 --- /dev/null +++ b/assets/icons/routine/weather.svg @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/pages/common/text_field/custom_text_field.dart b/lib/pages/common/text_field/custom_text_field.dart index b695da4a..23f033b6 100644 --- a/lib/pages/common/text_field/custom_text_field.dart +++ b/lib/pages/common/text_field/custom_text_field.dart @@ -1,59 +1,97 @@ import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/style.dart'; class StatefulTextField extends StatefulWidget { - const StatefulTextField( - {super.key, - required this.title, - this.hintText = 'Please enter', - required this.width, - this.elevation = 0, - required this.controller, - this.onSubmitted}); + const StatefulTextField({ + super.key, + required this.title, + this.hintText = 'Please enter', + required this.width, + this.elevation, + required this.controller, + this.onSubmitted, + this.boxDecoration, + this.borderRadius, + this.height, + this.padding, + this.icon, + this.hintColor, + }); final String title; final String hintText; final double width; - final double elevation; + final double? elevation; final TextEditingController controller; final Function? onSubmitted; + final BoxDecoration? boxDecoration; + final double? borderRadius; + final double? height; + final double? padding; + final IconData? icon; + final Color? hintColor; @override State createState() => _StatefulTextFieldState(); } class _StatefulTextFieldState extends State { + @override + void dispose() { + widget.controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - return Container( - child: CustomTextField( - title: widget.title, - controller: widget.controller, - hintText: widget.hintText, - width: widget.width, - elevation: widget.elevation, - onSubmittedFun: widget.onSubmitted), + return CustomTextField( + title: widget.title, + controller: widget.controller, + hintText: widget.hintText, + width: widget.width, + elevation: widget.elevation, + onSubmittedFun: widget.onSubmitted, + boxDecoration: widget.boxDecoration, + borderRadius: widget.borderRadius, + height: widget.height, + padding: widget.padding, + icon: widget.icon, + hintColor: widget.hintColor, ); } } class CustomTextField extends StatelessWidget { - const CustomTextField( - {super.key, - required this.title, - required this.controller, - this.hintText = 'Please enter', - required this.width, - this.elevation = 0, - this.onSubmittedFun}); + const CustomTextField({ + super.key, + required this.title, + required this.controller, + this.hintText = 'Please enter', + required this.width, + this.elevation, + this.onSubmittedFun, + this.boxDecoration, + this.borderRadius, + this.height, + this.padding, + this.icon, + this.hintColor, + }); final String title; final TextEditingController controller; final String hintText; final double width; - final double elevation; + final double? elevation; final Function? onSubmittedFun; + final BoxDecoration? boxDecoration; + final double? borderRadius; + final double? height; + final double? padding; + final IconData? icon; + final Color? hintColor; @override Widget build(BuildContext context) { @@ -71,26 +109,21 @@ class CustomTextField extends StatelessWidget { ), const SizedBox(height: 8), Material( - elevation: elevation, - borderRadius: BorderRadius.circular(8), + elevation: elevation ?? 0, + borderRadius: BorderRadius.circular(borderRadius ?? 8), child: Container( width: width, - height: 45, - decoration: containerDecoration, - - // decoration: BoxDecoration( - // color: Colors.white, - // borderRadius: BorderRadius.circular(8), - // ), + height: height ?? 45, + decoration: boxDecoration ?? containerDecoration, child: TextFormField( controller: controller, style: const TextStyle(color: Colors.black), decoration: InputDecoration( hintText: hintText, - hintStyle: const TextStyle(fontSize: 12), - contentPadding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + hintStyle: TextStyle(fontSize: 12, color: hintColor ?? ColorsManager.blackColor), + contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: padding ?? 10), border: InputBorder.none, + suffixIcon: icon != null ? Icon(icon, color: ColorsManager.greyColor) : null, ), onFieldSubmitted: (_) { onSubmittedFun!(); 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 df80c3e7..50e588b0 100644 --- a/lib/pages/device_managment/all_devices/models/devices_model.dart +++ b/lib/pages/device_managment/all_devices/models/devices_model.dart @@ -3,6 +3,7 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/device_spa import 'package:syncrow_web/pages/device_managment/all_devices/models/room.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/unit.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/enum/device_types.dart'; class AllDevicesModel { /* @@ -100,12 +101,8 @@ class AllDevicesModel { this.spaces, }); AllDevicesModel.fromJson(Map json) { - room = (json['room'] != null && (json['room'] is Map)) - ? DevicesModelRoom.fromJson(json['room']) - : null; - unit = (json['unit'] != null && (json['unit'] is Map)) - ? DevicesModelUnit.fromJson(json['unit']) - : null; + room = (json['room'] != null && (json['room'] is Map)) ? DevicesModelRoom.fromJson(json['room']) : null; + unit = (json['unit'] != null && (json['unit'] is Map)) ? DevicesModelUnit.fromJson(json['unit']) : null; community = (json['community'] != null && (json['community'] is Map)) ? DeviceCommunityModel.fromJson(json['community']) : null; @@ -117,7 +114,7 @@ class AllDevicesModel { categoryName = json['categoryName']?.toString(); createTime = int.tryParse(json['createTime']?.toString() ?? ''); gatewayId = json['gatewayId']?.toString(); - icon = json['icon'] ?? _getDefaultIcon(productType); + icon = json['icon'] ?? getDefaultIcon(productType); ip = json['ip']?.toString(); lat = json['lat']?.toString(); localKey = json['localKey']?.toString(); @@ -134,38 +131,88 @@ class AllDevicesModel { batteryLevel = int.tryParse(json['battery']?.toString() ?? ''); productName = json['productName']?.toString(); if (json['spaces'] != null && json['spaces'] is List) { - spaces = (json['spaces'] as List) - .map((space) => DeviceSpaceModel.fromJson(space)) - .toList(); + spaces = (json['spaces'] as List).map((space) => DeviceSpaceModel.fromJson(space)).toList(); } } - String _getDefaultIcon(String? productType) { - switch (productType) { - case 'LightBulb': - return Assets.lightBulb; - case 'CeilingSensor': - case 'WallSensor': - return Assets.sensors; - case 'AC': - return Assets.ac; - case 'DoorLock': - return Assets.doorLock; - case 'Curtain': - return Assets.curtain; - case '3G': - case '2G': - case '1G': - return Assets.gangSwitch; - case 'Gateway': - return Assets.gateway; - case 'WH': - return Assets.blackLogo; - case 'DS': - return Assets.sensors; - default: - return Assets.logo; + String getDefaultIcon(String? productType) { + /* + AC +GD +3G +3G +GW +DL +WPS +CPS +AC +CPS +WPS +GW +AC +CUR +DS +1GT +2GT +3GT +1G +1G +2G +2G +DS +WH +1GT +2GT +3GT +GD +WL +WL +3G +CUR +GW +PC +PC +SOS + + */ + DeviceType type = devicesTypesMap[productType] ?? DeviceType.Other; + String tempIcon = ''; + if (type == DeviceType.LightBulb) { + tempIcon = Assets.lightBulb; + } else if (type == DeviceType.CeilingSensor || type == DeviceType.WallSensor) { + tempIcon = Assets.sensors; + } else if (type == DeviceType.AC) { + tempIcon = Assets.ac; + } else if (type == DeviceType.DoorLock) { + tempIcon = Assets.doorLock; + } else if (type == DeviceType.Curtain) { + tempIcon = Assets.curtain; + } else if (type == DeviceType.ThreeGang) { + tempIcon = Assets.gangSwitch; + } else if (type == DeviceType.Gateway) { + tempIcon = Assets.gateway; + } else if (type == DeviceType.OneGang) { + tempIcon = Assets.oneGang; + } else if (type == DeviceType.TwoGang) { + tempIcon = Assets.twoGang; + } else if (type == DeviceType.WH) { + tempIcon = Assets.waterHeater; + } else if (type == DeviceType.DS) { + // tempIcon = Assets.mainDoor; + } else if (type == DeviceType.OneTouch) { + // tempIcon = Assets.oneGang; + } else if (type == DeviceType.TowTouch) { + // tempIcon = Assets.twoGang; + } else if (type == DeviceType.GarageDoor) { + //tempIcon = Assets.; + } else if (type == DeviceType.ThreeTouch) { + // tempIcon = Assets.gang3touch; + } else if (type == DeviceType.WaterLeak) { + tempIcon = Assets.waterLeakNormal; + } else { + tempIcon = Assets.logoHorizontal; } + return tempIcon; } Map toJson() { @@ -271,4 +318,23 @@ class AllDevicesModel { productName.hashCode ^ batteryLevel.hashCode; } + + Map devicesTypesMap = { + "AC": DeviceType.AC, + "GW": DeviceType.Gateway, + "CPS": DeviceType.CeilingSensor, + "DL": DeviceType.DoorLock, + "WPS": DeviceType.WallSensor, + "3G": DeviceType.ThreeGang, + "2G": DeviceType.TwoGang, + "1G": DeviceType.OneGang, + "CUR": DeviceType.Curtain, + "WH": DeviceType.WH, + "DS": DeviceType.DS, + "1GT": DeviceType.OneTouch, + "2GT": DeviceType.TowTouch, + "3GT": DeviceType.ThreeTouch, + "GD": DeviceType.GarageDoor, + "WL": DeviceType.WaterLeak, + }; } diff --git a/lib/pages/device_managment/all_devices/view/device_managment_page.dart b/lib/pages/device_managment/all_devices/view/device_managment_page.dart index d163a718..e0661b50 100644 --- a/lib/pages/device_managment/all_devices/view/device_managment_page.dart +++ b/lib/pages/device_managment/all_devices/view/device_managment_page.dart @@ -2,10 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/all_devices/widgets/device_managment_body.dart'; import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart'; import 'package:syncrow_web/pages/routiens/view/create_new_routine_view.dart'; -import 'package:syncrow_web/pages/routiens/view/routines_view.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -76,29 +74,30 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout { ); }), rightBody: const NavigateHomeGridView(), - scaffoldBody: BlocBuilder(builder: (context, state) { - if (state is SelectedTabState && state.selectedTab) { - return const RoutinesView(); - } - if (state is ShowCreateRoutineState && state.showCreateRoutine) { - return const CreateNewRoutineView(); - } - - return BlocBuilder( - builder: (context, deviceState) { - if (deviceState is DeviceManagementLoading) { - return const Center(child: CircularProgressIndicator()); - } else if (deviceState is DeviceManagementLoaded || deviceState is DeviceManagementFiltered) { - final devices = - (deviceState as dynamic).devices ?? (deviceState as DeviceManagementFiltered).filteredDevices; - - return DeviceManagementBody(devices: devices); - } else { - return const Center(child: Text('Error fetching Devices')); - } - }, - ); - }), + scaffoldBody: CreateNewRoutineView(), + // BlocBuilder(builder: (context, state) { + // if (state is SelectedTabState && state.selectedTab) { + // return const RoutinesView(); + // } + // if (state is ShowCreateRoutineState && state.showCreateRoutine) { + // return const CreateNewRoutineView(); + // } + // + // return BlocBuilder( + // builder: (context, deviceState) { + // if (deviceState is DeviceManagementLoading) { + // return const Center(child: CircularProgressIndicator()); + // } else if (deviceState is DeviceManagementLoaded || deviceState is DeviceManagementFiltered) { + // final devices = + // (deviceState as dynamic).devices ?? (deviceState as DeviceManagementFiltered).filteredDevices; + // + // return DeviceManagementBody(devices: devices); + // } else { + // return const Center(child: Text('Error fetching Devices')); + // } + // }, + // ); + // }), ), ); } diff --git a/lib/pages/routiens/bloc/routine_bloc.dart b/lib/pages/routiens/bloc/routine_bloc.dart new file mode 100644 index 00000000..c0b2b5c5 --- /dev/null +++ b/lib/pages/routiens/bloc/routine_bloc.dart @@ -0,0 +1,13 @@ +import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; + +part 'routine_event.dart'; +part 'routine_state.dart'; + +class RoutineBloc extends Bloc { + RoutineBloc() : super(RoutineInitial()) { + on((event, emit) { + // TODO: implement event handler + }); + } +} diff --git a/lib/pages/routiens/bloc/routine_event.dart b/lib/pages/routiens/bloc/routine_event.dart new file mode 100644 index 00000000..52ca0690 --- /dev/null +++ b/lib/pages/routiens/bloc/routine_event.dart @@ -0,0 +1,4 @@ +part of 'routine_bloc.dart'; + +@immutable +sealed class RoutineEvent {} diff --git a/lib/pages/routiens/bloc/routine_state.dart b/lib/pages/routiens/bloc/routine_state.dart new file mode 100644 index 00000000..aaf5cb97 --- /dev/null +++ b/lib/pages/routiens/bloc/routine_state.dart @@ -0,0 +1,6 @@ +part of 'routine_bloc.dart'; + +@immutable +sealed class RoutineState {} + +final class RoutineInitial extends RoutineState {} diff --git a/lib/pages/routiens/view/create_new_routine_view.dart b/lib/pages/routiens/view/create_new_routine_view.dart index dc770946..764eb374 100644 --- a/lib/pages/routiens/view/create_new_routine_view.dart +++ b/lib/pages/routiens/view/create_new_routine_view.dart @@ -1,10 +1,82 @@ import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/routiens/widgets/conditions_routines_devices_view.dart'; +import 'package:syncrow_web/pages/routiens/widgets/routine_search_and_buttons.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; class CreateNewRoutineView extends StatelessWidget { const CreateNewRoutineView({super.key}); @override Widget build(BuildContext context) { - return const Placeholder(); + return Container( + alignment: Alignment.topLeft, + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const RoutineSearchAndButtons(), + const SizedBox(height: 20), + Flexible( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: Card( + child: Container( + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(15), + ), + child: const ConditionsRoutinesDevicesView()), + ), + ), + const SizedBox( + width: 10, + ), + Expanded( + child: Column( + children: [ + Expanded( + child: Card( + margin: EdgeInsets.zero, + child: Container( + decoration: const BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(15), + topRight: Radius.circular(15), + ), + ), + ), + ), + ), + Container( + height: 2, + width: double.infinity, + color: ColorsManager.dialogBlueTitle, + ), + Expanded( + child: Card( + margin: EdgeInsets.zero, + child: Container( + decoration: const BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(15), + bottomRight: Radius.circular(15), + ), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); } } diff --git a/lib/pages/routiens/widgets/conditions_routines_devices_view.dart b/lib/pages/routiens/widgets/conditions_routines_devices_view.dart new file mode 100644 index 00000000..6e6ef4a2 --- /dev/null +++ b/lib/pages/routiens/widgets/conditions_routines_devices_view.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; +import 'package:syncrow_web/pages/routiens/widgets/routines_title_widget.dart'; +import 'package:syncrow_web/pages/routiens/widgets/search_bar_condition_title.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class ConditionsRoutinesDevicesView extends StatelessWidget { + const ConditionsRoutinesDevicesView({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 8.0), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ConditionTitleAndSearchBar(), + const SizedBox( + height: 10, + ), + const Wrap( + spacing: 10, + runSpacing: 10, + children: [ + DraggableCard( + imagePath: Assets.tabToRun, + title: 'Tab to run', + ), + DraggableCard( + imagePath: Assets.map, + title: 'Location', + ), + DraggableCard( + imagePath: Assets.weather, + title: 'Weather', + ), + DraggableCard( + imagePath: Assets.schedule, + title: 'Schedule', + ), + ], + ), + const SizedBox( + height: 10, + ), + const TitleRoutine( + title: 'Conditions', + subtitle: '(THEN)', + ), + const Wrap( + spacing: 10, + runSpacing: 10, + children: [ + DraggableCard( + imagePath: Assets.notification, + title: 'Send Notification', + ), + DraggableCard( + imagePath: Assets.delay, + title: 'Delay the action', + ), + ], + ), + const SizedBox( + height: 10, + ), + const TitleRoutine( + title: 'Routines', + subtitle: '(THEN)', + ), + const TitleRoutine( + title: 'Devices', + subtitle: '', + ), + BlocProvider( + create: (context) => DeviceManagementBloc() + ..add( + FetchDevices(), + ), + child: BlocBuilder( + builder: (context, state) { + if (state is DeviceManagementLoaded) { + return Wrap( + spacing: 10, + runSpacing: 10, + children: state.devices + .map((device) => DraggableCard( + imagePath: device.getDefaultIcon(device.productType), + title: device.name ?? '', + )) + .toList(), + ); + } + return const Center(child: CircularProgressIndicator()); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/routiens/widgets/dragable_card.dart b/lib/pages/routiens/widgets/dragable_card.dart new file mode 100644 index 00000000..490dd570 --- /dev/null +++ b/lib/pages/routiens/widgets/dragable_card.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class DraggableCard extends StatelessWidget { + const DraggableCard({ + super.key, + required this.imagePath, + required this.title, + this.titleColor, + }); + + final String imagePath; + final String title; + final Color? titleColor; + + @override + Widget build(BuildContext context) { + return Card( + color: ColorsManager.whiteColors, + child: SizedBox( + height: 123, + width: 90, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + height: 50, + width: 50, + decoration: BoxDecoration( + color: ColorsManager.CircleImageBackground, + borderRadius: BorderRadius.circular(90), + border: Border.all( + color: ColorsManager.graysColor, + ), + ), + padding: const EdgeInsets.all(8), + child: imagePath.contains('.svg') + ? SvgPicture.asset( + imagePath, + ) + : Image.network(imagePath), + ), + const SizedBox( + height: 8, + ), + Text( + title, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + maxLines: 2, + style: context.textTheme.bodySmall?.copyWith( + color: titleColor ?? ColorsManager.blackColor, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/routiens/widgets/routine_search_and_buttons.dart b/lib/pages/routiens/widgets/routine_search_and_buttons.dart new file mode 100644 index 00000000..1acce727 --- /dev/null +++ b/lib/pages/routiens/widgets/routine_search_and_buttons.dart @@ -0,0 +1,193 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/common/text_field/custom_text_field.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class RoutineSearchAndButtons extends StatelessWidget { + const RoutineSearchAndButtons({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Wrap( + runSpacing: 16, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: Wrap( + spacing: 12, + runSpacing: 12, + crossAxisAlignment: WrapCrossAlignment.end, + children: [ + ConstrainedBox( + constraints: + BoxConstraints(maxWidth: constraints.maxWidth > 700 ? 450 : constraints.maxWidth - 32), + child: StatefulTextField( + title: 'Routine Name', + height: 40, + controller: TextEditingController(), + hintText: 'Please enter the name', + boxDecoration: containerWhiteDecoration, + elevation: 0, + borderRadius: 15, + width: 450, + ), + ), + (constraints.maxWidth <= 1000) + ? const SizedBox() + : SizedBox( + height: 40, + width: 200, + child: Center( + child: DefaultButton( + onPressed: () {}, + borderRadius: 15, + elevation: 0, + borderColor: ColorsManager.greyColor, + backgroundColor: ColorsManager.boxColor, + child: const Text( + 'Settings', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + color: ColorsManager.primaryColor, + ), + ), + ), + ), + ), + ], + ), + ), + if (constraints.maxWidth > 1000) + Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 40, + width: 200, + child: Center( + child: DefaultButton( + onPressed: () {}, + borderRadius: 15, + elevation: 0, + borderColor: ColorsManager.greyColor, + backgroundColor: ColorsManager.boxColor, + child: const Text( + 'Cancel', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + color: ColorsManager.blackColor, + ), + ), + ), + ), + ), + const SizedBox(width: 12), + SizedBox( + height: 40, + width: 200, + child: Center( + child: DefaultButton( + onPressed: () {}, + borderRadius: 15, + elevation: 0, + backgroundColor: ColorsManager.primaryColor, + child: const Text( + 'Save', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + color: ColorsManager.whiteColors, + ), + ), + ), + ), + ), + ], + ), + ], + ), + if (constraints.maxWidth <= 1000) + Wrap( + runSpacing: 12, + children: [ + SizedBox( + height: 40, + width: 200, + child: Center( + child: DefaultButton( + onPressed: () {}, + borderRadius: 15, + elevation: 0, + borderColor: ColorsManager.greyColor, + backgroundColor: ColorsManager.boxColor, + child: const Text( + 'Settings', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + color: ColorsManager.primaryColor, + ), + ), + ), + ), + ), + const SizedBox(width: 12), + SizedBox( + height: 40, + width: 200, + child: Center( + child: DefaultButton( + onPressed: () {}, + borderRadius: 15, + elevation: 0, + borderColor: ColorsManager.greyColor, + backgroundColor: ColorsManager.boxColor, + child: const Text( + 'Cancel', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + color: ColorsManager.blackColor, + ), + ), + ), + ), + ), + const SizedBox(width: 12), + SizedBox( + height: 40, + width: 200, + child: Center( + child: DefaultButton( + onPressed: () {}, + borderRadius: 15, + elevation: 0, + backgroundColor: ColorsManager.primaryColor, + child: const Text( + 'Save', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + color: ColorsManager.whiteColors, + ), + ), + ), + ), + ), + ], + ), + ], + ); + }, + ); + } +} diff --git a/lib/pages/routiens/widgets/routines_title_widget.dart b/lib/pages/routiens/widgets/routines_title_widget.dart new file mode 100644 index 00000000..7d2a272f --- /dev/null +++ b/lib/pages/routiens/widgets/routines_title_widget.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class TitleRoutine extends StatelessWidget { + const TitleRoutine({ + super.key, + required this.title, + required this.subtitle, + }); + + final String title; + final String subtitle; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + title, + style: context.textTheme.titleLarge?.copyWith( + color: ColorsManager.greyColor, + ), + ), + const SizedBox( + width: 4, + ), + Text( + subtitle, + style: context.textTheme.titleLarge?.copyWith( + color: ColorsManager.greyColor, + ), + ), + ], + ); + } +} diff --git a/lib/pages/routiens/widgets/search_bar_condition_title.dart b/lib/pages/routiens/widgets/search_bar_condition_title.dart new file mode 100644 index 00000000..40d6b575 --- /dev/null +++ b/lib/pages/routiens/widgets/search_bar_condition_title.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/common/text_field/custom_text_field.dart'; +import 'package:syncrow_web/pages/routiens/widgets/routines_title_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +class ConditionTitleAndSearchBar extends StatelessWidget with HelperResponsiveLayout { + const ConditionTitleAndSearchBar({ + super.key, + }); + + @override + Widget build(BuildContext context) { + final isMedium = isMediumScreenSize(context); + final isSmall = isSmallScreenSize(context); + return isMedium || isSmall + ? Wrap( + spacing: 10, + runSpacing: 10, + children: [ + const TitleRoutine(title: 'Conditions', subtitle: '(IF)'), + StatefulTextField( + title: '', + width: 250, + height: 40, + hintText: 'Search', + elevation: 0, + borderRadius: 15, + icon: Icons.search, + hintColor: ColorsManager.grayColor, + boxDecoration: BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.circular(15), + ), + controller: TextEditingController(), + ), + ], + ) + : Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + const TitleRoutine(title: 'Conditions', subtitle: '(IF)'), + StatefulTextField( + title: '', + width: 250, + height: 40, + hintText: 'Search', + elevation: 0, + borderRadius: 15, + icon: Icons.search, + hintColor: ColorsManager.grayColor, + boxDecoration: BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.circular(15), + ), + controller: TextEditingController(), + ), + ], + ); + } +} diff --git a/lib/utils/color_manager.dart b/lib/utils/color_manager.dart index 481fb2fa..140ed370 100644 --- a/lib/utils/color_manager.dart +++ b/lib/utils/color_manager.dart @@ -41,6 +41,6 @@ abstract class ColorsManager { static const Color blue4 = Color(0xFF001E7E); static const Color textGreen = Color(0xFF008905); static const Color yaGreen = Color(0xFFFFBF44); + static const Color CircleImageBackground = Color(0xFFF4F4F4); } //background: #background: #5D5D5D; - diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index dad9b261..2c0c526f 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -192,4 +192,22 @@ class Assets { //assets/icons/sos_normal.svg static const String sosNormal = 'assets/icons/sos_normal.svg'; + + //assets/icons/routine/tab_to_run.svg + static const String tabToRun = 'assets/icons/routine/tab_to_run.svg'; + + //assets/icons/routine/schedule.svg + static const String schedule = 'assets/icons/routine/schedule.svg'; + + //assets/icons/routine/map.svg + static const String map = 'assets/icons/routine/map.svg'; + + //assets/icons/routine/weather.svg + static const String weather = 'assets/icons/routine/weather.svg'; + + //assets/icons/routine/notification.svg + static const String notification = 'assets/icons/routine/notification.svg'; + + //assets/icons/routine/delay.svg + static const String delay = 'assets/icons/routine/delay.svg'; } diff --git a/lib/utils/enum/device_types.dart b/lib/utils/enum/device_types.dart index 2b1ce8a5..7050f13a 100644 --- a/lib/utils/enum/device_types.dart +++ b/lib/utils/enum/device_types.dart @@ -11,9 +11,13 @@ enum DeviceType { CeilingSensor, WallSensor, WH, - DoorSensor, + DS, + OneTouch, + TowTouch, + ThreeTouch, GarageDoor, WaterLeak, + DoorSensor, Other, } /* diff --git a/lib/utils/style.dart b/lib/utils/style.dart index 145cf8a1..04108333 100644 --- a/lib/utils/style.dart +++ b/lib/utils/style.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; + import 'color_manager.dart'; -InputDecoration? textBoxDecoration({bool suffixIcon = false}) => - InputDecoration( +InputDecoration? textBoxDecoration({bool suffixIcon = false}) => InputDecoration( focusColor: ColorsManager.grayColor, suffixIcon: suffixIcon ? const Icon(Icons.search) : null, hintText: 'Search', @@ -30,14 +30,20 @@ InputDecoration? textBoxDecoration({bool suffixIcon = false}) => ), ); -BoxDecoration containerDecoration = BoxDecoration( - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.3), - spreadRadius: 2, - blurRadius: 4, - offset: const Offset(0, 5), // changes position of shadow - ), - ], - color: ColorsManager.boxColor, - borderRadius: const BorderRadius.all(Radius.circular(10))); +BoxDecoration containerDecoration = BoxDecoration(boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.3), + spreadRadius: 2, + blurRadius: 4, + offset: const Offset(0, 5), // changes position of shadow + ), +], color: ColorsManager.boxColor, borderRadius: const BorderRadius.all(Radius.circular(10))); + +BoxDecoration containerWhiteDecoration = BoxDecoration(boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.3), + spreadRadius: 2, + blurRadius: 4, + offset: const Offset(0, 5), // changes position of shadow + ), +], color: ColorsManager.whiteColors, borderRadius: const BorderRadius.all(Radius.circular(15))); diff --git a/pubspec.yaml b/pubspec.yaml index 7742e4da..9e35e58f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -76,6 +76,7 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - assets/icons/automation_functions/ + - assets/icons/routine/ - assets/icons/ - assets/images/ - assets/ From 458ec3976aa0c5d7adde1d551ad377b6797f618c Mon Sep 17 00:00:00 2001 From: ashrafzarkanisala Date: Sun, 17 Nov 2024 01:11:27 +0300 Subject: [PATCH 03/23] push dragging --- lib/pages/routiens/bloc/routine_bloc.dart | 19 ++- lib/pages/routiens/bloc/routine_event.dart | 26 +++- lib/pages/routiens/bloc/routine_state.dart | 24 +++- .../view/create_new_routine_view.dart | 126 ++++++++++-------- lib/pages/routiens/widgets/dragable_card.dart | 25 ++++ lib/pages/routiens/widgets/if_container.dart | 47 +++++++ .../routiens/widgets/then_container.dart | 47 +++++++ 7 files changed, 247 insertions(+), 67 deletions(-) create mode 100644 lib/pages/routiens/widgets/if_container.dart create mode 100644 lib/pages/routiens/widgets/then_container.dart diff --git a/lib/pages/routiens/bloc/routine_bloc.dart b/lib/pages/routiens/bloc/routine_bloc.dart index c0b2b5c5..86da3211 100644 --- a/lib/pages/routiens/bloc/routine_bloc.dart +++ b/lib/pages/routiens/bloc/routine_bloc.dart @@ -1,13 +1,22 @@ import 'package:bloc/bloc.dart'; -import 'package:meta/meta.dart'; +import 'package:equatable/equatable.dart'; part 'routine_event.dart'; part 'routine_state.dart'; class RoutineBloc extends Bloc { - RoutineBloc() : super(RoutineInitial()) { - on((event, emit) { - // TODO: implement event handler - }); + RoutineBloc() : super(const RoutineState()) { + on(_onAddToIfContainer); + on(_onAddToThenContainer); + } + + void _onAddToIfContainer(AddToIfContainer event, Emitter emit) { + final updatedIfItems = List>.from(state.ifItems)..add(event.item); + emit(state.copyWith(ifItems: updatedIfItems)); + } + + void _onAddToThenContainer(AddToThenContainer event, Emitter emit) { + final updatedThenItems = List>.from(state.thenItems)..add(event.item); + emit(state.copyWith(thenItems: updatedThenItems)); } } diff --git a/lib/pages/routiens/bloc/routine_event.dart b/lib/pages/routiens/bloc/routine_event.dart index 52ca0690..6570d637 100644 --- a/lib/pages/routiens/bloc/routine_event.dart +++ b/lib/pages/routiens/bloc/routine_event.dart @@ -1,4 +1,26 @@ part of 'routine_bloc.dart'; -@immutable -sealed class RoutineEvent {} +abstract class RoutineEvent extends Equatable { + const RoutineEvent(); + + @override + List get props => []; +} + +class AddToIfContainer extends RoutineEvent { + final Map item; + + const AddToIfContainer(this.item); + + @override + List get props => [item]; +} + +class AddToThenContainer extends RoutineEvent { + final Map item; + + const AddToThenContainer(this.item); + + @override + List get props => [item]; +} diff --git a/lib/pages/routiens/bloc/routine_state.dart b/lib/pages/routiens/bloc/routine_state.dart index aaf5cb97..3dbd5b9f 100644 --- a/lib/pages/routiens/bloc/routine_state.dart +++ b/lib/pages/routiens/bloc/routine_state.dart @@ -1,6 +1,24 @@ part of 'routine_bloc.dart'; -@immutable -sealed class RoutineState {} +class RoutineState extends Equatable { + final List> ifItems; + final List> thenItems; -final class RoutineInitial extends RoutineState {} + const RoutineState({ + this.ifItems = const [], + this.thenItems = const [], + }); + + RoutineState copyWith({ + List>? ifItems, + List>? thenItems, + }) { + return RoutineState( + ifItems: ifItems ?? this.ifItems, + thenItems: thenItems ?? this.thenItems, + ); + } + + @override + List get props => [ifItems, thenItems]; +} diff --git a/lib/pages/routiens/view/create_new_routine_view.dart b/lib/pages/routiens/view/create_new_routine_view.dart index 764eb374..99b4df71 100644 --- a/lib/pages/routiens/view/create_new_routine_view.dart +++ b/lib/pages/routiens/view/create_new_routine_view.dart @@ -1,6 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/widgets/conditions_routines_devices_view.dart'; +import 'package:syncrow_web/pages/routiens/widgets/if_container.dart'; import 'package:syncrow_web/pages/routiens/widgets/routine_search_and_buttons.dart'; +import 'package:syncrow_web/pages/routiens/widgets/then_container.dart'; import 'package:syncrow_web/utils/color_manager.dart'; class CreateNewRoutineView extends StatelessWidget { @@ -8,74 +12,82 @@ class CreateNewRoutineView extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - alignment: Alignment.topLeft, - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const RoutineSearchAndButtons(), - const SizedBox(height: 20), - Flexible( - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: Card( - child: Container( - decoration: BoxDecoration( - color: ColorsManager.whiteColors, - borderRadius: BorderRadius.circular(15), - ), - child: const ConditionsRoutinesDevicesView()), + return BlocProvider( + create: (context) => RoutineBloc(), + child: Container( + alignment: Alignment.topLeft, + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const RoutineSearchAndButtons(), + const SizedBox(height: 20), + Flexible( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: Card( + child: Container( + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(15), + ), + child: const ConditionsRoutinesDevicesView()), + ), ), - ), - const SizedBox( - width: 10, - ), - Expanded( - child: Column( - children: [ - Expanded( - child: Card( - margin: EdgeInsets.zero, - child: Container( - decoration: const BoxDecoration( - color: ColorsManager.whiteColors, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(15), - topRight: Radius.circular(15), + const SizedBox( + width: 10, + ), + Expanded( + child: Column( + children: [ + /// IF Container + Expanded( + child: Card( + margin: EdgeInsets.zero, + child: Container( + decoration: const BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(15), + topRight: Radius.circular(15), + ), ), + child: const IfContainer(), ), ), ), - ), - Container( - height: 2, - width: double.infinity, - color: ColorsManager.dialogBlueTitle, - ), - Expanded( - child: Card( - margin: EdgeInsets.zero, - child: Container( - decoration: const BoxDecoration( - color: ColorsManager.boxColor, - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(15), - bottomRight: Radius.circular(15), + Container( + height: 2, + width: double.infinity, + color: ColorsManager.dialogBlueTitle, + ), + + /// THEN Container + Expanded( + child: Card( + margin: EdgeInsets.zero, + child: Container( + decoration: const BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(15), + bottomRight: Radius.circular(15), + ), ), + child: const ThenContainer(), ), ), ), - ), - ], + ], + ), ), - ), - ], + ], + ), ), - ), - ], + ], + ), ), ); } diff --git a/lib/pages/routiens/widgets/dragable_card.dart b/lib/pages/routiens/widgets/dragable_card.dart index 490dd570..1724e04c 100644 --- a/lib/pages/routiens/widgets/dragable_card.dart +++ b/lib/pages/routiens/widgets/dragable_card.dart @@ -9,14 +9,28 @@ class DraggableCard extends StatelessWidget { required this.imagePath, required this.title, this.titleColor, + this.isDragged = false, }); final String imagePath; final String title; final Color? titleColor; + final bool isDragged; @override Widget build(BuildContext context) { + return Draggable>( + data: {'key': UniqueKey().toString(), 'imagePath': imagePath, 'title': title}, + feedback: Transform.rotate( + angle: -0.1, + child: _buildCardContent(context), + ), + childWhenDragging: _buildGreyContainer(), + child: isDragged ? _buildGreyContainer() : _buildCardContent(context), + ); + } + + Widget _buildCardContent(BuildContext context) { return Card( color: ColorsManager.whiteColors, child: SizedBox( @@ -60,4 +74,15 @@ class DraggableCard extends StatelessWidget { ), ); } + + Widget _buildGreyContainer() { + return Container( + height: 123, + width: 90, + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(20), + ), + ); + } } diff --git a/lib/pages/routiens/widgets/if_container.dart b/lib/pages/routiens/widgets/if_container.dart new file mode 100644 index 00000000..069f77b4 --- /dev/null +++ b/lib/pages/routiens/widgets/if_container.dart @@ -0,0 +1,47 @@ +// lib/pages/routiens/widgets/if_container.dart + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; + +class IfContainer extends StatelessWidget { + const IfContainer({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return DragTarget>( + builder: (context, candidateData, rejectedData) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('IF', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const SizedBox(height: 16), + Wrap( + spacing: 8, + runSpacing: 8, + children: state.ifItems + .map((item) => DraggableCard( + key: Key(item['key']!), + imagePath: item['imagePath']!, + title: item['title']!, + )) + .toList(), + ), + ], + ), + ); + }, + onAccept: (data) { + context.read().add(AddToIfContainer(data)); + }, + ); + }, + ); + } +} diff --git a/lib/pages/routiens/widgets/then_container.dart b/lib/pages/routiens/widgets/then_container.dart new file mode 100644 index 00000000..bc72bd9f --- /dev/null +++ b/lib/pages/routiens/widgets/then_container.dart @@ -0,0 +1,47 @@ +// lib/pages/routiens/widgets/then_container.dart + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; + +class ThenContainer extends StatelessWidget { + const ThenContainer({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return DragTarget>( + builder: (context, candidateData, rejectedData) { + return Container( + padding: const EdgeInsets.all(16), + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('THEN', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const SizedBox(height: 16), + Wrap( + spacing: 8, + runSpacing: 8, + children: state.thenItems + .map((item) => DraggableCard( + key: Key(item['key']!), + imagePath: item['imagePath']!, + title: item['title']!, + )) + .toList(), + ), + ], + ), + ); + }, + onAccept: (data) { + context.read().add(AddToThenContainer(data)); + }, + ); + }, + ); + } +} From 54143b3ba964779d56d0e41e248011b6c640b97f Mon Sep 17 00:00:00 2001 From: ashrafzarkanisala Date: Mon, 18 Nov 2024 01:13:28 +0300 Subject: [PATCH 04/23] push get scenes and automation --- lib/pages/routiens/bloc/routine_bloc.dart | 40 ++++ lib/pages/routiens/bloc/routine_event.dart | 18 ++ lib/pages/routiens/bloc/routine_state.dart | 27 ++- lib/pages/routiens/models/routine_model.dart | 30 +++ .../conditions_routines_devices_view.dart | 39 ++-- lib/pages/routiens/widgets/dragable_card.dart | 18 +- .../routiens/widgets/routine_devices.dart | 44 ++++ .../widgets/scenes_and_automations.dart | 42 ++++ lib/services/routines_api.dart | 216 ++++++++++++++++++ lib/utils/constants/api_const.dart | 26 +-- 10 files changed, 452 insertions(+), 48 deletions(-) create mode 100644 lib/pages/routiens/models/routine_model.dart create mode 100644 lib/pages/routiens/widgets/routine_devices.dart create mode 100644 lib/pages/routiens/widgets/scenes_and_automations.dart create mode 100644 lib/services/routines_api.dart diff --git a/lib/pages/routiens/bloc/routine_bloc.dart b/lib/pages/routiens/bloc/routine_bloc.dart index 86da3211..a7fd7228 100644 --- a/lib/pages/routiens/bloc/routine_bloc.dart +++ b/lib/pages/routiens/bloc/routine_bloc.dart @@ -1,13 +1,19 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/routiens/models/routine_model.dart'; +import 'package:syncrow_web/services/routines_api.dart'; part 'routine_event.dart'; part 'routine_state.dart'; +const spaceId = '25c96044-fadf-44bb-93c7-3c079e527ce6'; + class RoutineBloc extends Bloc { RoutineBloc() : super(const RoutineState()) { on(_onAddToIfContainer); on(_onAddToThenContainer); + on(_onLoadScenes); + on(_onLoadAutomation); } void _onAddToIfContainer(AddToIfContainer event, Emitter emit) { @@ -19,4 +25,38 @@ class RoutineBloc extends Bloc { final updatedThenItems = List>.from(state.thenItems)..add(event.item); emit(state.copyWith(thenItems: updatedThenItems)); } + + Future _onLoadScenes(LoadScenes event, Emitter emit) async { + emit(state.copyWith(isLoading: true, errorMessage: null)); + + try { + final scenes = await SceneApi.getScenesByUnitId(event.unitId); + emit(state.copyWith( + scenes: scenes, + isLoading: false, + )); + } catch (e) { + emit(state.copyWith( + isLoading: false, + errorMessage: 'Something went wrong', + )); + } + } + + Future _onLoadAutomation(LoadAutomation event, Emitter emit) async { + emit(state.copyWith(isLoading: true, errorMessage: null)); + + try { + final automations = await SceneApi.getAutomationByUnitId(event.unitId); + emit(state.copyWith( + automations: automations, + isLoading: false, + )); + } catch (e) { + emit(state.copyWith( + isLoading: false, + errorMessage: 'Something went wrong', + )); + } + } } diff --git a/lib/pages/routiens/bloc/routine_event.dart b/lib/pages/routiens/bloc/routine_event.dart index 6570d637..6dcacf9c 100644 --- a/lib/pages/routiens/bloc/routine_event.dart +++ b/lib/pages/routiens/bloc/routine_event.dart @@ -24,3 +24,21 @@ class AddToThenContainer extends RoutineEvent { @override List get props => [item]; } + +class LoadScenes extends RoutineEvent { + final String unitId; + + const LoadScenes(this.unitId); + + @override + List get props => [unitId]; +} + +class LoadAutomation extends RoutineEvent { + final String unitId; + + const LoadAutomation(this.unitId); + + @override + List get props => [unitId]; +} diff --git a/lib/pages/routiens/bloc/routine_state.dart b/lib/pages/routiens/bloc/routine_state.dart index 3dbd5b9f..8f715428 100644 --- a/lib/pages/routiens/bloc/routine_state.dart +++ b/lib/pages/routiens/bloc/routine_state.dart @@ -3,22 +3,47 @@ part of 'routine_bloc.dart'; class RoutineState extends Equatable { final List> ifItems; final List> thenItems; + final List> availableCards; + final List scenes; + final List automations; + final bool isLoading; + final String? errorMessage; const RoutineState({ this.ifItems = const [], this.thenItems = const [], + this.availableCards = const [], + this.scenes = const [], + this.automations = const [], + this.isLoading = false, + this.errorMessage, }); RoutineState copyWith({ List>? ifItems, List>? thenItems, + List? scenes, + List? automations, + bool? isLoading, + String? errorMessage, }) { return RoutineState( ifItems: ifItems ?? this.ifItems, thenItems: thenItems ?? this.thenItems, + scenes: scenes ?? this.scenes, + automations: automations ?? this.automations, + isLoading: isLoading ?? this.isLoading, + errorMessage: errorMessage ?? this.errorMessage, ); } @override - List get props => [ifItems, thenItems]; + List get props => [ + ifItems, + thenItems, + scenes, + automations, + isLoading, + errorMessage, + ]; } diff --git a/lib/pages/routiens/models/routine_model.dart b/lib/pages/routiens/models/routine_model.dart new file mode 100644 index 00000000..2db2247f --- /dev/null +++ b/lib/pages/routiens/models/routine_model.dart @@ -0,0 +1,30 @@ +import 'dart:convert'; + +class ScenesModel { + final String id; + final String name; + final String status; + final String type; + final String? icon; + + ScenesModel({required this.id, required this.name, required this.status, required this.type, this.icon}); + + factory ScenesModel.fromRawJson(String str) => ScenesModel.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory ScenesModel.fromJson(Map json) => ScenesModel( + id: json["id"], + name: json["name"] ?? '', + status: json["status"] ?? '', + type: json["type"] ?? '', + icon: json["icon"] as String?, + ); + + Map toJson() => { + "id": id, + "name": name, + "status": status, + "type": type, + }; +} diff --git a/lib/pages/routiens/widgets/conditions_routines_devices_view.dart b/lib/pages/routiens/widgets/conditions_routines_devices_view.dart index 6e6ef4a2..91100220 100644 --- a/lib/pages/routiens/widgets/conditions_routines_devices_view.dart +++ b/lib/pages/routiens/widgets/conditions_routines_devices_view.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart'; import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; +import 'package:syncrow_web/pages/routiens/widgets/routine_devices.dart'; import 'package:syncrow_web/pages/routiens/widgets/routines_title_widget.dart'; +import 'package:syncrow_web/pages/routiens/widgets/scenes_and_automations.dart'; import 'package:syncrow_web/pages/routiens/widgets/search_bar_condition_title.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; @@ -50,6 +50,9 @@ class ConditionsRoutinesDevicesView extends StatelessWidget { title: 'Conditions', subtitle: '(THEN)', ), + const SizedBox( + height: 10, + ), const Wrap( spacing: 10, runSpacing: 10, @@ -71,33 +74,21 @@ class ConditionsRoutinesDevicesView extends StatelessWidget { title: 'Routines', subtitle: '(THEN)', ), + const SizedBox( + height: 10, + ), + const ScenesAndAutomations(), + const SizedBox( + height: 10, + ), const TitleRoutine( title: 'Devices', subtitle: '', ), - BlocProvider( - create: (context) => DeviceManagementBloc() - ..add( - FetchDevices(), - ), - child: BlocBuilder( - builder: (context, state) { - if (state is DeviceManagementLoaded) { - return Wrap( - spacing: 10, - runSpacing: 10, - children: state.devices - .map((device) => DraggableCard( - imagePath: device.getDefaultIcon(device.productType), - title: device.name ?? '', - )) - .toList(), - ); - } - return const Center(child: CircularProgressIndicator()); - }, - ), + const SizedBox( + height: 10, ), + const RoutineDevices(), ], ), ), diff --git a/lib/pages/routiens/widgets/dragable_card.dart b/lib/pages/routiens/widgets/dragable_card.dart index 1724e04c..7e98da28 100644 --- a/lib/pages/routiens/widgets/dragable_card.dart +++ b/lib/pages/routiens/widgets/dragable_card.dart @@ -60,13 +60,17 @@ class DraggableCard extends StatelessWidget { const SizedBox( height: 8, ), - Text( - title, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - maxLines: 2, - style: context.textTheme.bodySmall?.copyWith( - color: titleColor ?? ColorsManager.blackColor, + Padding( + padding: const EdgeInsets.symmetric(horizontal: 3), + child: Text( + title, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + maxLines: 2, + style: context.textTheme.bodySmall?.copyWith( + color: titleColor ?? ColorsManager.blackColor, + fontSize: 12, + ), ), ), ], diff --git a/lib/pages/routiens/widgets/routine_devices.dart b/lib/pages/routiens/widgets/routine_devices.dart new file mode 100644 index 00000000..9fba225c --- /dev/null +++ b/lib/pages/routiens/widgets/routine_devices.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; + +class RoutineDevices extends StatelessWidget { + const RoutineDevices({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => DeviceManagementBloc() + ..add( + FetchDevices(), + ), + child: BlocBuilder( + builder: (context, state) { + if (state is DeviceManagementLoaded) { + final deviceList = state.devices + .where((device) => + device.productType == 'AC' || + device.productType == '1G' || + device.productType == '2G' || + device.productType == '3G') + .toList(); + return Wrap( + spacing: 10, + runSpacing: 10, + children: deviceList + .map((device) => DraggableCard( + imagePath: device.getDefaultIcon(device.productType), + title: device.name ?? '', + )) + .toList(), + ); + } + return const Center(child: CircularProgressIndicator()); + }, + ), + ); + } +} diff --git a/lib/pages/routiens/widgets/scenes_and_automations.dart b/lib/pages/routiens/widgets/scenes_and_automations.dart new file mode 100644 index 00000000..9819f0fe --- /dev/null +++ b/lib/pages/routiens/widgets/scenes_and_automations.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class ScenesAndAutomations extends StatelessWidget { + const ScenesAndAutomations({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => RoutineBloc() + ..add( + LoadScenes(spaceId), + ) + ..add( + LoadAutomation(spaceId), + ), + child: BlocBuilder( + builder: (context, state) { + if (state.scenes.isNotEmpty || state.automations.isNotEmpty) { + var scenes = [...state.scenes, ...state.automations]; + return Wrap( + spacing: 10, + runSpacing: 10, + children: scenes + .map((scene) => DraggableCard( + imagePath: Assets.logo, + title: scene.name ?? '', + )) + .toList(), + ); + } + return const Center(child: CircularProgressIndicator()); + }, + ), + ); + } +} diff --git a/lib/services/routines_api.dart b/lib/services/routines_api.dart new file mode 100644 index 00000000..edf865a3 --- /dev/null +++ b/lib/services/routines_api.dart @@ -0,0 +1,216 @@ +import 'package:syncrow_web/pages/routiens/models/routine_model.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/constants/api_const.dart'; + +class SceneApi { + static final HTTPService _httpService = HTTPService(); + +// //create scene +// static Future> createScene( +// CreateSceneModel createSceneModel) async { +// try { +// final response = await _httpService.post( +// path: ApiEndpoints.createScene, +// body: createSceneModel.toMap(), +// showServerMessage: false, +// expectedResponseModel: (json) { +// return json; +// }, +// ); +// return response; +// } catch (e) { +// rethrow; +// } +// } +// +// // create automation +// static Future> createAutomation( +// CreateAutomationModel createAutomationModel) async { +// try { +// final response = await _httpService.post( +// path: ApiEndpoints.createAutomation, +// body: createAutomationModel.toMap(), +// showServerMessage: false, +// expectedResponseModel: (json) { +// return json; +// }, +// ); +// return response; +// } catch (e) { +// rethrow; +// } +// } + + //get scene by unit id + + static Future> getScenesByUnitId(String unitId) async { + try { + final response = await _httpService.get( + path: ApiEndpoints.getSpaceScenes.replaceAll('{unitUuid}', unitId), + showServerMessage: false, + expectedResponseModel: (json) { + List scenes = []; + for (var scene in json) { + scenes.add(ScenesModel.fromJson(scene)); + } + return scenes; + }, + ); + return response; + } catch (e) { + rethrow; + } + } + + //getAutomation + + static Future> getAutomationByUnitId(String unitId) async { + try { + final response = await _httpService.get( + path: ApiEndpoints.getSpaceAutomation.replaceAll('{unitUuid}', unitId), + showServerMessage: false, + expectedResponseModel: (json) { + List scenes = []; + for (var scene in json) { + scenes.add(ScenesModel.fromJson(scene)); + } + return scenes; + }, + ); + return response; + } catch (e) { + rethrow; + } + } + + // static Future triggerScene(String sceneId) async { + // try { + // final response = await _httpService.post( + // path: ApiEndpoints.triggerScene.replaceAll('{sceneId}', sceneId), + // showServerMessage: false, + // expectedResponseModel: (json) => json['success'], + // ); + // return response; + // } catch (e) { + // rethrow; + // } + // } + +// //automation details +// static Future getAutomationDetails( +// String automationId) async { +// try { +// final response = await _httpService.get( +// path: ApiEndpoints.getAutomationDetails +// .replaceAll('{automationId}', automationId), +// showServerMessage: false, +// expectedResponseModel: (json) => SceneDetailsModel.fromJson(json), +// ); +// return response; +// } catch (e) { +// rethrow; +// } +// } +// +// //updateAutomationStatus +// static Future updateAutomationStatus(String automationId, +// AutomationStatusUpdate createAutomationEnable) async { +// try { +// final response = await _httpService.put( +// path: ApiEndpoints.updateAutomationStatus +// .replaceAll('{automationId}', automationId), +// body: createAutomationEnable.toMap(), +// expectedResponseModel: (json) => json['success'], +// ); +// return response; +// } catch (e) { +// rethrow; +// } +// } + + // //getScene + // + // static Future getSceneDetails(String sceneId) async { + // try { + // final response = await _httpService.get( + // path: ApiEndpoints.getScene.replaceAll('{sceneId}', sceneId), + // showServerMessage: false, + // expectedResponseModel: (json) => SceneDetailsModel.fromJson(json), + // ); + // return response; + // } catch (e) { + // rethrow; + // } + // } + // + // //update Scene + // static updateScene(CreateSceneModel createSceneModel, String sceneId) async { + // try { + // final response = await _httpService.put( + // path: ApiEndpoints.updateScene.replaceAll('{sceneId}', sceneId), + // body: createSceneModel + // .toJson(sceneId.isNotEmpty == true ? sceneId : null), + // expectedResponseModel: (json) { + // return json; + // }, + // ); + // return response; + // } catch (e) { + // rethrow; + // } + // } + // + // //update automation + // static updateAutomation( + // CreateAutomationModel createAutomationModel, String automationId) async { + // try { + // final response = await _httpService.put( + // path: ApiEndpoints.updateAutomation + // .replaceAll('{automationId}', automationId), + // body: createAutomationModel + // .toJson(automationId.isNotEmpty == true ? automationId : null), + // expectedResponseModel: (json) { + // return json; + // }, + // ); + // return response; + // } catch (e) { + // rethrow; + // } + // } + // + // //delete Scene + // + // static Future deleteScene( + // {required String unitUuid, required String sceneId}) async { + // try { + // final response = await _httpService.delete( + // path: ApiEndpoints.deleteScene + // .replaceAll('{sceneId}', sceneId) + // .replaceAll('{unitUuid}', unitUuid), + // showServerMessage: false, + // expectedResponseModel: (json) => json['statusCode'] == 200, + // ); + // return response; + // } catch (e) { + // rethrow; + // } + // } + // + // // delete automation + // static Future deleteAutomation( + // {required String unitUuid, required String automationId}) async { + // try { + // final response = await _httpService.delete( + // path: ApiEndpoints.deleteAutomation + // .replaceAll('{automationId}', automationId) + // .replaceAll('{unitUuid}', unitUuid), + // showServerMessage: false, + // expectedResponseModel: (json) => json['statusCode'] == 200, + // ); + // return response; + // } catch (e) { + // rethrow; + // } + // } +} diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index bf167ab5..759ef7d7 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -11,16 +11,12 @@ abstract class ApiEndpoints { static const String visitorPassword = '/visitor-password'; static const String getDevices = '/visitor-password/devices'; - static const String sendOnlineOneTime = - '/visitor-password/temporary-password/online/one-time'; - static const String sendOnlineMultipleTime = - '/visitor-password/temporary-password/online/multiple-time'; + static const String sendOnlineOneTime = '/visitor-password/temporary-password/online/one-time'; + static const String sendOnlineMultipleTime = '/visitor-password/temporary-password/online/multiple-time'; //offline Password - static const String sendOffLineOneTime = - '/visitor-password/temporary-password/offline/one-time'; - static const String sendOffLineMultipleTime = - '/visitor-password/temporary-password/offline/multiple-time'; + static const String sendOffLineOneTime = '/visitor-password/temporary-password/offline/one-time'; + static const String sendOffLineMultipleTime = '/visitor-password/temporary-password/offline/multiple-time'; static const String getUser = '/user/{userUuid}'; @@ -40,14 +36,12 @@ abstract class ApiEndpoints { '/device/report-logs/{uuid}?code={code}&startTime={startTime}&endTime={endTime}'; static const String scheduleByDeviceId = '/schedule/{deviceUuid}'; - static const String getScheduleByDeviceId = - '/schedule/{deviceUuid}?category={category}'; - static const String deleteScheduleByDeviceId = - '/schedule/{deviceUuid}/{scheduleUuid}'; - static const String updateScheduleByDeviceId = - '/schedule/enable/{deviceUuid}'; + static const String getScheduleByDeviceId = '/schedule/{deviceUuid}?category={category}'; + static const String deleteScheduleByDeviceId = '/schedule/{deviceUuid}/{scheduleUuid}'; + static const String updateScheduleByDeviceId = '/schedule/enable/{deviceUuid}'; static const String factoryReset = '/device/factory/reset/{deviceUuid}'; - static const String powerClamp = - '/device/{powerClampUuid}/power-clamp/status'; + static const String powerClamp = '/device/{powerClampUuid}/power-clamp/status'; + static const String getSpaceScenes = '/scene/tap-to-run/{unitUuid}'; + static const String getSpaceAutomation = '/automation/{unitUuid}'; } From 57b8f6b03ea1786f28ee4c1ca975e9b9c3ce58e5 Mon Sep 17 00:00:00 2001 From: ashrafzarkanisala Date: Tue, 19 Nov 2024 00:54:43 +0300 Subject: [PATCH 05/23] push add ac fucntions and handle doplicate adding to the then or if items --- assets/icons/functions_icons/ac_cooling.svg | 12 + assets/icons/functions_icons/ac_fan_auto.svg | 9 + assets/icons/functions_icons/ac_fan_high.svg | 12 + assets/icons/functions_icons/ac_fan_low.svg | 12 + .../icons/functions_icons/ac_fan_middle.svg | 12 + assets/icons/functions_icons/ac_heating.svg | 14 ++ assets/icons/functions_icons/ac_power.svg | 5 + assets/icons/functions_icons/ac_power_off.svg | 5 + .../automation_functions/card_unlock.svg | 14 ++ .../automation_functions/current_temp.svg | 11 + .../automation_functions/doorbell.svg | 13 ++ .../doorlock_normal_open.svg | 12 + .../automation_functions/double_lock.svg | 12 + .../fingerprint_unlock.svg | 79 +++++++ .../automation_functions/hijack_alarm.svg | 13 ++ .../automation_functions/lock_alarm.svg | 149 ++++++++++++ .../automation_functions/motion.svg | 14 ++ .../automation_functions/password_unlock.svg | 16 ++ .../automation_functions/presence.svg | 18 ++ .../automation_functions/presence_state.svg | 18 ++ .../remote_unlock_req.svg | 15 ++ .../remote_unlock_via_app.svg | 40 ++++ .../residual_electricity.svg | 4 + .../automation_functions/self_test_result.svg | 23 ++ .../temp_password_unlock.svg | 16 ++ .../icons/functions_icons/celsius_degrees.svg | 7 + assets/icons/functions_icons/child_lock.svg | 5 + .../icons/functions_icons/factory_reset.svg | 10 + assets/icons/functions_icons/fan_speed.svg | 12 + .../icons/functions_icons/far_detection.svg | 16 ++ .../far_detection_function.svg | 17 ++ assets/icons/functions_icons/freezing.svg | 3 + assets/icons/functions_icons/indicator.svg | 12 + .../icons/functions_icons/light_countdown.svg | 42 ++++ assets/icons/functions_icons/master_state.svg | 18 ++ .../functions_icons/motion_detection.svg | 21 ++ .../functions_icons/motionless_detection.svg | 6 + assets/icons/functions_icons/nobody_time.svg | 4 + assets/icons/functions_icons/reset_off.svg | 6 + .../functions_icons/scene_child_lock.svg | 12 + .../functions_icons/scene_child_unlock.svg | 12 + .../icons/functions_icons/scene_refresh.svg | 6 + assets/icons/functions_icons/sensitivity.svg | 14 ++ .../sesitivity_operation_icon.svg | 5 + .../functions_icons/switch_alarm_sound.svg | 10 + assets/icons/functions_icons/tempreture.svg | 11 + .../all_devices/models/devices_model.dart | 25 ++ lib/pages/routiens/bloc/routine_bloc.dart | 16 +- lib/pages/routiens/bloc/routine_event.dart | 4 +- lib/pages/routiens/bloc/routine_state.dart | 8 +- lib/pages/routiens/enum/ac_values_enum.dart | 9 + lib/pages/routiens/helper/ac_helper.dart | 190 ++++++++++++++++ lib/pages/routiens/models/ac/ac_function.dart | 215 ++++++++++++++++++ .../models/ac/ac_operational_value.dart | 11 + .../routiens/models/device_functions.dart | 17 ++ lib/pages/routiens/models/routine_item.dart | 29 +++ lib/pages/routiens/widgets/dragable_card.dart | 12 +- .../routiens/widgets/routine_devices.dart | 14 +- .../widgets/scenes_and_automations.dart | 14 +- lib/utils/constants/assets.dart | 55 +++++ 60 files changed, 1392 insertions(+), 24 deletions(-) create mode 100644 assets/icons/functions_icons/ac_cooling.svg create mode 100644 assets/icons/functions_icons/ac_fan_auto.svg create mode 100644 assets/icons/functions_icons/ac_fan_high.svg create mode 100644 assets/icons/functions_icons/ac_fan_low.svg create mode 100644 assets/icons/functions_icons/ac_fan_middle.svg create mode 100644 assets/icons/functions_icons/ac_heating.svg create mode 100644 assets/icons/functions_icons/ac_power.svg create mode 100644 assets/icons/functions_icons/ac_power_off.svg create mode 100644 assets/icons/functions_icons/automation_functions/card_unlock.svg create mode 100644 assets/icons/functions_icons/automation_functions/current_temp.svg create mode 100644 assets/icons/functions_icons/automation_functions/doorbell.svg create mode 100644 assets/icons/functions_icons/automation_functions/doorlock_normal_open.svg create mode 100644 assets/icons/functions_icons/automation_functions/double_lock.svg create mode 100644 assets/icons/functions_icons/automation_functions/fingerprint_unlock.svg create mode 100644 assets/icons/functions_icons/automation_functions/hijack_alarm.svg create mode 100644 assets/icons/functions_icons/automation_functions/lock_alarm.svg create mode 100644 assets/icons/functions_icons/automation_functions/motion.svg create mode 100644 assets/icons/functions_icons/automation_functions/password_unlock.svg create mode 100644 assets/icons/functions_icons/automation_functions/presence.svg create mode 100644 assets/icons/functions_icons/automation_functions/presence_state.svg create mode 100644 assets/icons/functions_icons/automation_functions/remote_unlock_req.svg create mode 100644 assets/icons/functions_icons/automation_functions/remote_unlock_via_app.svg create mode 100644 assets/icons/functions_icons/automation_functions/residual_electricity.svg create mode 100644 assets/icons/functions_icons/automation_functions/self_test_result.svg create mode 100644 assets/icons/functions_icons/automation_functions/temp_password_unlock.svg create mode 100644 assets/icons/functions_icons/celsius_degrees.svg create mode 100644 assets/icons/functions_icons/child_lock.svg create mode 100644 assets/icons/functions_icons/factory_reset.svg create mode 100644 assets/icons/functions_icons/fan_speed.svg create mode 100644 assets/icons/functions_icons/far_detection.svg create mode 100644 assets/icons/functions_icons/far_detection_function.svg create mode 100644 assets/icons/functions_icons/freezing.svg create mode 100644 assets/icons/functions_icons/indicator.svg create mode 100644 assets/icons/functions_icons/light_countdown.svg create mode 100644 assets/icons/functions_icons/master_state.svg create mode 100644 assets/icons/functions_icons/motion_detection.svg create mode 100644 assets/icons/functions_icons/motionless_detection.svg create mode 100644 assets/icons/functions_icons/nobody_time.svg create mode 100644 assets/icons/functions_icons/reset_off.svg create mode 100644 assets/icons/functions_icons/scene_child_lock.svg create mode 100644 assets/icons/functions_icons/scene_child_unlock.svg create mode 100644 assets/icons/functions_icons/scene_refresh.svg create mode 100644 assets/icons/functions_icons/sensitivity.svg create mode 100644 assets/icons/functions_icons/sesitivity_operation_icon.svg create mode 100644 assets/icons/functions_icons/switch_alarm_sound.svg create mode 100644 assets/icons/functions_icons/tempreture.svg create mode 100644 lib/pages/routiens/enum/ac_values_enum.dart create mode 100644 lib/pages/routiens/helper/ac_helper.dart create mode 100644 lib/pages/routiens/models/ac/ac_function.dart create mode 100644 lib/pages/routiens/models/ac/ac_operational_value.dart create mode 100644 lib/pages/routiens/models/device_functions.dart create mode 100644 lib/pages/routiens/models/routine_item.dart diff --git a/assets/icons/functions_icons/ac_cooling.svg b/assets/icons/functions_icons/ac_cooling.svg new file mode 100644 index 00000000..e95c0d4e --- /dev/null +++ b/assets/icons/functions_icons/ac_cooling.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/ac_fan_auto.svg b/assets/icons/functions_icons/ac_fan_auto.svg new file mode 100644 index 00000000..0acacfef --- /dev/null +++ b/assets/icons/functions_icons/ac_fan_auto.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/icons/functions_icons/ac_fan_high.svg b/assets/icons/functions_icons/ac_fan_high.svg new file mode 100644 index 00000000..d6131531 --- /dev/null +++ b/assets/icons/functions_icons/ac_fan_high.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/ac_fan_low.svg b/assets/icons/functions_icons/ac_fan_low.svg new file mode 100644 index 00000000..f4bf56b7 --- /dev/null +++ b/assets/icons/functions_icons/ac_fan_low.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/ac_fan_middle.svg b/assets/icons/functions_icons/ac_fan_middle.svg new file mode 100644 index 00000000..ee940238 --- /dev/null +++ b/assets/icons/functions_icons/ac_fan_middle.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/ac_heating.svg b/assets/icons/functions_icons/ac_heating.svg new file mode 100644 index 00000000..47a160c8 --- /dev/null +++ b/assets/icons/functions_icons/ac_heating.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/ac_power.svg b/assets/icons/functions_icons/ac_power.svg new file mode 100644 index 00000000..cc2127f0 --- /dev/null +++ b/assets/icons/functions_icons/ac_power.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/functions_icons/ac_power_off.svg b/assets/icons/functions_icons/ac_power_off.svg new file mode 100644 index 00000000..70f7f9aa --- /dev/null +++ b/assets/icons/functions_icons/ac_power_off.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/functions_icons/automation_functions/card_unlock.svg b/assets/icons/functions_icons/automation_functions/card_unlock.svg new file mode 100644 index 00000000..dd77680a --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/card_unlock.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/automation_functions/current_temp.svg b/assets/icons/functions_icons/automation_functions/current_temp.svg new file mode 100644 index 00000000..42cceb23 --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/current_temp.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/icons/functions_icons/automation_functions/doorbell.svg b/assets/icons/functions_icons/automation_functions/doorbell.svg new file mode 100644 index 00000000..1dc515a9 --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/doorbell.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/automation_functions/doorlock_normal_open.svg b/assets/icons/functions_icons/automation_functions/doorlock_normal_open.svg new file mode 100644 index 00000000..8f4a5901 --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/doorlock_normal_open.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/automation_functions/double_lock.svg b/assets/icons/functions_icons/automation_functions/double_lock.svg new file mode 100644 index 00000000..d8ad971d --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/double_lock.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/automation_functions/fingerprint_unlock.svg b/assets/icons/functions_icons/automation_functions/fingerprint_unlock.svg new file mode 100644 index 00000000..f9f5b84c --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/fingerprint_unlock.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/automation_functions/hijack_alarm.svg b/assets/icons/functions_icons/automation_functions/hijack_alarm.svg new file mode 100644 index 00000000..e32997fb --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/hijack_alarm.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/automation_functions/lock_alarm.svg b/assets/icons/functions_icons/automation_functions/lock_alarm.svg new file mode 100644 index 00000000..8bd2deeb --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/lock_alarm.svg @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/automation_functions/motion.svg b/assets/icons/functions_icons/automation_functions/motion.svg new file mode 100644 index 00000000..8d69463b --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/motion.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/automation_functions/password_unlock.svg b/assets/icons/functions_icons/automation_functions/password_unlock.svg new file mode 100644 index 00000000..1920b69f --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/password_unlock.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/automation_functions/presence.svg b/assets/icons/functions_icons/automation_functions/presence.svg new file mode 100644 index 00000000..d71a474d --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/presence.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/automation_functions/presence_state.svg b/assets/icons/functions_icons/automation_functions/presence_state.svg new file mode 100644 index 00000000..d5de48e1 --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/presence_state.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/automation_functions/remote_unlock_req.svg b/assets/icons/functions_icons/automation_functions/remote_unlock_req.svg new file mode 100644 index 00000000..da128ff7 --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/remote_unlock_req.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/automation_functions/remote_unlock_via_app.svg b/assets/icons/functions_icons/automation_functions/remote_unlock_via_app.svg new file mode 100644 index 00000000..39fc859b --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/remote_unlock_via_app.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/automation_functions/residual_electricity.svg b/assets/icons/functions_icons/automation_functions/residual_electricity.svg new file mode 100644 index 00000000..6a5b6127 --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/residual_electricity.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/functions_icons/automation_functions/self_test_result.svg b/assets/icons/functions_icons/automation_functions/self_test_result.svg new file mode 100644 index 00000000..8739327b --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/self_test_result.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/automation_functions/temp_password_unlock.svg b/assets/icons/functions_icons/automation_functions/temp_password_unlock.svg new file mode 100644 index 00000000..98d7573c --- /dev/null +++ b/assets/icons/functions_icons/automation_functions/temp_password_unlock.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/celsius_degrees.svg b/assets/icons/functions_icons/celsius_degrees.svg new file mode 100644 index 00000000..7acbd6e7 --- /dev/null +++ b/assets/icons/functions_icons/celsius_degrees.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/assets/icons/functions_icons/child_lock.svg b/assets/icons/functions_icons/child_lock.svg new file mode 100644 index 00000000..6b0138bf --- /dev/null +++ b/assets/icons/functions_icons/child_lock.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/functions_icons/factory_reset.svg b/assets/icons/functions_icons/factory_reset.svg new file mode 100644 index 00000000..7a47f24b --- /dev/null +++ b/assets/icons/functions_icons/factory_reset.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/functions_icons/fan_speed.svg b/assets/icons/functions_icons/fan_speed.svg new file mode 100644 index 00000000..07a48834 --- /dev/null +++ b/assets/icons/functions_icons/fan_speed.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/far_detection.svg b/assets/icons/functions_icons/far_detection.svg new file mode 100644 index 00000000..2827d94a --- /dev/null +++ b/assets/icons/functions_icons/far_detection.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/far_detection_function.svg b/assets/icons/functions_icons/far_detection_function.svg new file mode 100644 index 00000000..894b84ed --- /dev/null +++ b/assets/icons/functions_icons/far_detection_function.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/freezing.svg b/assets/icons/functions_icons/freezing.svg new file mode 100644 index 00000000..6c02f2e4 --- /dev/null +++ b/assets/icons/functions_icons/freezing.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/functions_icons/indicator.svg b/assets/icons/functions_icons/indicator.svg new file mode 100644 index 00000000..b58a976e --- /dev/null +++ b/assets/icons/functions_icons/indicator.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/light_countdown.svg b/assets/icons/functions_icons/light_countdown.svg new file mode 100644 index 00000000..94f65b9a --- /dev/null +++ b/assets/icons/functions_icons/light_countdown.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/master_state.svg b/assets/icons/functions_icons/master_state.svg new file mode 100644 index 00000000..0aafae1a --- /dev/null +++ b/assets/icons/functions_icons/master_state.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/motion_detection.svg b/assets/icons/functions_icons/motion_detection.svg new file mode 100644 index 00000000..a9b2d685 --- /dev/null +++ b/assets/icons/functions_icons/motion_detection.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/motionless_detection.svg b/assets/icons/functions_icons/motionless_detection.svg new file mode 100644 index 00000000..25a767c1 --- /dev/null +++ b/assets/icons/functions_icons/motionless_detection.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/functions_icons/nobody_time.svg b/assets/icons/functions_icons/nobody_time.svg new file mode 100644 index 00000000..df80b517 --- /dev/null +++ b/assets/icons/functions_icons/nobody_time.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/functions_icons/reset_off.svg b/assets/icons/functions_icons/reset_off.svg new file mode 100644 index 00000000..eac88f2b --- /dev/null +++ b/assets/icons/functions_icons/reset_off.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/functions_icons/scene_child_lock.svg b/assets/icons/functions_icons/scene_child_lock.svg new file mode 100644 index 00000000..7e56164a --- /dev/null +++ b/assets/icons/functions_icons/scene_child_lock.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/scene_child_unlock.svg b/assets/icons/functions_icons/scene_child_unlock.svg new file mode 100644 index 00000000..4eafbdea --- /dev/null +++ b/assets/icons/functions_icons/scene_child_unlock.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/scene_refresh.svg b/assets/icons/functions_icons/scene_refresh.svg new file mode 100644 index 00000000..c54ffb04 --- /dev/null +++ b/assets/icons/functions_icons/scene_refresh.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/functions_icons/sensitivity.svg b/assets/icons/functions_icons/sensitivity.svg new file mode 100644 index 00000000..b75ebd3e --- /dev/null +++ b/assets/icons/functions_icons/sensitivity.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/icons/functions_icons/sesitivity_operation_icon.svg b/assets/icons/functions_icons/sesitivity_operation_icon.svg new file mode 100644 index 00000000..612148c5 --- /dev/null +++ b/assets/icons/functions_icons/sesitivity_operation_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/functions_icons/switch_alarm_sound.svg b/assets/icons/functions_icons/switch_alarm_sound.svg new file mode 100644 index 00000000..db645338 --- /dev/null +++ b/assets/icons/functions_icons/switch_alarm_sound.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/functions_icons/tempreture.svg b/assets/icons/functions_icons/tempreture.svg new file mode 100644 index 00000000..448083a7 --- /dev/null +++ b/assets/icons/functions_icons/tempreture.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + 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 50e588b0..b37ff36c 100644 --- a/lib/pages/device_managment/all_devices/models/devices_model.dart +++ b/lib/pages/device_managment/all_devices/models/devices_model.dart @@ -2,6 +2,8 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/device_com import 'package:syncrow_web/pages/device_managment/all_devices/models/device_space_model.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/room.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/unit.dart'; +import 'package:syncrow_web/pages/routiens/models/ac/ac_function.dart'; +import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/enum/device_types.dart'; @@ -69,6 +71,7 @@ class AllDevicesModel { int? batteryLevel; String? productName; List? spaces; + List? _deviceFunctions; AllDevicesModel({ this.room, @@ -215,6 +218,28 @@ SOS return tempIcon; } + List get deviceFunctions { + _deviceFunctions ??= _getDeviceFunctions(); + return _deviceFunctions!; + } + + List _getDeviceFunctions() { + switch (productType) { + case 'AC': + return [ + SwitchFunction(deviceId: uuid ?? '', deviceName: name ?? ''), + ModeFunction(deviceId: uuid ?? '', deviceName: name ?? ''), + TempSetFunction(deviceId: uuid ?? '', deviceName: name ?? ''), + LevelFunction(deviceId: uuid ?? '', deviceName: name ?? ''), + ChildLockFunction(deviceId: uuid ?? '', deviceName: name ?? ''), + ]; + + // other product types + default: + return []; + } + } + Map toJson() { final data = {}; if (room != null) { diff --git a/lib/pages/routiens/bloc/routine_bloc.dart b/lib/pages/routiens/bloc/routine_bloc.dart index a7fd7228..dfbb824c 100644 --- a/lib/pages/routiens/bloc/routine_bloc.dart +++ b/lib/pages/routiens/bloc/routine_bloc.dart @@ -17,13 +17,21 @@ class RoutineBloc extends Bloc { } void _onAddToIfContainer(AddToIfContainer event, Emitter emit) { - final updatedIfItems = List>.from(state.ifItems)..add(event.item); - emit(state.copyWith(ifItems: updatedIfItems)); + if (!_isDuplicate(state.ifItems, event.item)) { + final updatedIfItems = List>.from(state.ifItems)..add(event.item); + emit(state.copyWith(ifItems: updatedIfItems)); + } } void _onAddToThenContainer(AddToThenContainer event, Emitter emit) { - final updatedThenItems = List>.from(state.thenItems)..add(event.item); - emit(state.copyWith(thenItems: updatedThenItems)); + if (!_isDuplicate(state.thenItems, event.item)) { + final updatedThenItems = List>.from(state.thenItems)..add(event.item); + emit(state.copyWith(thenItems: updatedThenItems)); + } + } + + bool _isDuplicate(List> items, Map newItem) { + return items.any((item) => item['imagePath'] == newItem['imagePath'] && item['title'] == newItem['title']); } Future _onLoadScenes(LoadScenes event, Emitter emit) async { diff --git a/lib/pages/routiens/bloc/routine_event.dart b/lib/pages/routiens/bloc/routine_event.dart index 6dcacf9c..aaee73c7 100644 --- a/lib/pages/routiens/bloc/routine_event.dart +++ b/lib/pages/routiens/bloc/routine_event.dart @@ -8,7 +8,7 @@ abstract class RoutineEvent extends Equatable { } class AddToIfContainer extends RoutineEvent { - final Map item; + final Map item; const AddToIfContainer(this.item); @@ -17,7 +17,7 @@ class AddToIfContainer extends RoutineEvent { } class AddToThenContainer extends RoutineEvent { - final Map item; + final Map item; const AddToThenContainer(this.item); diff --git a/lib/pages/routiens/bloc/routine_state.dart b/lib/pages/routiens/bloc/routine_state.dart index 8f715428..63d2e6f8 100644 --- a/lib/pages/routiens/bloc/routine_state.dart +++ b/lib/pages/routiens/bloc/routine_state.dart @@ -1,8 +1,8 @@ part of 'routine_bloc.dart'; class RoutineState extends Equatable { - final List> ifItems; - final List> thenItems; + final List> ifItems; + final List> thenItems; final List> availableCards; final List scenes; final List automations; @@ -20,8 +20,8 @@ class RoutineState extends Equatable { }); RoutineState copyWith({ - List>? ifItems, - List>? thenItems, + List>? ifItems, + List>? thenItems, List? scenes, List? automations, bool? isLoading, diff --git a/lib/pages/routiens/enum/ac_values_enum.dart b/lib/pages/routiens/enum/ac_values_enum.dart new file mode 100644 index 00000000..e7d0f865 --- /dev/null +++ b/lib/pages/routiens/enum/ac_values_enum.dart @@ -0,0 +1,9 @@ +enum AcValuesEnums { + Cooling, + Heating, + Ventilation, +} + +enum TempModes { hot, cold, wind } + +enum FanSpeeds { auto, low, middle, high } diff --git a/lib/pages/routiens/helper/ac_helper.dart b/lib/pages/routiens/helper/ac_helper.dart new file mode 100644 index 00000000..6cdb415d --- /dev/null +++ b/lib/pages/routiens/helper/ac_helper.dart @@ -0,0 +1,190 @@ +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/routiens/bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/models/ac/ac_function.dart'; +import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +mixin ACHelper { + Future?> showACFunctionsDialog(BuildContext context, List> functions) { + List acFunctions = functions.whereType().toList(); + String? selectedFunction; + dynamic selectedValue; + + return showDialog( + context: context, + builder: (BuildContext context) { + return StatefulBuilder( + builder: (context, setState) { + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: Container( + width: selectedFunction != null ? 600 : 360, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.only(top: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'AC Functions', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.primaryColorWithOpacity, + fontWeight: FontWeight.bold, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 50), + child: Container( + height: 1, + width: double.infinity, + color: ColorsManager.greyColor, + ), + ), + Flexible( + child: Row( + children: [ + Expanded( + child: ListView.separated( + shrinkWrap: true, + itemCount: acFunctions.length, + separatorBuilder: (context, index) => Divider(), + itemBuilder: (context, index) { + final function = acFunctions[index]; + return ListTile( + leading: Image.asset(function.icon, width: 24, height: 24), + title: Text(function.operationName), + trailing: Icon(Icons.arrow_forward_ios), + onTap: () { + setState(() { + selectedFunction = function.code; + selectedValue = null; + }); + }, + ); + }, + ), + ), + if (selectedFunction != null) + Container( + width: 1, + color: ColorsManager.greyColor, + ), + if (selectedFunction != null) + Expanded( + child: ListView.separated( + shrinkWrap: true, + itemCount: acFunctions + .firstWhere((f) => f.code == selectedFunction) + .getOperationalValues() + .length, + separatorBuilder: (context, index) => Divider(), + itemBuilder: (context, index) { + final operationalValue = acFunctions.firstWhere((f) => f.code == selectedFunction) + ..getOperationalValues()[index]; + return ListTile( + leading: Image.asset(operationalValue.icon, width: 24, height: 24), + title: Text(operationalValue.getOperationalValues()[index].description), + trailing: Radio( + value: operationalValue.getOperationalValues()[index].value, + groupValue: selectedValue, + onChanged: (value) { + setState(() { + selectedValue = value; + }); + }, + ), + ); + }, + ), + ), + ], + ), + ), + Container( + height: 1, + width: double.infinity, + color: ColorsManager.greyColor, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: Center( + child: Text( + 'Cancel', + style: Theme.of(context).textTheme.bodyMedium!.copyWith(color: ColorsManager.greyColor), + ), + ), + ), + Container( + height: 50, + width: 1, + color: ColorsManager.greyColor, + ), + GestureDetector( + onTap: () { + // Handle the confirmation action here + Navigator.pop(context, { + 'function': selectedFunction, + 'value': selectedValue, + }); + }, + child: Center( + child: Text( + 'Confirm', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.primaryColorWithOpacity, + ), + ), + ), + ), + ], + ), + ], + ), + ), + ); + }, + ); + }, + ); + } + + void handleACDeviceDrop(BuildContext context, Map data) { + final device = data['device'] as AllDevicesModel; + final acFunctions = device.deviceFunctions; + + showACFunctionsDialog(context, acFunctions).then((result) { + if (result != null) { + _addACDeviceToRoutine(context, data, result); + } + }); + } + + void handleNonACDeviceDrop(BuildContext context, Map data) { + context.read().add(AddToThenContainer(data)); + } + + void _addACDeviceToRoutine(BuildContext context, Map deviceData, Map functionData) { + final updatedData = { + ...deviceData, + 'function': functionData['function'], + 'value': functionData['value'], + }; + + context.read().add(AddToThenContainer(updatedData)); + + _logACFunctionSelection(functionData); + } + + void _logACFunctionSelection(Map functionData) { + print('Selected AC function: ${functionData['function']}, Value: ${functionData['value']}'); + } +} diff --git a/lib/pages/routiens/models/ac/ac_function.dart b/lib/pages/routiens/models/ac/ac_function.dart new file mode 100644 index 00000000..3afe74c6 --- /dev/null +++ b/lib/pages/routiens/models/ac/ac_function.dart @@ -0,0 +1,215 @@ +import 'package:syncrow_web/pages/device_managment/ac/model/ac_model.dart'; +import 'package:syncrow_web/pages/routiens/models/ac/ac_operational_value.dart'; +import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +abstract class ACFunction extends DeviceFunction { + ACFunction({ + required String deviceId, + required String deviceName, + required String code, + required String operationName, + required String icon, + }) : super( + deviceId: deviceId, + deviceName: deviceName, + code: code, + operationName: operationName, + icon: icon, + ); + + @override + AcStatusModel execute(AcStatusModel currentStatus, dynamic newValue); + + List getOperationalValues(); +} + +class SwitchFunction extends ACFunction { + SwitchFunction({required super.deviceId, required super.deviceName}) + : super( + code: 'switch', + operationName: 'Power', + icon: Assets.assetsAcPower, + ); + + @override + AcStatusModel execute(AcStatusModel currentStatus, dynamic newValue) { + return currentStatus.copyWith(acSwitch: newValue as bool); + } + + @override + List getOperationalValues() => [ + ACOperationalValue( + icon: Assets.assetsAcPower, + description: "ON", + value: true, + ), + ACOperationalValue( + icon: Assets.assetsAcPowerOFF, + description: "OFF", + value: false, + ), + ]; +} + +class ModeFunction extends ACFunction { + ModeFunction({required super.deviceId, required super.deviceName}) + : super( + code: 'mode', + operationName: 'Mode', + icon: Assets.assetsFreezing, + ); + + @override + AcStatusModel execute(AcStatusModel currentStatus, dynamic newValue) { + return currentStatus.copyWith(modeString: (newValue as TempModes).toString().split('.').last); + } + + @override + List getOperationalValues() => [ + ACOperationalValue( + icon: Assets.assetsAcCooling, + description: "Cooling", + value: TempModes.cold, + ), + ACOperationalValue( + icon: Assets.assetsAcHeating, + description: "Heating", + value: TempModes.hot, + ), + ACOperationalValue( + icon: Assets.assetsFanSpeed, + description: "Ventilation", + value: TempModes.wind, + ), + ]; +} + +class TempSetFunction extends ACFunction { + final int min; + final int max; + final int step; + + TempSetFunction({required super.deviceId, required super.deviceName}) + : min = 200, + max = 300, + step = 5, + super( + code: 'temp_set', + operationName: 'Set Temperature', + icon: Assets.assetsTempreture, + ); + + @override + AcStatusModel execute(AcStatusModel currentStatus, dynamic newValue) { + return currentStatus.copyWith(tempSet: newValue as int); + } + + @override + List getOperationalValues() { + List values = []; + for (int temp = min; temp <= max; temp += step) { + values.add(ACOperationalValue( + icon: Assets.assetsTempreture, + description: "${temp / 10}°C", + value: temp, + )); + } + return values; + } +} + +class LevelFunction extends ACFunction { + LevelFunction({required super.deviceId, required super.deviceName}) + : super( + code: 'level', + operationName: 'Fan Speed', + icon: Assets.assetsFanSpeed, + ); + + @override + AcStatusModel execute(AcStatusModel currentStatus, dynamic newValue) { + return currentStatus.copyWith(fanSpeedsString: (newValue as FanSpeeds).toString().split('.').last); + } + + @override + List getOperationalValues() => [ + ACOperationalValue( + icon: Assets.assetsAcFanLow, + description: "LOW", + value: FanSpeeds.low, + ), + ACOperationalValue( + icon: Assets.assetsAcFanMiddle, + description: "MIDDLE", + value: FanSpeeds.middle, + ), + ACOperationalValue( + icon: Assets.assetsAcFanHigh, + description: "HIGH", + value: FanSpeeds.high, + ), + ACOperationalValue( + icon: Assets.assetsAcFanAuto, + description: "AUTO", + value: FanSpeeds.auto, + ), + ]; +} + +class ChildLockFunction extends ACFunction { + ChildLockFunction({required super.deviceId, required super.deviceName}) + : super( + code: 'child_lock', + operationName: 'Child Lock', + icon: Assets.assetsChildLock, + ); + + @override + AcStatusModel execute(AcStatusModel currentStatus, dynamic newValue) { + return currentStatus.copyWith(childLock: newValue as bool); + } + + @override + List getOperationalValues() => [ + ACOperationalValue( + icon: Assets.assetsSceneChildLock, + description: "Lock", + value: true, + ), + ACOperationalValue( + icon: Assets.assetsSceneChildUnlock, + description: "Unlock", + value: false, + ), + ]; +} + +/* + + final deviceId = 'AC001'; + final deviceName = 'Living Room AC'; + + // Initial AC status + var acStatus = AcStatusModel( + uuid: deviceId, + acSwitch: false, + modeString: 'cold', + tempSet: 220, + currentTemp: 250, + fanSpeedsString: 'auto', + childLock: false, + ); + + // Get all AC functions + final acFunctions = getACFunctions(deviceId, deviceName); + + // Example: Turn on the AC + final switchFunction = acFunctions.firstWhere((f) => f is SwitchFunction) as SwitchFunction; + acStatus = switchFunction.execute(acStatus, true); + + // Example: Change mode to heat + final modeFunction = acFunctions.firstWhere((f) => f is ModeFunction) as ModeFunction; + acStatus = modeFunction.execute(acStatus, TempModes.hot); + + */ diff --git a/lib/pages/routiens/models/ac/ac_operational_value.dart b/lib/pages/routiens/models/ac/ac_operational_value.dart new file mode 100644 index 00000000..4ca45d10 --- /dev/null +++ b/lib/pages/routiens/models/ac/ac_operational_value.dart @@ -0,0 +1,11 @@ +class ACOperationalValue { + final String icon; + final String description; + final dynamic value; + + ACOperationalValue({ + required this.icon, + required this.description, + required this.value, + }); +} diff --git a/lib/pages/routiens/models/device_functions.dart b/lib/pages/routiens/models/device_functions.dart new file mode 100644 index 00000000..fa6ac673 --- /dev/null +++ b/lib/pages/routiens/models/device_functions.dart @@ -0,0 +1,17 @@ +abstract class DeviceFunction { + final String deviceId; + final String deviceName; + final String code; + final String operationName; + final String icon; + + DeviceFunction({ + required this.deviceId, + required this.deviceName, + required this.code, + required this.operationName, + required this.icon, + }); + + T execute(T currentStatus, dynamic newValue); +} diff --git a/lib/pages/routiens/models/routine_item.dart b/lib/pages/routiens/models/routine_item.dart new file mode 100644 index 00000000..33920634 --- /dev/null +++ b/lib/pages/routiens/models/routine_item.dart @@ -0,0 +1,29 @@ +import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; + +class RoutineItem { + final AllDevicesModel device; + final String? function; + final dynamic value; + + RoutineItem({ + required this.device, + this.function, + this.value, + }); + + Map toMap() { + return { + 'device': device, + 'function': function, + 'value': value, + }; + } + + factory RoutineItem.fromMap(Map map) { + return RoutineItem( + device: map['device'] as AllDevicesModel, + function: map['function'], + value: map['value'], + ); + } +} diff --git a/lib/pages/routiens/widgets/dragable_card.dart b/lib/pages/routiens/widgets/dragable_card.dart index 7e98da28..2e7d1a0e 100644 --- a/lib/pages/routiens/widgets/dragable_card.dart +++ b/lib/pages/routiens/widgets/dragable_card.dart @@ -10,24 +10,32 @@ class DraggableCard extends StatelessWidget { required this.title, this.titleColor, this.isDragged = false, + this.isDisabled = false, }); final String imagePath; final String title; final Color? titleColor; final bool isDragged; + final bool isDisabled; @override Widget build(BuildContext context) { - return Draggable>( + Widget card = Draggable>( data: {'key': UniqueKey().toString(), 'imagePath': imagePath, 'title': title}, feedback: Transform.rotate( angle: -0.1, child: _buildCardContent(context), ), childWhenDragging: _buildGreyContainer(), - child: isDragged ? _buildGreyContainer() : _buildCardContent(context), + child: _buildCardContent(context), ); + + if (isDisabled) { + card = AbsorbPointer(child: card); + } + + return card; } Widget _buildCardContent(BuildContext context) { diff --git a/lib/pages/routiens/widgets/routine_devices.dart b/lib/pages/routiens/widgets/routine_devices.dart index 9fba225c..12d8492e 100644 --- a/lib/pages/routiens/widgets/routine_devices.dart +++ b/lib/pages/routiens/widgets/routine_devices.dart @@ -28,12 +28,14 @@ class RoutineDevices extends StatelessWidget { return Wrap( spacing: 10, runSpacing: 10, - children: deviceList - .map((device) => DraggableCard( - imagePath: device.getDefaultIcon(device.productType), - title: device.name ?? '', - )) - .toList(), + children: deviceList.asMap().entries.map((entry) { + final index = entry.key; + final device = entry.value; + return DraggableCard( + imagePath: device.getDefaultIcon(device.productType), + title: device.name ?? '', + ); + }).toList(), ); } return const Center(child: CircularProgressIndicator()); diff --git a/lib/pages/routiens/widgets/scenes_and_automations.dart b/lib/pages/routiens/widgets/scenes_and_automations.dart index 9819f0fe..7c90305b 100644 --- a/lib/pages/routiens/widgets/scenes_and_automations.dart +++ b/lib/pages/routiens/widgets/scenes_and_automations.dart @@ -26,12 +26,14 @@ class ScenesAndAutomations extends StatelessWidget { return Wrap( spacing: 10, runSpacing: 10, - children: scenes - .map((scene) => DraggableCard( - imagePath: Assets.logo, - title: scene.name ?? '', - )) - .toList(), + children: scenes.asMap().entries.map((entry) { + final index = entry.key; + final scene = entry.value; + return DraggableCard( + imagePath: Assets.logo, + title: scene.name ?? '', + ); + }).toList(), ); } return const Center(child: CircularProgressIndicator()); diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index 2c0c526f..6c542017 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -210,4 +210,59 @@ class Assets { //assets/icons/routine/delay.svg 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 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 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 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 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 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 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 assetsSwitchAlarmSound = "assets/icons/functions_icons/switch_alarm_sound.svg"; + static const String assetsResetOff = "assets/icons/functions_icons/reset_off.svg"; + +// Assets for automation_functions + static const String assetsCardUnlock = "assets/icons/functions_icons/automation_functions/card_unlock.svg"; + static const String assetsDoorbell = "assets/icons/functions_icons/automation_functions/doorbell.svg"; + static const String assetsDoorlockNormalOpen = + "assets/icons/functions_icons/automation_functions/doorlock_normal_open.svg"; + static const String assetsDoubleLock = "assets/icons/functions_icons/automation_functions/double_lock.svg"; + static const String assetsFingerprintUnlock = + "assets/icons/functions_icons/automation_functions/fingerprint_unlock.svg"; + static const String assetsHijackAlarm = "assets/icons/functions_icons/automation_functions/hijack_alarm.svg"; + static const String assetsLockAlarm = "assets/icons/functions_icons/automation_functions/lock_alarm.svg"; + static const String assetsPasswordUnlock = "assets/icons/functions_icons/automation_functions/password_unlock.svg"; + static const String assetsRemoteUnlockReq = "assets/icons/functions_icons/automation_functions/remote_unlock_req.svg"; + static const String assetsRemoteUnlockViaApp = + "assets/icons/functions_icons/automation_functions/remote_unlock_via_app.svg"; + static const String assetsResidualElectricity = + "assets/icons/functions_icons/automation_functions/residual_electricity.svg"; + static const String assetsTempPasswordUnlock = + "assets/icons/functions_icons/automation_functions/temp_password_unlock.svg"; + static const String assetsSelfTestResult = "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 assetsCurrentTemp = "assets/icons/functions_icons/automation_functions/current_temp.svg"; + static const String assetsPresenceState = "assets/icons/functions_icons/automation_functions/presence_state.svg"; } From 4441878bddd1f6d96cf1cd26c714d283ea7fde47 Mon Sep 17 00:00:00 2001 From: ashrafzarkanisala Date: Thu, 21 Nov 2024 00:50:06 +0300 Subject: [PATCH 06/23] push ac functions and gang switches functions --- .vscode/settings.json | 5 + .../all_devices/models/devices_model.dart | 58 +- lib/pages/routiens/helper/ac_helper.dart | 541 ++++++++++++------ .../dialog_helper/device_dialog_helper.dart | 53 ++ .../helper/one_gang_switch_helper.dart | 206 +++++++ .../helper/three_gang_switch_helper.dart | 211 +++++++ .../helper/two_gang_switch_helper.dart | 209 +++++++ .../gang_switches/base_switch_function.dart | 17 + .../one_gang_switch/one_gang_switch.dart | 57 ++ .../switch_operational_value.dart | 17 + .../three_gang_switch/three_gang_switch.dart | 168 ++++++ .../two_gang_switch/two_gang_switch.dart | 115 ++++ lib/pages/routiens/widgets/dragable_card.dart | 11 +- lib/pages/routiens/widgets/if_container.dart | 23 +- .../routiens/widgets/routine_devices.dart | 9 +- .../widgets/scenes_and_automations.dart | 7 +- .../routiens/widgets/then_container.dart | 21 +- .../Runner/Logs/Build/.dat.nosync1585.W9c579 | 95 +++ ...-9521-4D0C-959C-43A07F62CC12.xcactivitylog | Bin 0 -> 217 bytes ...-C79D-4D4B-891A-12C476DFCB10.xcactivitylog | Bin 0 -> 209 bytes .../Runner/Logs/Build/LogStoreManifest.plist | 53 ++ .../Runner/Logs/Launch/LogStoreManifest.plist | 10 + .../Logs/Localization/LogStoreManifest.plist | 10 + .../Logs/Package/LogStoreManifest.plist | 10 + .../Runner/Logs/Test/LogStoreManifest.plist | 10 + macos/DerivedData/Runner/info.plist | 10 + pubspec.yaml | 1 + 27 files changed, 1739 insertions(+), 188 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 lib/pages/routiens/helper/dialog_helper/device_dialog_helper.dart create mode 100644 lib/pages/routiens/helper/one_gang_switch_helper.dart create mode 100644 lib/pages/routiens/helper/three_gang_switch_helper.dart create mode 100644 lib/pages/routiens/helper/two_gang_switch_helper.dart create mode 100644 lib/pages/routiens/models/gang_switches/base_switch_function.dart create mode 100644 lib/pages/routiens/models/gang_switches/one_gang_switch/one_gang_switch.dart create mode 100644 lib/pages/routiens/models/gang_switches/switch_operational_value.dart create mode 100644 lib/pages/routiens/models/gang_switches/three_gang_switch/three_gang_switch.dart create mode 100644 lib/pages/routiens/models/gang_switches/two_gang_switch/two_gang_switch.dart create mode 100644 macos/DerivedData/Runner/Logs/Build/.dat.nosync1585.W9c579 create mode 100644 macos/DerivedData/Runner/Logs/Build/DEC061F9-9521-4D0C-959C-43A07F62CC12.xcactivitylog create mode 100644 macos/DerivedData/Runner/Logs/Build/FB42CDDD-C79D-4D4B-891A-12C476DFCB10.xcactivitylog create mode 100644 macos/DerivedData/Runner/Logs/Build/LogStoreManifest.plist create mode 100644 macos/DerivedData/Runner/Logs/Launch/LogStoreManifest.plist create mode 100644 macos/DerivedData/Runner/Logs/Localization/LogStoreManifest.plist create mode 100644 macos/DerivedData/Runner/Logs/Package/LogStoreManifest.plist create mode 100644 macos/DerivedData/Runner/Logs/Test/LogStoreManifest.plist create mode 100644 macos/DerivedData/Runner/info.plist diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..b87628bd --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "cSpell.words": [ + "automations" + ] +} \ No newline at end of file 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 b37ff36c..367a4fe8 100644 --- a/lib/pages/device_managment/all_devices/models/devices_model.dart +++ b/lib/pages/device_managment/all_devices/models/devices_model.dart @@ -4,6 +4,9 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/room.dart' import 'package:syncrow_web/pages/device_managment/all_devices/models/unit.dart'; import 'package:syncrow_web/pages/routiens/models/ac/ac_function.dart'; import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; +import 'package:syncrow_web/pages/routiens/models/gang_switches/one_gang_switch/one_gang_switch.dart'; +import 'package:syncrow_web/pages/routiens/models/gang_switches/three_gang_switch/three_gang_switch.dart'; +import 'package:syncrow_web/pages/routiens/models/gang_switches/two_gang_switch/two_gang_switch.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/enum/device_types.dart'; @@ -71,7 +74,6 @@ class AllDevicesModel { int? batteryLevel; String? productName; List? spaces; - List? _deviceFunctions; AllDevicesModel({ this.room, @@ -104,8 +106,12 @@ class AllDevicesModel { this.spaces, }); AllDevicesModel.fromJson(Map json) { - room = (json['room'] != null && (json['room'] is Map)) ? DevicesModelRoom.fromJson(json['room']) : null; - unit = (json['unit'] != null && (json['unit'] is Map)) ? DevicesModelUnit.fromJson(json['unit']) : null; + room = (json['room'] != null && (json['room'] is Map)) + ? DevicesModelRoom.fromJson(json['room']) + : null; + unit = (json['unit'] != null && (json['unit'] is Map)) + ? DevicesModelUnit.fromJson(json['unit']) + : null; community = (json['community'] != null && (json['community'] is Map)) ? DeviceCommunityModel.fromJson(json['community']) : null; @@ -134,7 +140,9 @@ class AllDevicesModel { batteryLevel = int.tryParse(json['battery']?.toString() ?? ''); productName = json['productName']?.toString(); if (json['spaces'] != null && json['spaces'] is List) { - spaces = (json['spaces'] as List).map((space) => DeviceSpaceModel.fromJson(space)).toList(); + spaces = (json['spaces'] as List) + .map((space) => DeviceSpaceModel.fromJson(space)) + .toList(); } } @@ -182,7 +190,8 @@ SOS String tempIcon = ''; if (type == DeviceType.LightBulb) { tempIcon = Assets.lightBulb; - } else if (type == DeviceType.CeilingSensor || type == DeviceType.WallSensor) { + } else if (type == DeviceType.CeilingSensor || + type == DeviceType.WallSensor) { tempIcon = Assets.sensors; } else if (type == DeviceType.AC) { tempIcon = Assets.ac; @@ -218,11 +227,11 @@ SOS return tempIcon; } - List get deviceFunctions { - _deviceFunctions ??= _getDeviceFunctions(); - return _deviceFunctions!; + List get functions { + return _getDeviceFunctions(); } + //! Functions for Devices Types List _getDeviceFunctions() { switch (productType) { case 'AC': @@ -234,7 +243,38 @@ SOS ChildLockFunction(deviceId: uuid ?? '', deviceName: name ?? ''), ]; - // other product types + case '1G': + return [ + OneGangSwitchFunction(deviceId: uuid ?? '', deviceName: name ?? ''), + OneGangCountdownFunction( + deviceId: uuid ?? '', deviceName: name ?? ''), + ]; + + case '2G': + return [ + TwoGangSwitch1Function(deviceId: uuid ?? '', deviceName: name ?? ''), + TwoGangSwitch2Function(deviceId: uuid ?? '', deviceName: name ?? ''), + TwoGangCountdown1Function( + deviceId: uuid ?? '', deviceName: name ?? ''), + TwoGangCountdown2Function( + deviceId: uuid ?? '', deviceName: name ?? ''), + ]; + + case '3G': + return [ + ThreeGangSwitch1Function( + deviceId: uuid ?? '', deviceName: name ?? ''), + ThreeGangSwitch2Function( + deviceId: uuid ?? '', deviceName: name ?? ''), + ThreeGangSwitch3Function( + deviceId: uuid ?? '', deviceName: name ?? ''), + ThreeGangCountdown1Function( + deviceId: uuid ?? '', deviceName: name ?? ''), + ThreeGangCountdown2Function( + deviceId: uuid ?? '', deviceName: name ?? ''), + ThreeGangCountdown3Function( + deviceId: uuid ?? '', deviceName: name ?? ''), + ]; default: return []; } diff --git a/lib/pages/routiens/helper/ac_helper.dart b/lib/pages/routiens/helper/ac_helper.dart index 6cdb415d..4c216a89 100644 --- a/lib/pages/routiens/helper/ac_helper.dart +++ b/lib/pages/routiens/helper/ac_helper.dart @@ -1,154 +1,39 @@ 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/routiens/bloc/routine_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:syncrow_web/pages/routiens/models/ac/ac_function.dart'; import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; -mixin ACHelper { - Future?> showACFunctionsDialog(BuildContext context, List> functions) { +class ACHelper { + static Future?> showACFunctionsDialog( + BuildContext context, + List> functions, + ) async { List acFunctions = functions.whereType().toList(); String? selectedFunction; - dynamic selectedValue; + dynamic selectedValue = 20; + String? selectedCondition = "=="; + List _selectedConditions = [false, true, false]; - return showDialog( + return showDialog?>( context: context, builder: (BuildContext context) { return StatefulBuilder( builder: (context, setState) { return AlertDialog( contentPadding: EdgeInsets.zero, - content: Container( - width: selectedFunction != null ? 600 : 360, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(20), - ), - padding: const EdgeInsets.only(top: 20), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'AC Functions', - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: ColorsManager.primaryColorWithOpacity, - fontWeight: FontWeight.bold, - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 50), - child: Container( - height: 1, - width: double.infinity, - color: ColorsManager.greyColor, - ), - ), - Flexible( - child: Row( - children: [ - Expanded( - child: ListView.separated( - shrinkWrap: true, - itemCount: acFunctions.length, - separatorBuilder: (context, index) => Divider(), - itemBuilder: (context, index) { - final function = acFunctions[index]; - return ListTile( - leading: Image.asset(function.icon, width: 24, height: 24), - title: Text(function.operationName), - trailing: Icon(Icons.arrow_forward_ios), - onTap: () { - setState(() { - selectedFunction = function.code; - selectedValue = null; - }); - }, - ); - }, - ), - ), - if (selectedFunction != null) - Container( - width: 1, - color: ColorsManager.greyColor, - ), - if (selectedFunction != null) - Expanded( - child: ListView.separated( - shrinkWrap: true, - itemCount: acFunctions - .firstWhere((f) => f.code == selectedFunction) - .getOperationalValues() - .length, - separatorBuilder: (context, index) => Divider(), - itemBuilder: (context, index) { - final operationalValue = acFunctions.firstWhere((f) => f.code == selectedFunction) - ..getOperationalValues()[index]; - return ListTile( - leading: Image.asset(operationalValue.icon, width: 24, height: 24), - title: Text(operationalValue.getOperationalValues()[index].description), - trailing: Radio( - value: operationalValue.getOperationalValues()[index].value, - groupValue: selectedValue, - onChanged: (value) { - setState(() { - selectedValue = value; - }); - }, - ), - ); - }, - ), - ), - ], - ), - ), - Container( - height: 1, - width: double.infinity, - color: ColorsManager.greyColor, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - GestureDetector( - onTap: () { - Navigator.pop(context); - }, - child: Center( - child: Text( - 'Cancel', - style: Theme.of(context).textTheme.bodyMedium!.copyWith(color: ColorsManager.greyColor), - ), - ), - ), - Container( - height: 50, - width: 1, - color: ColorsManager.greyColor, - ), - GestureDetector( - onTap: () { - // Handle the confirmation action here - Navigator.pop(context, { - 'function': selectedFunction, - 'value': selectedValue, - }); - }, - child: Center( - child: Text( - 'Confirm', - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: ColorsManager.primaryColorWithOpacity, - ), - ), - ), - ), - ], - ), - ], - ), + content: _buildDialogContent( + context, + setState, + acFunctions, + selectedFunction, + selectedValue, + selectedCondition, + _selectedConditions, + (fn) => selectedFunction = fn, + (val) => selectedValue = val, + (cond) => selectedCondition = cond, ), ); }, @@ -157,34 +42,370 @@ mixin ACHelper { ); } - void handleACDeviceDrop(BuildContext context, Map data) { - final device = data['device'] as AllDevicesModel; - final acFunctions = device.deviceFunctions; - - showACFunctionsDialog(context, acFunctions).then((result) { - if (result != null) { - _addACDeviceToRoutine(context, data, result); - } - }); + /// Build dialog content for AC functions dialog + static Widget _buildDialogContent( + BuildContext context, + StateSetter setState, + List acFunctions, + String? selectedFunction, + dynamic selectedValue, + String? selectedCondition, + List selectedConditions, + Function(String?) onFunctionSelected, + Function(dynamic) onValueSelected, + Function(String?) onConditionSelected, + ) { + 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: [ + _buildDialogHeader(context), + Flexible( + child: Row( + children: [ + _buildFunctionsList( + context, + setState, + acFunctions, + selectedFunction, + onFunctionSelected, + ), + if (selectedFunction != null) + _buildValueSelector( + context, + setState, + selectedFunction, + selectedValue, + selectedCondition, + selectedConditions, + onValueSelected, + onConditionSelected, + acFunctions, + ), + ], + ), + ), + _buildDialogFooter( + context, + selectedFunction, + selectedValue, + selectedCondition, + ), + ], + ), + ); } - void handleNonACDeviceDrop(BuildContext context, Map data) { - context.read().add(AddToThenContainer(data)); + /// Build header for AC functions dialog + static Widget _buildDialogHeader(BuildContext context) { + return Column( + children: [ + Text( + 'AC Condition', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.primaryColorWithOpacity, + fontWeight: FontWeight.bold, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 50), + child: Container( + height: 1, + width: double.infinity, + color: ColorsManager.greyColor, + ), + ), + ], + ); } - void _addACDeviceToRoutine(BuildContext context, Map deviceData, Map functionData) { - final updatedData = { - ...deviceData, - 'function': functionData['function'], - 'value': functionData['value'], - }; - - context.read().add(AddToThenContainer(updatedData)); - - _logACFunctionSelection(functionData); + /// Build functions list for AC functions dialog + static Widget _buildFunctionsList( + BuildContext context, + StateSetter setState, + List acFunctions, + String? selectedFunction, + Function(String?) onFunctionSelected, + ) { + return Expanded( + child: ListView.separated( + shrinkWrap: false, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: acFunctions.length, + separatorBuilder: (context, index) => const Divider( + color: ColorsManager.dividerColor, + ), + itemBuilder: (context, index) { + final function = acFunctions[index]; + return ListTile( + leading: SvgPicture.asset( + function.icon, + 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: () => setState(() => onFunctionSelected(function.code)), + ); + }, + ), + ); } - void _logACFunctionSelection(Map functionData) { - print('Selected AC function: ${functionData['function']}, Value: ${functionData['value']}'); + /// Build value selector for AC functions dialog + static Widget _buildValueSelector( + BuildContext context, + StateSetter setState, + String selectedFunction, + dynamic selectedValue, + String? selectedCondition, + List selectedConditions, + Function(dynamic) onValueSelected, + Function(String?) onConditionSelected, + List acFunctions, + ) { + if (selectedFunction == 'temp_set' || selectedFunction == 'temp_current') { + return Expanded( + child: _buildTemperatureSelector( + context, + setState, + selectedValue, + selectedCondition, + selectedConditions, + onValueSelected, + onConditionSelected, + ), + ); + } + + final selectedFn = + acFunctions.firstWhere((f) => f.code == selectedFunction); + final values = selectedFn.getOperationalValues(); + return Expanded( + child: _buildOperationalValuesList( + context, + setState, + values, + selectedValue, + onValueSelected, + ), + ); + } + + /// Build temperature selector for AC functions dialog + static Widget _buildTemperatureSelector( + BuildContext context, + StateSetter setState, + dynamic selectedValue, + String? selectedCondition, + List selectedConditions, + Function(dynamic) onValueSelected, + Function(String?) onConditionSelected, + ) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildConditionToggle( + context, + setState, + selectedConditions, + onConditionSelected, + ), + const SizedBox(height: 20), + _buildTemperatureDisplay(context, selectedValue), + const SizedBox(height: 20), + _buildTemperatureSlider( + context, + setState, + selectedValue, + onValueSelected, + ), + ], + ); + } + + /// Build condition toggle for AC functions dialog + static Widget _buildConditionToggle( + BuildContext context, + StateSetter setState, + List selectedConditions, + Function(String?) onConditionSelected, + ) { + return ToggleButtons( + onPressed: (int index) { + setState(() { + for (int i = 0; i < selectedConditions.length; i++) { + selectedConditions[i] = i == index; + } + onConditionSelected(index == 0 + ? "<" + : index == 1 + ? "==" + : ">"); + }); + }, + 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: selectedConditions, + children: const [Text("<"), Text("="), Text(">")], + ); + } + + /// Build temperature display for AC functions dialog + static Widget _buildTemperatureDisplay( + BuildContext context, dynamic selectedValue) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + decoration: BoxDecoration( + color: ColorsManager.primaryColorWithOpacity.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + '${selectedValue ?? 20}°C', + style: context.textTheme.headlineMedium!.copyWith( + color: ColorsManager.primaryColorWithOpacity, + ), + ), + ); + } + + static Widget _buildTemperatureSlider( + BuildContext context, + StateSetter setState, + dynamic selectedValue, + Function(dynamic) onValueSelected, + ) { + final currentValue = selectedValue is int ? selectedValue.toDouble() : 20.0; + return Slider( + value: currentValue, + min: 16, + max: 30, + divisions: 14, + label: '${currentValue.toInt()}°C', + onChanged: (value) { + setState(() => onValueSelected(value.toInt())); + }, + ); + } + + static Widget _buildOperationalValuesList( + BuildContext context, + StateSetter setState, + List values, + dynamic selectedValue, + Function(dynamic) onValueSelected, + ) { + return ListView.builder( + shrinkWrap: false, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: values.length, + itemBuilder: (context, index) { + final value = values[index]; + return ListTile( + leading: SvgPicture.asset( + value.icon, + width: 24, + height: 24, + ), + title: Text( + value.description, + style: context.textTheme.bodyMedium, + ), + trailing: Radio( + value: value.value, + groupValue: selectedValue, + onChanged: (newValue) { + setState(() => onValueSelected(newValue)); + }, + ), + ); + }, + ); + } + + static Widget _buildDialogFooter( + BuildContext context, + String? selectedFunction, + dynamic selectedValue, + String? selectedCondition, + ) { + return Container( + decoration: const BoxDecoration( + border: Border( + top: BorderSide( + color: ColorsManager.greyColor, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildFooterButton( + context, + 'Cancel', + selectedFunction != null ? 299 : 179, + () => Navigator.pop(context), + ), + _buildFooterButton( + context, + 'Confirm', + selectedFunction != null ? 299 : 179, + selectedFunction != null && selectedValue != null + ? () => Navigator.pop(context, { + 'function': selectedFunction, + 'value': selectedValue, + 'condition': selectedCondition ?? "==", + }) + : null, + ), + ], + ), + ); + } + + static Widget _buildFooterButton( + BuildContext context, + String text, + double width, + VoidCallback? onTap, + ) { + return GestureDetector( + onTap: onTap, + child: SizedBox( + height: 50, + width: width, + child: Center( + child: Text( + text, + style: context.textTheme.bodyMedium!.copyWith( + color: onTap != null + ? ColorsManager.primaryColorWithOpacity + : ColorsManager.textGray, + ), + ), + ), + ), + ); } } diff --git a/lib/pages/routiens/helper/dialog_helper/device_dialog_helper.dart b/lib/pages/routiens/helper/dialog_helper/device_dialog_helper.dart new file mode 100644 index 00000000..0046db8a --- /dev/null +++ b/lib/pages/routiens/helper/dialog_helper/device_dialog_helper.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/routiens/helper/ac_helper.dart'; +import 'package:syncrow_web/pages/routiens/helper/one_gang_switch_helper.dart'; +import 'package:syncrow_web/pages/routiens/helper/three_gang_switch_helper.dart'; +import 'package:syncrow_web/pages/routiens/helper/two_gang_switch_helper.dart'; +import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; + +class DeviceDialogHelper { + static Future?> showDeviceDialog( + BuildContext context, + Map data, + ) async { + final functions = data['functions'] as List; + + try { + final result = await _getDialogForDeviceType( + context, + data['productType'], + functions, + ); + + if (result != null) { + return {...data, ...result}; + } + } catch (e) { + debugPrint('Error: $e'); + } + + return null; + } + + static Future?> _getDialogForDeviceType( + BuildContext context, + String productType, + List functions, + ) async { + switch (productType) { + case 'AC': + return ACHelper.showACFunctionsDialog(context, functions); + case '1G': + return OneGangSwitchHelper.showSwitchFunctionsDialog( + context, functions); + case '2G': + return TwoGangSwitchHelper.showSwitchFunctionsDialog( + context, functions); + case '3G': + return ThreeGangSwitchHelper.showSwitchFunctionsDialog( + context, functions); + default: + return null; + } + } +} diff --git a/lib/pages/routiens/helper/one_gang_switch_helper.dart b/lib/pages/routiens/helper/one_gang_switch_helper.dart new file mode 100644 index 00000000..30f25a96 --- /dev/null +++ b/lib/pages/routiens/helper/one_gang_switch_helper.dart @@ -0,0 +1,206 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; +import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart'; +import 'package:syncrow_web/pages/routiens/models/gang_switches/one_gang_switch/one_gang_switch.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class OneGangSwitchHelper { + static Future?> showSwitchFunctionsDialog( + BuildContext context, List> functions) async { + List> switchFunctions = functions + .where( + (f) => f is OneGangSwitchFunction || f is OneGangCountdownFunction) + .toList(); + String? selectedFunction; + dynamic selectedValue; + + return showDialog?>( + context: context, + builder: (BuildContext context) { + return StatefulBuilder( + builder: (context, setState) { + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: 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: [ + Text( + '1 Gang Light Switch Condition', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.primaryColorWithOpacity, + fontWeight: FontWeight.bold, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 15, horizontal: 50), + child: Container( + height: 1, + width: double.infinity, + color: ColorsManager.greyColor, + ), + ), + Flexible( + child: Row( + children: [ + Expanded( + child: ListView.separated( + shrinkWrap: false, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: switchFunctions.length, + separatorBuilder: (context, index) => + const Divider( + color: ColorsManager.dividerColor, + ), + itemBuilder: (context, index) { + final function = switchFunctions[index]; + return ListTile( + leading: SvgPicture.asset( + function.icon, + 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: () { + setState(() { + selectedFunction = function.code; + selectedValue = null; + }); + }, + ); + }, + ), + ), + if (selectedFunction != null) + Expanded( + child: Builder( + builder: (context) { + final selectedFn = switchFunctions.firstWhere( + (f) => f.code == selectedFunction) + as BaseSwitchFunction; + final values = + selectedFn.getOperationalValues(); + return ListView.builder( + shrinkWrap: false, + physics: + const AlwaysScrollableScrollPhysics(), + itemCount: values.length, + itemBuilder: (context, index) { + final value = values[index]; + return ListTile( + leading: SvgPicture.asset( + value.icon, + width: 24, + height: 24, + ), + title: Text( + value.description, + style: context.textTheme.bodyMedium, + ), + trailing: Radio( + value: value.value, + groupValue: selectedValue, + onChanged: (newValue) { + setState(() { + selectedValue = newValue; + }); + }, + ), + ); + }, + ); + }, + ), + ), + ], + ), + ), + Container( + height: 1, + width: double.infinity, + color: ColorsManager.greyColor, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: Container( + height: 50, + width: selectedFunction != null ? 299 : 179, + decoration: const BoxDecoration( + border: Border( + right: + BorderSide(color: ColorsManager.greyColor), + ), + ), + child: Center( + child: Text( + 'Cancel', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: ColorsManager.greyColor), + ), + ), + ), + ), + GestureDetector( + onTap: () { + if (selectedFunction != null && + selectedValue != null) { + Navigator.pop(context, { + 'function': selectedFunction, + 'value': selectedValue, + }); + } + }, + child: SizedBox( + height: 50, + width: selectedFunction != null ? 299 : 179, + child: Center( + child: Text( + 'Confirm', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: + ColorsManager.primaryColorWithOpacity, + ), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ); + }, + ); + }, + ); + } +} diff --git a/lib/pages/routiens/helper/three_gang_switch_helper.dart b/lib/pages/routiens/helper/three_gang_switch_helper.dart new file mode 100644 index 00000000..89aa2d95 --- /dev/null +++ b/lib/pages/routiens/helper/three_gang_switch_helper.dart @@ -0,0 +1,211 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; +import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart'; +import 'package:syncrow_web/pages/routiens/models/gang_switches/three_gang_switch/three_gang_switch.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class ThreeGangSwitchHelper { + static Future?> showSwitchFunctionsDialog( + BuildContext context, List> functions) async { + List> switchFunctions = functions + .where((f) => + f is ThreeGangSwitch1Function || + f is ThreeGangSwitch2Function || + f is ThreeGangSwitch3Function || + f is ThreeGangCountdown1Function || + f is ThreeGangCountdown2Function || + f is ThreeGangCountdown3Function) + .toList(); + String? selectedFunction; + dynamic selectedValue; + + return showDialog?>( + context: context, + builder: (BuildContext context) { + return StatefulBuilder( + builder: (context, setState) { + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: 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: [ + Text( + '3 Gangs Light Switch Condition', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.primaryColorWithOpacity, + fontWeight: FontWeight.bold, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 15, horizontal: 50), + child: Container( + height: 1, + width: double.infinity, + color: ColorsManager.greyColor, + ), + ), + Flexible( + child: Row( + children: [ + Expanded( + child: ListView.separated( + shrinkWrap: false, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: switchFunctions.length, + separatorBuilder: (context, index) => + const Divider( + color: ColorsManager.dividerColor, + ), + itemBuilder: (context, index) { + final function = switchFunctions[index]; + return ListTile( + leading: SvgPicture.asset( + function.icon, + 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: () { + setState(() { + selectedFunction = function.code; + selectedValue = null; + }); + }, + ); + }, + ), + ), + if (selectedFunction != null) + Expanded( + child: Builder( + builder: (context) { + final selectedFn = switchFunctions.firstWhere( + (f) => f.code == selectedFunction) + as BaseSwitchFunction; + final values = + selectedFn.getOperationalValues(); + return ListView.builder( + shrinkWrap: false, + physics: + const AlwaysScrollableScrollPhysics(), + itemCount: values.length, + itemBuilder: (context, index) { + final value = values[index]; + return ListTile( + leading: SvgPicture.asset( + value.icon, + width: 24, + height: 24, + ), + title: Text( + value.description, + style: context.textTheme.bodyMedium, + ), + trailing: Radio( + value: value.value, + groupValue: selectedValue, + onChanged: (newValue) { + setState(() { + selectedValue = newValue; + }); + }, + ), + ); + }, + ); + }, + ), + ), + ], + ), + ), + Container( + height: 1, + width: double.infinity, + color: ColorsManager.greyColor, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: Container( + height: 50, + width: selectedFunction != null ? 299 : 179, + decoration: const BoxDecoration( + border: Border( + right: + BorderSide(color: ColorsManager.greyColor), + ), + ), + child: Center( + child: Text( + 'Cancel', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: ColorsManager.greyColor), + ), + ), + ), + ), + GestureDetector( + onTap: () { + if (selectedFunction != null && + selectedValue != null) { + Navigator.pop(context, { + 'function': selectedFunction, + 'value': selectedValue, + }); + } + }, + child: SizedBox( + height: 50, + width: selectedFunction != null ? 299 : 179, + child: Center( + child: Text( + 'Confirm', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: + ColorsManager.primaryColorWithOpacity, + ), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ); + }, + ); + }, + ); + } +} diff --git a/lib/pages/routiens/helper/two_gang_switch_helper.dart b/lib/pages/routiens/helper/two_gang_switch_helper.dart new file mode 100644 index 00000000..1d271ac7 --- /dev/null +++ b/lib/pages/routiens/helper/two_gang_switch_helper.dart @@ -0,0 +1,209 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; +import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart'; +import 'package:syncrow_web/pages/routiens/models/gang_switches/two_gang_switch/two_gang_switch.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class TwoGangSwitchHelper { + static Future?> showSwitchFunctionsDialog( + BuildContext context, List> functions) async { + List> switchFunctions = functions + .where((f) => + f is TwoGangSwitch1Function || + f is TwoGangSwitch2Function || + f is TwoGangCountdown1Function || + f is TwoGangCountdown2Function) + .toList(); + String? selectedFunction; + dynamic selectedValue; + + return showDialog?>( + context: context, + builder: (BuildContext context) { + return StatefulBuilder( + builder: (context, setState) { + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: 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: [ + Text( + '2 Gangs Light Switch Condition', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.primaryColorWithOpacity, + fontWeight: FontWeight.bold, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 15, horizontal: 50), + child: Container( + height: 1, + width: double.infinity, + color: ColorsManager.greyColor, + ), + ), + Flexible( + child: Row( + children: [ + Expanded( + child: ListView.separated( + shrinkWrap: false, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: switchFunctions.length, + separatorBuilder: (context, index) => + const Divider( + color: ColorsManager.dividerColor, + ), + itemBuilder: (context, index) { + final function = switchFunctions[index]; + return ListTile( + leading: SvgPicture.asset( + function.icon, + 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: () { + setState(() { + selectedFunction = function.code; + selectedValue = null; + }); + }, + ); + }, + ), + ), + if (selectedFunction != null) + Expanded( + child: Builder( + builder: (context) { + final selectedFn = switchFunctions.firstWhere( + (f) => f.code == selectedFunction) + as BaseSwitchFunction; + final values = + selectedFn.getOperationalValues(); + return ListView.builder( + shrinkWrap: false, + physics: + const AlwaysScrollableScrollPhysics(), + itemCount: values.length, + itemBuilder: (context, index) { + final value = values[index]; + return ListTile( + leading: SvgPicture.asset( + value.icon, + width: 24, + height: 24, + ), + title: Text( + value.description, + style: context.textTheme.bodyMedium, + ), + trailing: Radio( + value: value.value, + groupValue: selectedValue, + onChanged: (newValue) { + setState(() { + selectedValue = newValue; + }); + }, + ), + ); + }, + ); + }, + ), + ), + ], + ), + ), + Container( + height: 1, + width: double.infinity, + color: ColorsManager.greyColor, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: Container( + height: 50, + width: selectedFunction != null ? 299 : 179, + decoration: const BoxDecoration( + border: Border( + right: + BorderSide(color: ColorsManager.greyColor), + ), + ), + child: Center( + child: Text( + 'Cancel', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: ColorsManager.greyColor), + ), + ), + ), + ), + GestureDetector( + onTap: () { + if (selectedFunction != null && + selectedValue != null) { + Navigator.pop(context, { + 'function': selectedFunction, + 'value': selectedValue, + }); + } + }, + child: SizedBox( + height: 50, + width: selectedFunction != null ? 299 : 179, + child: Center( + child: Text( + 'Confirm', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: + ColorsManager.primaryColorWithOpacity, + ), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ); + }, + ); + }, + ); + } +} diff --git a/lib/pages/routiens/models/gang_switches/base_switch_function.dart b/lib/pages/routiens/models/gang_switches/base_switch_function.dart new file mode 100644 index 00000000..3c89bebd --- /dev/null +++ b/lib/pages/routiens/models/gang_switches/base_switch_function.dart @@ -0,0 +1,17 @@ +import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; +import 'package:syncrow_web/pages/routiens/models/gang_switches/switch_operational_value.dart'; + +abstract class BaseSwitchFunction extends DeviceFunction { + BaseSwitchFunction({ + required super.deviceId, + required super.deviceName, + required super.code, + required super.operationName, + required super.icon, + }); + + @override + bool execute(bool currentStatus, dynamic newValue); + + List getOperationalValues(); +} diff --git a/lib/pages/routiens/models/gang_switches/one_gang_switch/one_gang_switch.dart b/lib/pages/routiens/models/gang_switches/one_gang_switch/one_gang_switch.dart new file mode 100644 index 00000000..32bc436e --- /dev/null +++ b/lib/pages/routiens/models/gang_switches/one_gang_switch/one_gang_switch.dart @@ -0,0 +1,57 @@ +import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart'; +import 'package:syncrow_web/pages/routiens/models/gang_switches/switch_operational_value.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class OneGangSwitchFunction extends BaseSwitchFunction { + OneGangSwitchFunction({required super.deviceId, required super.deviceName}) + : super( + code: 'switch_1', + operationName: 'Light Switch', + icon: Assets.assetsAcPower, + ); + + @override + bool execute(bool currentStatus, dynamic newValue) { + return newValue as bool; + } + + @override + List getOperationalValues() => [ + SwitchOperationalValue( + icon: Assets.assetsAcPower, + description: "ON", + value: true, + ), + SwitchOperationalValue( + icon: Assets.assetsAcPowerOFF, + description: "OFF", + value: false, + ), + ]; +} + +class OneGangCountdownFunction extends BaseSwitchFunction { + OneGangCountdownFunction({required super.deviceId, required super.deviceName}) + : super( + code: 'countdown_1', + operationName: 'Light Countdown', + icon: Assets.assetsLightCountdown, + ); + + @override + bool execute(bool currentStatus, dynamic newValue) { + return newValue as bool; + } + + @override + List getOperationalValues() => [ + SwitchOperationalValue( + icon: '', + description: "sec", + value: 0.0, + minValue: 0, + maxValue: 43200, + stepValue: 1, + ), + ]; +} diff --git a/lib/pages/routiens/models/gang_switches/switch_operational_value.dart b/lib/pages/routiens/models/gang_switches/switch_operational_value.dart new file mode 100644 index 00000000..eabd4a35 --- /dev/null +++ b/lib/pages/routiens/models/gang_switches/switch_operational_value.dart @@ -0,0 +1,17 @@ +class SwitchOperationalValue { + final String icon; + final String description; + final dynamic value; + final double? minValue; + final double? maxValue; + final double? stepValue; + + SwitchOperationalValue({ + required this.icon, + required this.value, + this.description = '', + this.minValue, + this.maxValue, + this.stepValue, + }); +} diff --git a/lib/pages/routiens/models/gang_switches/three_gang_switch/three_gang_switch.dart b/lib/pages/routiens/models/gang_switches/three_gang_switch/three_gang_switch.dart new file mode 100644 index 00000000..8ff186ef --- /dev/null +++ b/lib/pages/routiens/models/gang_switches/three_gang_switch/three_gang_switch.dart @@ -0,0 +1,168 @@ +import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart'; +import 'package:syncrow_web/pages/routiens/models/gang_switches/switch_operational_value.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class ThreeGangSwitch1Function extends BaseSwitchFunction { + ThreeGangSwitch1Function({required super.deviceId, required super.deviceName}) + : super( + code: 'switch_1', + operationName: 'Light 1 Switch', + icon: Assets.assetsAcPower, + ); + + @override + bool execute(bool currentStatus, dynamic newValue) { + return newValue as bool; + } + + @override + List getOperationalValues() => [ + SwitchOperationalValue( + icon: Assets.assetsAcPower, + description: "ON", + value: true, + ), + SwitchOperationalValue( + icon: Assets.assetsAcPowerOFF, + description: "OFF", + value: false, + ), + ]; +} + +class ThreeGangCountdown1Function extends BaseSwitchFunction { + ThreeGangCountdown1Function( + {required super.deviceId, required super.deviceName}) + : super( + code: 'countdown_1', + operationName: 'Light 1 Countdown', + icon: Assets.assetsLightCountdown, + ); + + @override + bool execute(bool currentStatus, dynamic newValue) { + return newValue as bool; + } + + @override + List getOperationalValues() => [ + SwitchOperationalValue( + icon: '', + description: "sec", + value: 0.0, + minValue: 0, + maxValue: 43200, + stepValue: 1, + ), + ]; +} + +class ThreeGangSwitch2Function extends BaseSwitchFunction { + ThreeGangSwitch2Function({required super.deviceId, required super.deviceName}) + : super( + code: 'switch_2', + operationName: 'Light 2 Switch', + icon: Assets.assetsAcPower, + ); + + @override + bool execute(bool currentStatus, dynamic newValue) { + return newValue as bool; + } + + @override + List getOperationalValues() => [ + SwitchOperationalValue( + icon: Assets.assetsAcPower, + description: "ON", + value: true, + ), + SwitchOperationalValue( + icon: Assets.assetsAcPowerOFF, + description: "OFF", + value: false, + ), + ]; +} + +class ThreeGangCountdown2Function extends BaseSwitchFunction { + ThreeGangCountdown2Function( + {required super.deviceId, required super.deviceName}) + : super( + code: 'countdown_2', + operationName: 'Light 2 Countdown', + icon: Assets.assetsLightCountdown, + ); + + @override + bool execute(bool currentStatus, dynamic newValue) { + return newValue as bool; + } + + @override + List getOperationalValues() => [ + SwitchOperationalValue( + icon: '', + description: "sec", + value: 0.0, + minValue: 0, + maxValue: 43200, + stepValue: 1, + ), + ]; +} + +class ThreeGangSwitch3Function extends BaseSwitchFunction { + ThreeGangSwitch3Function({required super.deviceId, required super.deviceName}) + : super( + code: 'switch_3', + operationName: 'Light 3 Switch', + icon: Assets.assetsAcPower, + ); + + @override + bool execute(bool currentStatus, dynamic newValue) { + return newValue as bool; + } + + @override + List getOperationalValues() => [ + SwitchOperationalValue( + icon: Assets.assetsAcPower, + description: "ON", + value: true, + ), + SwitchOperationalValue( + icon: Assets.assetsAcPowerOFF, + description: "OFF", + value: false, + ), + ]; +} + +class ThreeGangCountdown3Function extends BaseSwitchFunction { + ThreeGangCountdown3Function( + {required super.deviceId, required super.deviceName}) + : super( + code: 'countdown_3', + operationName: 'Light 3 Countdown', + icon: Assets.assetsLightCountdown, + ); + + @override + bool execute(bool currentStatus, dynamic newValue) { + return newValue as bool; + } + + @override + List getOperationalValues() => [ + SwitchOperationalValue( + icon: '', + description: "sec", + value: 0.0, + minValue: 0, + maxValue: 43200, + stepValue: 1, + ), + ]; +} diff --git a/lib/pages/routiens/models/gang_switches/two_gang_switch/two_gang_switch.dart b/lib/pages/routiens/models/gang_switches/two_gang_switch/two_gang_switch.dart new file mode 100644 index 00000000..7408d9d0 --- /dev/null +++ b/lib/pages/routiens/models/gang_switches/two_gang_switch/two_gang_switch.dart @@ -0,0 +1,115 @@ +import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart'; +import 'package:syncrow_web/pages/routiens/models/gang_switches/switch_operational_value.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class TwoGangSwitch1Function extends BaseSwitchFunction { + TwoGangSwitch1Function({required super.deviceId, required super.deviceName}) + : super( + code: 'switch_1', + operationName: 'Light 1 Switch', + icon: Assets.assetsAcPower, + ); + + @override + bool execute(bool currentStatus, dynamic newValue) { + return newValue as bool; + } + + @override + List getOperationalValues() => [ + SwitchOperationalValue( + icon: Assets.assetsAcPower, + description: "ON", + value: true, + ), + SwitchOperationalValue( + icon: Assets.assetsAcPowerOFF, + description: "OFF", + value: false, + ), + ]; +} + +class TwoGangSwitch2Function extends BaseSwitchFunction { + TwoGangSwitch2Function({required String deviceId, required String deviceName}) + : super( + deviceId: deviceId, + deviceName: deviceName, + code: 'switch_2', + operationName: 'Light 2 Switch', + icon: Assets.assetsAcPower, + ); + + @override + bool execute(bool currentStatus, dynamic newValue) { + return newValue as bool; + } + + @override + List getOperationalValues() => [ + SwitchOperationalValue( + icon: Assets.assetsAcPower, + description: "ON", + value: true, + ), + SwitchOperationalValue( + icon: Assets.assetsAcPowerOFF, + description: "OFF", + value: false, + ), + ]; +} + +class TwoGangCountdown1Function extends BaseSwitchFunction { + TwoGangCountdown1Function( + {required super.deviceId, required super.deviceName}) + : super( + code: 'countdown_1', + operationName: 'Light 1 Countdown', + icon: Assets.assetsLightCountdown, + ); + + @override + bool execute(bool currentStatus, dynamic newValue) { + return newValue as bool; + } + + @override + List getOperationalValues() => [ + SwitchOperationalValue( + icon: '', + description: "sec", + value: 0.0, + minValue: 0, + maxValue: 43200, + stepValue: 1, + ), + ]; +} + +class TwoGangCountdown2Function extends BaseSwitchFunction { + TwoGangCountdown2Function( + {required super.deviceId, required super.deviceName}) + : super( + code: 'countdown_2', + operationName: 'Light 2 Countdown', + icon: Assets.assetsLightCountdown, + ); + + @override + bool execute(bool currentStatus, dynamic newValue) { + return newValue as bool; + } + + @override + List getOperationalValues() => [ + SwitchOperationalValue( + icon: '', + description: "sec", + value: 0.0, + minValue: 0, + maxValue: 43200, + stepValue: 1, + ), + ]; +} diff --git a/lib/pages/routiens/widgets/dragable_card.dart b/lib/pages/routiens/widgets/dragable_card.dart index 2e7d1a0e..8ed4afff 100644 --- a/lib/pages/routiens/widgets/dragable_card.dart +++ b/lib/pages/routiens/widgets/dragable_card.dart @@ -11,6 +11,7 @@ class DraggableCard extends StatelessWidget { this.titleColor, this.isDragged = false, this.isDisabled = false, + this.deviceData, }); final String imagePath; @@ -18,11 +19,17 @@ class DraggableCard extends StatelessWidget { final Color? titleColor; final bool isDragged; final bool isDisabled; + final Map? deviceData; @override Widget build(BuildContext context) { - Widget card = Draggable>( - data: {'key': UniqueKey().toString(), 'imagePath': imagePath, 'title': title}, + Widget card = Draggable>( + data: deviceData ?? + { + 'key': UniqueKey().toString(), + 'imagePath': imagePath, + 'title': title, + }, feedback: Transform.rotate( angle: -0.1, child: _buildCardContent(context), diff --git a/lib/pages/routiens/widgets/if_container.dart b/lib/pages/routiens/widgets/if_container.dart index 069f77b4..ba0d67ab 100644 --- a/lib/pages/routiens/widgets/if_container.dart +++ b/lib/pages/routiens/widgets/if_container.dart @@ -1,18 +1,17 @@ -// lib/pages/routiens/widgets/if_container.dart - import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/helper/dialog_helper/device_dialog_helper.dart'; import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; class IfContainer extends StatelessWidget { - const IfContainer({Key? key}) : super(key: key); + const IfContainer({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - return DragTarget>( + return DragTarget>( builder: (context, candidateData, rejectedData) { return Container( width: double.infinity, @@ -20,7 +19,9 @@ class IfContainer extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('IF', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const Text('IF', + style: + TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 16), Wrap( spacing: 8, @@ -37,8 +38,16 @@ class IfContainer extends StatelessWidget { ), ); }, - onAccept: (data) { - context.read().add(AddToIfContainer(data)); + onWillAccept: (data) => data != null, + onAccept: (data) async { + final result = + await DeviceDialogHelper.showDeviceDialog(context, data); + if (result != null) { + context.read().add(AddToIfContainer(result)); + } else if (!['AC', '1G', '2G', '3G'] + .contains(data['productType'])) { + context.read().add(AddToIfContainer(data)); + } }, ); }, diff --git a/lib/pages/routiens/widgets/routine_devices.dart b/lib/pages/routiens/widgets/routine_devices.dart index 12d8492e..c845af19 100644 --- a/lib/pages/routiens/widgets/routine_devices.dart +++ b/lib/pages/routiens/widgets/routine_devices.dart @@ -29,11 +29,18 @@ class RoutineDevices extends StatelessWidget { spacing: 10, runSpacing: 10, children: deviceList.asMap().entries.map((entry) { - final index = entry.key; final device = entry.value; return DraggableCard( imagePath: device.getDefaultIcon(device.productType), title: device.name ?? '', + deviceData: { + 'key': UniqueKey().toString(), + 'imagePath': device.getDefaultIcon(device.productType), + 'title': device.name ?? '', + 'deviceId': device.uuid, + 'productType': device.productType, + 'functions': device.functions, + }, ); }).toList(), ); diff --git a/lib/pages/routiens/widgets/scenes_and_automations.dart b/lib/pages/routiens/widgets/scenes_and_automations.dart index 7c90305b..b455b2f5 100644 --- a/lib/pages/routiens/widgets/scenes_and_automations.dart +++ b/lib/pages/routiens/widgets/scenes_and_automations.dart @@ -14,10 +14,10 @@ class ScenesAndAutomations extends StatelessWidget { return BlocProvider( create: (context) => RoutineBloc() ..add( - LoadScenes(spaceId), + const LoadScenes(spaceId), ) ..add( - LoadAutomation(spaceId), + const LoadAutomation(spaceId), ), child: BlocBuilder( builder: (context, state) { @@ -27,11 +27,10 @@ class ScenesAndAutomations extends StatelessWidget { spacing: 10, runSpacing: 10, children: scenes.asMap().entries.map((entry) { - final index = entry.key; final scene = entry.value; return DraggableCard( imagePath: Assets.logo, - title: scene.name ?? '', + title: scene.name, ); }).toList(), ); diff --git a/lib/pages/routiens/widgets/then_container.dart b/lib/pages/routiens/widgets/then_container.dart index bc72bd9f..9bd7fd00 100644 --- a/lib/pages/routiens/widgets/then_container.dart +++ b/lib/pages/routiens/widgets/then_container.dart @@ -3,16 +3,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/helper/dialog_helper/device_dialog_helper.dart'; import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; class ThenContainer extends StatelessWidget { - const ThenContainer({Key? key}) : super(key: key); + const ThenContainer({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - return DragTarget>( + return DragTarget>( builder: (context, candidateData, rejectedData) { return Container( padding: const EdgeInsets.all(16), @@ -20,7 +21,9 @@ class ThenContainer extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('THEN', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const Text('THEN', + style: + TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 16), Wrap( spacing: 8, @@ -37,8 +40,16 @@ class ThenContainer extends StatelessWidget { ), ); }, - onAccept: (data) { - context.read().add(AddToThenContainer(data)); + onWillAccept: (data) => data != null, + onAccept: (data) async { + final result = + await DeviceDialogHelper.showDeviceDialog(context, data); + if (result != null) { + context.read().add(AddToThenContainer(result)); + } else if (!['AC', '1G', '2G', '3G'] + .contains(data['productType'])) { + context.read().add(AddToThenContainer(data)); + } }, ); }, diff --git a/macos/DerivedData/Runner/Logs/Build/.dat.nosync1585.W9c579 b/macos/DerivedData/Runner/Logs/Build/.dat.nosync1585.W9c579 new file mode 100644 index 00000000..309af193 --- /dev/null +++ b/macos/DerivedData/Runner/Logs/Build/.dat.nosync1585.W9c579 @@ -0,0 +1,95 @@ + + + + + logFormatVersion + 11 + logs + + DEC061F9-9521-4D0C-959C-43A07F62CC12 + + className + IDECommandLineBuildLog + documentTypeString + <nil> + domainType + Xcode.IDEActivityLogDomainType.BuildLog + fileName + DEC061F9-9521-4D0C-959C-43A07F62CC12.xcactivitylog + hasPrimaryLog + + primaryObservable + + highLevelStatus + S + totalNumberOfAnalyzerIssues + 0 + totalNumberOfErrors + 0 + totalNumberOfTestFailures + 0 + totalNumberOfWarnings + 0 + + schemeIdentifier-containerName + Runner project + schemeIdentifier-schemeName + Flutter Assemble + schemeIdentifier-sharedScheme + 1 + signature + Cleaning workspace Runner with scheme Flutter Assemble + timeStartedRecording + 752000674.27645695 + timeStoppedRecording + 752000674.42918503 + title + Cleaning workspace Runner with scheme Flutter Assemble + uniqueIdentifier + DEC061F9-9521-4D0C-959C-43A07F62CC12 + + FB42CDDD-C79D-4D4B-891A-12C476DFCB10 + + className + IDECommandLineBuildLog + documentTypeString + <nil> + domainType + Xcode.IDEActivityLogDomainType.BuildLog + fileName + FB42CDDD-C79D-4D4B-891A-12C476DFCB10.xcactivitylog + hasPrimaryLog + + primaryObservable + + highLevelStatus + S + totalNumberOfAnalyzerIssues + 0 + totalNumberOfErrors + 0 + totalNumberOfTestFailures + 0 + totalNumberOfWarnings + 0 + + schemeIdentifier-containerName + Runner project + schemeIdentifier-schemeName + Runner + schemeIdentifier-sharedScheme + 1 + signature + Cleaning workspace Runner with scheme Runner + timeStartedRecording + 752000674.90370798 + timeStoppedRecording + 752000675.05962098 + title + Cleaning workspace Runner with scheme Runner + uniqueIdentifier + FB42CDDD-C79D-4D4B-891A-12C476DFCB10 + + + + diff --git a/macos/DerivedData/Runner/Logs/Build/DEC061F9-9521-4D0C-959C-43A07F62CC12.xcactivitylog b/macos/DerivedData/Runner/Logs/Build/DEC061F9-9521-4D0C-959C-43A07F62CC12.xcactivitylog new file mode 100644 index 0000000000000000000000000000000000000000..c811e6cbcebca9a7a43e017168389b0481562c19 GIT binary patch literal 217 zcmV;~04Dz*iwFP!000006P-`ZO2jZ2e3j55c*&A4Nu#-0Qwu^*>cNvxldrqMCRsMA z%f7u45j@LWhWUY+=V@dBE$3H{!-Gj7%+XCg;{E1VH>Ew?u~z8j)h36#8tHBEpT1nG zHKZ{6_;NYo?l;}Z#;iC;ANS;(P9OO=TO?0gjCdkvUk)VO0|q4Hx|LG3K4%Q?EB?E< zfZ|+qAQz>rlJIt`aS`f|Kq+PIH!2aTsV|v=QB&P + + + + logFormatVersion + 11 + logs + + DEC061F9-9521-4D0C-959C-43A07F62CC12 + + className + IDECommandLineBuildLog + documentTypeString + <nil> + domainType + Xcode.IDEActivityLogDomainType.BuildLog + fileName + DEC061F9-9521-4D0C-959C-43A07F62CC12.xcactivitylog + hasPrimaryLog + + primaryObservable + + highLevelStatus + S + totalNumberOfAnalyzerIssues + 0 + totalNumberOfErrors + 0 + totalNumberOfTestFailures + 0 + totalNumberOfWarnings + 0 + + schemeIdentifier-containerName + Runner project + schemeIdentifier-schemeName + Flutter Assemble + schemeIdentifier-sharedScheme + 1 + signature + Cleaning workspace Runner with scheme Flutter Assemble + timeStartedRecording + 752000674.27645695 + timeStoppedRecording + 752000674.42918503 + title + Cleaning workspace Runner with scheme Flutter Assemble + uniqueIdentifier + DEC061F9-9521-4D0C-959C-43A07F62CC12 + + + + diff --git a/macos/DerivedData/Runner/Logs/Launch/LogStoreManifest.plist b/macos/DerivedData/Runner/Logs/Launch/LogStoreManifest.plist new file mode 100644 index 00000000..f38de442 --- /dev/null +++ b/macos/DerivedData/Runner/Logs/Launch/LogStoreManifest.plist @@ -0,0 +1,10 @@ + + + + + logFormatVersion + 11 + logs + + + diff --git a/macos/DerivedData/Runner/Logs/Localization/LogStoreManifest.plist b/macos/DerivedData/Runner/Logs/Localization/LogStoreManifest.plist new file mode 100644 index 00000000..f38de442 --- /dev/null +++ b/macos/DerivedData/Runner/Logs/Localization/LogStoreManifest.plist @@ -0,0 +1,10 @@ + + + + + logFormatVersion + 11 + logs + + + diff --git a/macos/DerivedData/Runner/Logs/Package/LogStoreManifest.plist b/macos/DerivedData/Runner/Logs/Package/LogStoreManifest.plist new file mode 100644 index 00000000..f38de442 --- /dev/null +++ b/macos/DerivedData/Runner/Logs/Package/LogStoreManifest.plist @@ -0,0 +1,10 @@ + + + + + logFormatVersion + 11 + logs + + + diff --git a/macos/DerivedData/Runner/Logs/Test/LogStoreManifest.plist b/macos/DerivedData/Runner/Logs/Test/LogStoreManifest.plist new file mode 100644 index 00000000..f38de442 --- /dev/null +++ b/macos/DerivedData/Runner/Logs/Test/LogStoreManifest.plist @@ -0,0 +1,10 @@ + + + + + logFormatVersion + 11 + logs + + + diff --git a/macos/DerivedData/Runner/info.plist b/macos/DerivedData/Runner/info.plist new file mode 100644 index 00000000..3594c152 --- /dev/null +++ b/macos/DerivedData/Runner/info.plist @@ -0,0 +1,10 @@ + + + + + LastAccessedDate + 2024-10-30T17:04:35Z + WorkspacePath + /Users/akmz/Developer/web/syncrow-web/web/macos/Runner.xcworkspace + + diff --git a/pubspec.yaml b/pubspec.yaml index 9e35e58f..8dc6b6bc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -76,6 +76,7 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - assets/icons/automation_functions/ + - assets/icons/functions_icons/ - assets/icons/routine/ - assets/icons/ - assets/images/ From 1d6673b5b0c11b0b870d94b0e62a8e676df961eb Mon Sep 17 00:00:00 2001 From: ashrafzarkanisala Date: Fri, 22 Nov 2024 16:55:30 +0300 Subject: [PATCH 07/23] adding fucntions and values to routine bloc --- lib/pages/routiens/bloc/routine_bloc.dart | 44 +- lib/pages/routiens/bloc/routine_event.dart | 16 + lib/pages/routiens/bloc/routine_state.dart | 5 + lib/pages/routiens/helper/ac_helper.dart | 562 ++++++------------ .../dialog_helper/device_dialog_helper.dart | 24 +- .../helper/one_gang_switch_helper.dart | 333 +++++++---- .../helper/three_gang_switch_helper.dart | 335 +++++++---- .../helper/two_gang_switch_helper.dart | 334 +++++++---- .../routiens/models/device_functions.dart | 44 ++ lib/pages/routiens/widgets/dragable_card.dart | 147 +++-- 10 files changed, 1094 insertions(+), 750 deletions(-) diff --git a/lib/pages/routiens/bloc/routine_bloc.dart b/lib/pages/routiens/bloc/routine_bloc.dart index dfbb824c..d69df552 100644 --- a/lib/pages/routiens/bloc/routine_bloc.dart +++ b/lib/pages/routiens/bloc/routine_bloc.dart @@ -1,5 +1,6 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; import 'package:syncrow_web/pages/routiens/models/routine_model.dart'; import 'package:syncrow_web/services/routines_api.dart'; @@ -14,27 +15,55 @@ class RoutineBloc extends Bloc { on(_onAddToThenContainer); on(_onLoadScenes); on(_onLoadAutomation); + on(_onAddFunction); + on(_onRemoveFunction); + on(_onClearFunctions); } void _onAddToIfContainer(AddToIfContainer event, Emitter emit) { if (!_isDuplicate(state.ifItems, event.item)) { - final updatedIfItems = List>.from(state.ifItems)..add(event.item); + final updatedIfItems = List>.from(state.ifItems) + ..add(event.item); emit(state.copyWith(ifItems: updatedIfItems)); } } - void _onAddToThenContainer(AddToThenContainer event, Emitter emit) { + void _onAddToThenContainer( + AddToThenContainer event, Emitter emit) { if (!_isDuplicate(state.thenItems, event.item)) { - final updatedThenItems = List>.from(state.thenItems)..add(event.item); + final updatedThenItems = List>.from(state.thenItems) + ..add(event.item); emit(state.copyWith(thenItems: updatedThenItems)); } } - bool _isDuplicate(List> items, Map newItem) { - return items.any((item) => item['imagePath'] == newItem['imagePath'] && item['title'] == newItem['title']); + void _onAddFunction(AddFunction event, Emitter emit) { + final functions = List.from(state.selectedFunctions); + functions.add(event.function); + emit(state.copyWith(selectedFunctions: functions)); } - Future _onLoadScenes(LoadScenes event, Emitter emit) async { + void _onRemoveFunction(RemoveFunction event, Emitter emit) { + final functions = List.from(state.selectedFunctions) + ..removeWhere((f) => + f.function == event.function.function && + f.value == event.function.value); + emit(state.copyWith(selectedFunctions: functions)); + } + + void _onClearFunctions(ClearFunctions event, Emitter emit) { + emit(state.copyWith(selectedFunctions: [])); + } + + bool _isDuplicate( + List> items, Map newItem) { + return items.any((item) => + item['imagePath'] == newItem['imagePath'] && + item['title'] == newItem['title']); + } + + Future _onLoadScenes( + LoadScenes event, Emitter emit) async { emit(state.copyWith(isLoading: true, errorMessage: null)); try { @@ -51,7 +80,8 @@ class RoutineBloc extends Bloc { } } - Future _onLoadAutomation(LoadAutomation event, Emitter emit) async { + Future _onLoadAutomation( + LoadAutomation event, Emitter emit) async { emit(state.copyWith(isLoading: true, errorMessage: null)); try { diff --git a/lib/pages/routiens/bloc/routine_event.dart b/lib/pages/routiens/bloc/routine_event.dart index aaee73c7..d3af8134 100644 --- a/lib/pages/routiens/bloc/routine_event.dart +++ b/lib/pages/routiens/bloc/routine_event.dart @@ -42,3 +42,19 @@ class LoadAutomation extends RoutineEvent { @override List get props => [unitId]; } + +class AddFunction extends RoutineEvent { + final DeviceFunctionData function; + const AddFunction(this.function); + @override + List get props => [function]; +} + +class RemoveFunction extends RoutineEvent { + final DeviceFunctionData function; + const RemoveFunction(this.function); + @override + List get props => [function]; +} + +class ClearFunctions extends RoutineEvent {} diff --git a/lib/pages/routiens/bloc/routine_state.dart b/lib/pages/routiens/bloc/routine_state.dart index 63d2e6f8..95aab557 100644 --- a/lib/pages/routiens/bloc/routine_state.dart +++ b/lib/pages/routiens/bloc/routine_state.dart @@ -6,6 +6,7 @@ class RoutineState extends Equatable { final List> availableCards; final List scenes; final List automations; + final List selectedFunctions; final bool isLoading; final String? errorMessage; @@ -15,6 +16,7 @@ class RoutineState extends Equatable { this.availableCards = const [], this.scenes = const [], this.automations = const [], + this.selectedFunctions = const [], this.isLoading = false, this.errorMessage, }); @@ -24,6 +26,7 @@ class RoutineState extends Equatable { List>? thenItems, List? scenes, List? automations, + List? selectedFunctions, bool? isLoading, String? errorMessage, }) { @@ -32,6 +35,7 @@ class RoutineState extends Equatable { thenItems: thenItems ?? this.thenItems, scenes: scenes ?? this.scenes, automations: automations ?? this.automations, + selectedFunctions: selectedFunctions ?? this.selectedFunctions, isLoading: isLoading ?? this.isLoading, errorMessage: errorMessage ?? this.errorMessage, ); @@ -43,6 +47,7 @@ class RoutineState extends Equatable { thenItems, scenes, automations, + selectedFunctions, isLoading, errorMessage, ]; diff --git a/lib/pages/routiens/helper/ac_helper.dart b/lib/pages/routiens/helper/ac_helper.dart index 4c216a89..b14acd3a 100644 --- a/lib/pages/routiens/helper/ac_helper.dart +++ b/lib/pages/routiens/helper/ac_helper.dart @@ -1,411 +1,219 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/models/ac/ac_function.dart'; import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class ACHelper { - static Future?> showACFunctionsDialog( + static Future showACFunctionsDialog( BuildContext context, List> functions, ) async { List acFunctions = functions.whereType().toList(); - String? selectedFunction; - dynamic selectedValue = 20; - String? selectedCondition = "=="; - List _selectedConditions = [false, true, false]; + // Track multiple selections using a map + Map selectedValues = {}; + List selectedFunctions = []; - return showDialog?>( + await showDialog( context: context, builder: (BuildContext context) { - return StatefulBuilder( - builder: (context, setState) { - return AlertDialog( - contentPadding: EdgeInsets.zero, - content: _buildDialogContent( - context, - setState, - acFunctions, - selectedFunction, - selectedValue, - selectedCondition, - _selectedConditions, - (fn) => selectedFunction = fn, - (val) => selectedValue = val, - (cond) => selectedCondition = cond, - ), - ); - }, + return Dialog( + child: Container( + width: 600, + height: 450, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.only(top: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'AC Functions', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.primaryColorWithOpacity, + fontWeight: FontWeight.bold, + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(vertical: 15, horizontal: 50), + child: Container( + height: 1, + width: double.infinity, + color: ColorsManager.greyColor, + ), + ), + Expanded( + child: Row( + children: [ + Expanded( + child: ListView.separated( + itemCount: acFunctions.length, + separatorBuilder: (_, __) => const Divider( + color: ColorsManager.dividerColor, + ), + itemBuilder: (context, index) { + final function = acFunctions[index]; + final isSelected = + selectedValues.containsKey(function.code); + return ListTile( + tileColor: + isSelected ? Colors.grey.shade100 : null, + leading: SvgPicture.asset( + function.icon, + width: 24, + height: 24, + ), + title: Text( + function.operationName, + style: context.textTheme.bodyMedium, + ), + trailing: isSelected + ? Icon( + Icons.check_circle, + color: + ColorsManager.primaryColorWithOpacity, + size: 20, + ) + : const Icon( + Icons.arrow_forward_ios, + size: 16, + color: ColorsManager.textGray, + ), + onTap: () { + if (isSelected) { + selectedValues.remove(function.code); + selectedFunctions.removeWhere( + (f) => f.function == function.code); + } + (context as Element).markNeedsBuild(); + }, + ); + }, + ), + ), + Expanded( + child: Builder( + builder: (context) { + final selectedFunction = acFunctions.firstWhere( + (f) => selectedValues.containsKey(f.code), + orElse: () => acFunctions.first, + ); + return _buildValueSelector( + context, + selectedFunction, + selectedValues[selectedFunction.code], + (value) { + selectedValues[selectedFunction.code] = value; + // Update or add the function data + final functionData = DeviceFunctionData( + entityId: selectedFunction.deviceId, + function: selectedFunction.code, + operationName: selectedFunction.operationName, + value: value, + valueDescription: _getValueDescription( + selectedFunction, value), + ); + + final existingIndex = + selectedFunctions.indexWhere((f) => + f.function == selectedFunction.code); + if (existingIndex != -1) { + selectedFunctions[existingIndex] = + functionData; + } else { + selectedFunctions.add(functionData); + } + + (context as Element).markNeedsBuild(); + }, + ); + }, + ), + ), + ], + ), + ), + Container( + height: 1, + width: double.infinity, + color: ColorsManager.greyColor, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + 'Cancel', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: ColorsManager.greyColor), + ), + ), + TextButton( + onPressed: selectedFunctions.isNotEmpty + ? () { + // Add all selected functions to the bloc + for (final function in selectedFunctions) { + context + .read() + .add(AddFunction(function)); + } + Navigator.pop(context, true); + } + : null, + child: Text( + 'Confirm', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.primaryColorWithOpacity, + ), + ), + ), + ], + ), + ], + ), + ), ); }, ); } - /// Build dialog content for AC functions dialog - static Widget _buildDialogContent( + static Widget _buildValueSelector( BuildContext context, - StateSetter setState, - List acFunctions, - String? selectedFunction, + ACFunction function, dynamic selectedValue, - String? selectedCondition, - List selectedConditions, - Function(String?) onFunctionSelected, Function(dynamic) onValueSelected, - Function(String?) onConditionSelected, ) { + final values = function.getOperationalValues(); 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: [ - _buildDialogHeader(context), - Flexible( - child: Row( - children: [ - _buildFunctionsList( - context, - setState, - acFunctions, - selectedFunction, - onFunctionSelected, - ), - if (selectedFunction != null) - _buildValueSelector( - context, - setState, - selectedFunction, - selectedValue, - selectedCondition, - selectedConditions, - onValueSelected, - onConditionSelected, - acFunctions, - ), - ], - ), - ), - _buildDialogFooter( - context, - selectedFunction, - selectedValue, - selectedCondition, - ), - ], - ), - ); - } - - /// Build header for AC functions dialog - static Widget _buildDialogHeader(BuildContext context) { - return Column( - children: [ - Text( - 'AC Condition', - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: ColorsManager.primaryColorWithOpacity, - fontWeight: FontWeight.bold, - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 50), - child: Container( - height: 1, - width: double.infinity, - color: ColorsManager.greyColor, - ), - ), - ], - ); - } - - /// Build functions list for AC functions dialog - static Widget _buildFunctionsList( - BuildContext context, - StateSetter setState, - List acFunctions, - String? selectedFunction, - Function(String?) onFunctionSelected, - ) { - return Expanded( - child: ListView.separated( - shrinkWrap: false, - physics: const AlwaysScrollableScrollPhysics(), - itemCount: acFunctions.length, - separatorBuilder: (context, index) => const Divider( - color: ColorsManager.dividerColor, - ), + height: 200, + padding: const EdgeInsets.symmetric(horizontal: 20), + child: ListView.builder( + itemCount: values.length, itemBuilder: (context, index) { - final function = acFunctions[index]; - return ListTile( - leading: SvgPicture.asset( - function.icon, - 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: () => setState(() => onFunctionSelected(function.code)), + final value = values[index]; + return RadioListTile( + value: value.value, + groupValue: selectedValue, + onChanged: onValueSelected, + title: Text(value.description), ); }, ), ); } - /// Build value selector for AC functions dialog - static Widget _buildValueSelector( - BuildContext context, - StateSetter setState, - String selectedFunction, - dynamic selectedValue, - String? selectedCondition, - List selectedConditions, - Function(dynamic) onValueSelected, - Function(String?) onConditionSelected, - List acFunctions, - ) { - if (selectedFunction == 'temp_set' || selectedFunction == 'temp_current') { - return Expanded( - child: _buildTemperatureSelector( - context, - setState, - selectedValue, - selectedCondition, - selectedConditions, - onValueSelected, - onConditionSelected, - ), - ); - } - - final selectedFn = - acFunctions.firstWhere((f) => f.code == selectedFunction); - final values = selectedFn.getOperationalValues(); - return Expanded( - child: _buildOperationalValuesList( - context, - setState, - values, - selectedValue, - onValueSelected, - ), - ); - } - - /// Build temperature selector for AC functions dialog - static Widget _buildTemperatureSelector( - BuildContext context, - StateSetter setState, - dynamic selectedValue, - String? selectedCondition, - List selectedConditions, - Function(dynamic) onValueSelected, - Function(String?) onConditionSelected, - ) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildConditionToggle( - context, - setState, - selectedConditions, - onConditionSelected, - ), - const SizedBox(height: 20), - _buildTemperatureDisplay(context, selectedValue), - const SizedBox(height: 20), - _buildTemperatureSlider( - context, - setState, - selectedValue, - onValueSelected, - ), - ], - ); - } - - /// Build condition toggle for AC functions dialog - static Widget _buildConditionToggle( - BuildContext context, - StateSetter setState, - List selectedConditions, - Function(String?) onConditionSelected, - ) { - return ToggleButtons( - onPressed: (int index) { - setState(() { - for (int i = 0; i < selectedConditions.length; i++) { - selectedConditions[i] = i == index; - } - onConditionSelected(index == 0 - ? "<" - : index == 1 - ? "==" - : ">"); - }); - }, - 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: selectedConditions, - children: const [Text("<"), Text("="), Text(">")], - ); - } - - /// Build temperature display for AC functions dialog - static Widget _buildTemperatureDisplay( - BuildContext context, dynamic selectedValue) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - decoration: BoxDecoration( - color: ColorsManager.primaryColorWithOpacity.withOpacity(0.1), - borderRadius: BorderRadius.circular(10), - ), - child: Text( - '${selectedValue ?? 20}°C', - style: context.textTheme.headlineMedium!.copyWith( - color: ColorsManager.primaryColorWithOpacity, - ), - ), - ); - } - - static Widget _buildTemperatureSlider( - BuildContext context, - StateSetter setState, - dynamic selectedValue, - Function(dynamic) onValueSelected, - ) { - final currentValue = selectedValue is int ? selectedValue.toDouble() : 20.0; - return Slider( - value: currentValue, - min: 16, - max: 30, - divisions: 14, - label: '${currentValue.toInt()}°C', - onChanged: (value) { - setState(() => onValueSelected(value.toInt())); - }, - ); - } - - static Widget _buildOperationalValuesList( - BuildContext context, - StateSetter setState, - List values, - dynamic selectedValue, - Function(dynamic) onValueSelected, - ) { - return ListView.builder( - shrinkWrap: false, - physics: const AlwaysScrollableScrollPhysics(), - itemCount: values.length, - itemBuilder: (context, index) { - final value = values[index]; - return ListTile( - leading: SvgPicture.asset( - value.icon, - width: 24, - height: 24, - ), - title: Text( - value.description, - style: context.textTheme.bodyMedium, - ), - trailing: Radio( - value: value.value, - groupValue: selectedValue, - onChanged: (newValue) { - setState(() => onValueSelected(newValue)); - }, - ), - ); - }, - ); - } - - static Widget _buildDialogFooter( - BuildContext context, - String? selectedFunction, - dynamic selectedValue, - String? selectedCondition, - ) { - return Container( - decoration: const BoxDecoration( - border: Border( - top: BorderSide( - color: ColorsManager.greyColor, - ), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _buildFooterButton( - context, - 'Cancel', - selectedFunction != null ? 299 : 179, - () => Navigator.pop(context), - ), - _buildFooterButton( - context, - 'Confirm', - selectedFunction != null ? 299 : 179, - selectedFunction != null && selectedValue != null - ? () => Navigator.pop(context, { - 'function': selectedFunction, - 'value': selectedValue, - 'condition': selectedCondition ?? "==", - }) - : null, - ), - ], - ), - ); - } - - static Widget _buildFooterButton( - BuildContext context, - String text, - double width, - VoidCallback? onTap, - ) { - return GestureDetector( - onTap: onTap, - child: SizedBox( - height: 50, - width: width, - child: Center( - child: Text( - text, - style: context.textTheme.bodyMedium!.copyWith( - color: onTap != null - ? ColorsManager.primaryColorWithOpacity - : ColorsManager.textGray, - ), - ), - ), - ), - ); + static String _getValueDescription(ACFunction function, dynamic value) { + final values = function.getOperationalValues(); + final selectedValue = values.firstWhere((v) => v.value == value); + return selectedValue.description; } } diff --git a/lib/pages/routiens/helper/dialog_helper/device_dialog_helper.dart b/lib/pages/routiens/helper/dialog_helper/device_dialog_helper.dart index 0046db8a..ea4743b7 100644 --- a/lib/pages/routiens/helper/dialog_helper/device_dialog_helper.dart +++ b/lib/pages/routiens/helper/dialog_helper/device_dialog_helper.dart @@ -13,15 +13,11 @@ class DeviceDialogHelper { final functions = data['functions'] as List; try { - final result = await _getDialogForDeviceType( + await _getDialogForDeviceType( context, data['productType'], functions, ); - - if (result != null) { - return {...data, ...result}; - } } catch (e) { debugPrint('Error: $e'); } @@ -29,25 +25,25 @@ class DeviceDialogHelper { return null; } - static Future?> _getDialogForDeviceType( + static Future _getDialogForDeviceType( BuildContext context, String productType, List functions, ) async { switch (productType) { case 'AC': - return ACHelper.showACFunctionsDialog(context, functions); + await ACHelper.showACFunctionsDialog(context, functions); + break; case '1G': - return OneGangSwitchHelper.showSwitchFunctionsDialog( - context, functions); + await OneGangSwitchHelper.showSwitchFunctionsDialog(context, functions); + break; case '2G': - return TwoGangSwitchHelper.showSwitchFunctionsDialog( - context, functions); + await TwoGangSwitchHelper.showSwitchFunctionsDialog(context, functions); + break; case '3G': - return ThreeGangSwitchHelper.showSwitchFunctionsDialog( + await ThreeGangSwitchHelper.showSwitchFunctionsDialog( context, functions); - default: - return null; + break; } } } diff --git a/lib/pages/routiens/helper/one_gang_switch_helper.dart b/lib/pages/routiens/helper/one_gang_switch_helper.dart index 30f25a96..c9a79377 100644 --- a/lib/pages/routiens/helper/one_gang_switch_helper.dart +++ b/lib/pages/routiens/helper/one_gang_switch_helper.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart'; import 'package:syncrow_web/pages/routiens/models/gang_switches/one_gang_switch/one_gang_switch.dart'; @@ -7,16 +9,18 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class OneGangSwitchHelper { - static Future?> showSwitchFunctionsDialog( + static Future showSwitchFunctionsDialog( BuildContext context, List> functions) async { List> switchFunctions = functions .where( (f) => f is OneGangSwitchFunction || f is OneGangCountdownFunction) .toList(); - String? selectedFunction; - dynamic selectedValue; + Map selectedValues = {}; + List selectedFunctions = []; + String? selectedCondition = "<"; + List selectedConditions = [true, false, false]; - return showDialog?>( + await showDialog( context: context, builder: (BuildContext context) { return StatefulBuilder( @@ -24,7 +28,7 @@ class OneGangSwitchHelper { return AlertDialog( contentPadding: EdgeInsets.zero, content: Container( - width: selectedFunction != null ? 600 : 360, + width: 600, height: 450, decoration: BoxDecoration( color: Colors.white, @@ -50,21 +54,23 @@ class OneGangSwitchHelper { color: ColorsManager.greyColor, ), ), - Flexible( + Expanded( child: Row( children: [ + // Left side: Function list Expanded( child: ListView.separated( - shrinkWrap: false, - physics: const AlwaysScrollableScrollPhysics(), itemCount: switchFunctions.length, - separatorBuilder: (context, index) => - const Divider( + separatorBuilder: (_, __) => const Divider( color: ColorsManager.dividerColor, ), itemBuilder: (context, index) { final function = switchFunctions[index]; + final isSelected = + selectedValues.containsKey(function.code); return ListTile( + tileColor: + isSelected ? Colors.grey.shade100 : null, leading: SvgPicture.asset( function.icon, width: 24, @@ -74,62 +80,129 @@ class OneGangSwitchHelper { function.operationName, style: context.textTheme.bodyMedium, ), - trailing: const Icon( - Icons.arrow_forward_ios, - size: 16, - color: ColorsManager.textGray, - ), + trailing: isSelected + ? Icon( + Icons.check_circle, + color: ColorsManager + .primaryColorWithOpacity, + size: 20, + ) + : const Icon( + Icons.arrow_forward_ios, + size: 16, + color: ColorsManager.textGray, + ), onTap: () { - setState(() { - selectedFunction = function.code; - selectedValue = null; - }); + if (isSelected) { + selectedValues.remove(function.code); + selectedFunctions.removeWhere( + (f) => f.function == function.code); + } + (context as Element).markNeedsBuild(); }, ); }, ), ), - if (selectedFunction != null) - Expanded( - child: Builder( - builder: (context) { - final selectedFn = switchFunctions.firstWhere( - (f) => f.code == selectedFunction) - as BaseSwitchFunction; - final values = - selectedFn.getOperationalValues(); - return ListView.builder( - shrinkWrap: false, - physics: - const AlwaysScrollableScrollPhysics(), - itemCount: values.length, - itemBuilder: (context, index) { - final value = values[index]; - return ListTile( - leading: SvgPicture.asset( - value.icon, - width: 24, - height: 24, - ), - title: Text( - value.description, - style: context.textTheme.bodyMedium, - ), - trailing: Radio( - value: value.value, - groupValue: selectedValue, - onChanged: (newValue) { - setState(() { - selectedValue = newValue; - }); - }, - ), + // Right side: Value selector + Expanded( + child: Builder( + builder: (context) { + final selectedFn = switchFunctions.firstWhere( + (f) => selectedValues.containsKey(f.code), + orElse: () => switchFunctions.first, + ) as BaseSwitchFunction; + + if (selectedFn is OneGangCountdownFunction) { + return _buildCountDownSelector( + context, + setState, + selectedValues[selectedFn.code] ?? 0, + selectedCondition, + selectedConditions, + (value) { + selectedValues[selectedFn.code] = value; + final functionData = DeviceFunctionData( + entityId: selectedFn.deviceId, + function: selectedFn.code, + operationName: selectedFn.operationName, + value: value, + condition: selectedCondition, + valueDescription: '${value} sec', ); + + final existingIndex = + selectedFunctions.indexWhere((f) => + f.function == selectedFn.code); + if (existingIndex != -1) { + selectedFunctions[existingIndex] = + functionData; + } else { + selectedFunctions.add(functionData); + } + (context as Element).markNeedsBuild(); + }, + (condition) { + setState(() { + selectedCondition = condition; + }); }, ); - }, - ), + } + + final values = + selectedFn.getOperationalValues(); + return ListView.builder( + itemCount: values.length, + itemBuilder: (context, index) { + final value = values[index]; + return ListTile( + leading: SvgPicture.asset( + value.icon, + width: 24, + height: 24, + ), + title: Text( + value.description, + style: context.textTheme.bodyMedium, + ), + trailing: Radio( + value: value.value, + groupValue: + selectedValues[selectedFn.code], + onChanged: (newValue) { + selectedValues[selectedFn.code] = + newValue; + final functionData = + DeviceFunctionData( + entityId: selectedFn.deviceId, + function: selectedFn.code, + operationName: + selectedFn.operationName, + value: newValue, + valueDescription: value.description, + ); + + final existingIndex = + selectedFunctions.indexWhere( + (f) => + f.function == + selectedFn.code); + if (existingIndex != -1) { + selectedFunctions[existingIndex] = + functionData; + } else { + selectedFunctions.add(functionData); + } + (context as Element).markNeedsBuild(); + }, + ), + ); + }, + ); + }, ), + ), ], ), ), @@ -141,55 +214,35 @@ class OneGangSwitchHelper { Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - GestureDetector( - onTap: () { - Navigator.pop(context); - }, - child: Container( - height: 50, - width: selectedFunction != null ? 299 : 179, - decoration: const BoxDecoration( - border: Border( - right: - BorderSide(color: ColorsManager.greyColor), - ), - ), - child: Center( - child: Text( - 'Cancel', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: ColorsManager.greyColor), - ), - ), + TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + 'Cancel', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: ColorsManager.greyColor), ), ), - GestureDetector( - onTap: () { - if (selectedFunction != null && - selectedValue != null) { - Navigator.pop(context, { - 'function': selectedFunction, - 'value': selectedValue, - }); - } - }, - child: SizedBox( - height: 50, - width: selectedFunction != null ? 299 : 179, - child: Center( - child: Text( - 'Confirm', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith( - color: - ColorsManager.primaryColorWithOpacity, - ), - ), - ), + TextButton( + onPressed: selectedFunctions.isNotEmpty + ? () { + for (final function in selectedFunctions) { + context + .read() + .add(AddFunction(function)); + } + Navigator.pop(context, true); + } + : null, + child: Text( + 'Confirm', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: ColorsManager.primaryColorWithOpacity, + ), ), ), ], @@ -203,4 +256,78 @@ class OneGangSwitchHelper { }, ); } + + /// Build countdown selector for switch functions dialog + static Widget _buildCountDownSelector( + BuildContext context, + StateSetter setState, + dynamic selectedValue, + String? selectedCondition, + List selectedConditions, + Function(dynamic) onValueSelected, + Function(String?) onConditionSelected, + ) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildConditionToggle( + context, + setState, + selectedConditions, + onConditionSelected, + ), + const SizedBox(height: 20), + Text( + '${selectedValue.toString()} sec', + style: Theme.of(context).textTheme.headlineMedium, + ), + const SizedBox(height: 20), + Slider( + value: selectedValue.toDouble(), + min: 0, + max: 300, // 5 minutes in seconds + divisions: 300, + onChanged: (value) { + setState(() { + onValueSelected(value.toInt()); + }); + }, + ), + ], + ); + } + + /// Build condition toggle for AC functions dialog + static Widget _buildConditionToggle( + BuildContext context, + StateSetter setState, + List selectedConditions, + Function(String?) onConditionSelected, + ) { + return ToggleButtons( + onPressed: (int index) { + setState(() { + for (int i = 0; i < selectedConditions.length; i++) { + selectedConditions[i] = i == index; + } + onConditionSelected(index == 0 + ? "<" + : index == 1 + ? "==" + : ">"); + }); + }, + 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: selectedConditions, + children: const [Text("<"), Text("="), Text(">")], + ); + } } diff --git a/lib/pages/routiens/helper/three_gang_switch_helper.dart b/lib/pages/routiens/helper/three_gang_switch_helper.dart index 89aa2d95..56d1f3bb 100644 --- a/lib/pages/routiens/helper/three_gang_switch_helper.dart +++ b/lib/pages/routiens/helper/three_gang_switch_helper.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart'; import 'package:syncrow_web/pages/routiens/models/gang_switches/three_gang_switch/three_gang_switch.dart'; @@ -7,7 +9,7 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class ThreeGangSwitchHelper { - static Future?> showSwitchFunctionsDialog( + static Future showSwitchFunctionsDialog( BuildContext context, List> functions) async { List> switchFunctions = functions .where((f) => @@ -18,10 +20,12 @@ class ThreeGangSwitchHelper { f is ThreeGangCountdown2Function || f is ThreeGangCountdown3Function) .toList(); - String? selectedFunction; - dynamic selectedValue; + Map selectedValues = {}; + List selectedFunctions = []; + String? selectedCondition = "<"; + List selectedConditions = [true, false, false]; - return showDialog?>( + await showDialog( context: context, builder: (BuildContext context) { return StatefulBuilder( @@ -29,7 +33,7 @@ class ThreeGangSwitchHelper { return AlertDialog( contentPadding: EdgeInsets.zero, content: Container( - width: selectedFunction != null ? 600 : 360, + width: 600, height: 450, decoration: BoxDecoration( color: Colors.white, @@ -55,21 +59,23 @@ class ThreeGangSwitchHelper { color: ColorsManager.greyColor, ), ), - Flexible( + Expanded( child: Row( children: [ + // Left side: Function list Expanded( child: ListView.separated( - shrinkWrap: false, - physics: const AlwaysScrollableScrollPhysics(), itemCount: switchFunctions.length, - separatorBuilder: (context, index) => - const Divider( + separatorBuilder: (_, __) => const Divider( color: ColorsManager.dividerColor, ), itemBuilder: (context, index) { final function = switchFunctions[index]; + final isSelected = + selectedValues.containsKey(function.code); return ListTile( + tileColor: + isSelected ? Colors.grey.shade100 : null, leading: SvgPicture.asset( function.icon, width: 24, @@ -79,62 +85,131 @@ class ThreeGangSwitchHelper { function.operationName, style: context.textTheme.bodyMedium, ), - trailing: const Icon( - Icons.arrow_forward_ios, - size: 16, - color: ColorsManager.textGray, - ), + trailing: isSelected + ? Icon( + Icons.check_circle, + color: ColorsManager + .primaryColorWithOpacity, + size: 20, + ) + : const Icon( + Icons.arrow_forward_ios, + size: 16, + color: ColorsManager.textGray, + ), onTap: () { - setState(() { - selectedFunction = function.code; - selectedValue = null; - }); + if (isSelected) { + selectedValues.remove(function.code); + selectedFunctions.removeWhere( + (f) => f.function == function.code); + } + (context as Element).markNeedsBuild(); }, ); }, ), ), - if (selectedFunction != null) - Expanded( - child: Builder( - builder: (context) { - final selectedFn = switchFunctions.firstWhere( - (f) => f.code == selectedFunction) - as BaseSwitchFunction; - final values = - selectedFn.getOperationalValues(); - return ListView.builder( - shrinkWrap: false, - physics: - const AlwaysScrollableScrollPhysics(), - itemCount: values.length, - itemBuilder: (context, index) { - final value = values[index]; - return ListTile( - leading: SvgPicture.asset( - value.icon, - width: 24, - height: 24, - ), - title: Text( - value.description, - style: context.textTheme.bodyMedium, - ), - trailing: Radio( - value: value.value, - groupValue: selectedValue, - onChanged: (newValue) { - setState(() { - selectedValue = newValue; - }); - }, - ), + // Right side: Value selector + Expanded( + child: Builder( + builder: (context) { + final selectedFn = switchFunctions.firstWhere( + (f) => selectedValues.containsKey(f.code), + orElse: () => switchFunctions.first, + ) as BaseSwitchFunction; + + if (selectedFn is ThreeGangCountdown1Function || + selectedFn is ThreeGangCountdown2Function || + selectedFn is ThreeGangCountdown3Function) { + return _buildCountDownSelector( + context, + setState, + selectedValues[selectedFn.code] ?? 0, + selectedCondition, + selectedConditions, + (value) { + selectedValues[selectedFn.code] = value; + final functionData = DeviceFunctionData( + entityId: selectedFn.deviceId, + function: selectedFn.code, + operationName: selectedFn.operationName, + value: value, + condition: selectedCondition, + valueDescription: '${value} sec', ); + + final existingIndex = + selectedFunctions.indexWhere((f) => + f.function == selectedFn.code); + if (existingIndex != -1) { + selectedFunctions[existingIndex] = + functionData; + } else { + selectedFunctions.add(functionData); + } + (context as Element).markNeedsBuild(); + }, + (condition) { + setState(() { + selectedCondition = condition; + }); }, ); - }, - ), + } + + final values = + selectedFn.getOperationalValues(); + return ListView.builder( + itemCount: values.length, + itemBuilder: (context, index) { + final value = values[index]; + return ListTile( + leading: SvgPicture.asset( + value.icon, + width: 24, + height: 24, + ), + title: Text( + value.description, + style: context.textTheme.bodyMedium, + ), + trailing: Radio( + value: value.value, + groupValue: + selectedValues[selectedFn.code], + onChanged: (newValue) { + selectedValues[selectedFn.code] = + newValue; + final functionData = + DeviceFunctionData( + entityId: selectedFn.deviceId, + function: selectedFn.code, + operationName: + selectedFn.operationName, + value: newValue, + valueDescription: value.description, + ); + + final existingIndex = + selectedFunctions.indexWhere( + (f) => + f.function == + selectedFn.code); + if (existingIndex != -1) { + selectedFunctions[existingIndex] = + functionData; + } else { + selectedFunctions.add(functionData); + } + (context as Element).markNeedsBuild(); + }, + ), + ); + }, + ); + }, ), + ), ], ), ), @@ -146,55 +221,35 @@ class ThreeGangSwitchHelper { Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - GestureDetector( - onTap: () { - Navigator.pop(context); - }, - child: Container( - height: 50, - width: selectedFunction != null ? 299 : 179, - decoration: const BoxDecoration( - border: Border( - right: - BorderSide(color: ColorsManager.greyColor), - ), - ), - child: Center( - child: Text( - 'Cancel', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: ColorsManager.greyColor), - ), - ), + TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + 'Cancel', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: ColorsManager.greyColor), ), ), - GestureDetector( - onTap: () { - if (selectedFunction != null && - selectedValue != null) { - Navigator.pop(context, { - 'function': selectedFunction, - 'value': selectedValue, - }); - } - }, - child: SizedBox( - height: 50, - width: selectedFunction != null ? 299 : 179, - child: Center( - child: Text( - 'Confirm', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith( - color: - ColorsManager.primaryColorWithOpacity, - ), - ), - ), + TextButton( + onPressed: selectedFunctions.isNotEmpty + ? () { + for (final function in selectedFunctions) { + context + .read() + .add(AddFunction(function)); + } + Navigator.pop(context, true); + } + : null, + child: Text( + 'Confirm', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: ColorsManager.primaryColorWithOpacity, + ), ), ), ], @@ -208,4 +263,78 @@ class ThreeGangSwitchHelper { }, ); } + + /// Build countdown selector for switch functions dialog + static Widget _buildCountDownSelector( + BuildContext context, + StateSetter setState, + dynamic selectedValue, + String? selectedCondition, + List selectedConditions, + Function(dynamic) onValueSelected, + Function(String?) onConditionSelected, + ) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildConditionToggle( + context, + setState, + selectedConditions, + onConditionSelected, + ), + const SizedBox(height: 20), + Text( + '${selectedValue.toString()} sec', + style: Theme.of(context).textTheme.headlineMedium, + ), + const SizedBox(height: 20), + Slider( + value: selectedValue.toDouble(), + min: 0, + max: 300, // 5 minutes in seconds + divisions: 300, + onChanged: (value) { + setState(() { + onValueSelected(value.toInt()); + }); + }, + ), + ], + ); + } + + /// Build condition toggle for AC functions dialog + static Widget _buildConditionToggle( + BuildContext context, + StateSetter setState, + List selectedConditions, + Function(String?) onConditionSelected, + ) { + return ToggleButtons( + onPressed: (int index) { + setState(() { + for (int i = 0; i < selectedConditions.length; i++) { + selectedConditions[i] = i == index; + } + onConditionSelected(index == 0 + ? "<" + : index == 1 + ? "==" + : ">"); + }); + }, + 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: selectedConditions, + children: const [Text("<"), Text("="), Text(">")], + ); + } } diff --git a/lib/pages/routiens/helper/two_gang_switch_helper.dart b/lib/pages/routiens/helper/two_gang_switch_helper.dart index 1d271ac7..6f2febc7 100644 --- a/lib/pages/routiens/helper/two_gang_switch_helper.dart +++ b/lib/pages/routiens/helper/two_gang_switch_helper.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart'; import 'package:syncrow_web/pages/routiens/models/gang_switches/two_gang_switch/two_gang_switch.dart'; @@ -7,7 +9,7 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class TwoGangSwitchHelper { - static Future?> showSwitchFunctionsDialog( + static Future showSwitchFunctionsDialog( BuildContext context, List> functions) async { List> switchFunctions = functions .where((f) => @@ -16,10 +18,12 @@ class TwoGangSwitchHelper { f is TwoGangCountdown1Function || f is TwoGangCountdown2Function) .toList(); - String? selectedFunction; - dynamic selectedValue; + Map selectedValues = {}; + List selectedFunctions = []; + String? selectedCondition = "<"; + List selectedConditions = [true, false, false]; - return showDialog?>( + await showDialog( context: context, builder: (BuildContext context) { return StatefulBuilder( @@ -27,7 +31,7 @@ class TwoGangSwitchHelper { return AlertDialog( contentPadding: EdgeInsets.zero, content: Container( - width: selectedFunction != null ? 600 : 360, + width: 600, height: 450, decoration: BoxDecoration( color: Colors.white, @@ -53,21 +57,23 @@ class TwoGangSwitchHelper { color: ColorsManager.greyColor, ), ), - Flexible( + Expanded( child: Row( children: [ + // Left side: Function list Expanded( child: ListView.separated( - shrinkWrap: false, - physics: const AlwaysScrollableScrollPhysics(), itemCount: switchFunctions.length, - separatorBuilder: (context, index) => - const Divider( + separatorBuilder: (_, __) => const Divider( color: ColorsManager.dividerColor, ), itemBuilder: (context, index) { final function = switchFunctions[index]; + final isSelected = + selectedValues.containsKey(function.code); return ListTile( + tileColor: + isSelected ? Colors.grey.shade100 : null, leading: SvgPicture.asset( function.icon, width: 24, @@ -77,62 +83,130 @@ class TwoGangSwitchHelper { function.operationName, style: context.textTheme.bodyMedium, ), - trailing: const Icon( - Icons.arrow_forward_ios, - size: 16, - color: ColorsManager.textGray, - ), + trailing: isSelected + ? Icon( + Icons.check_circle, + color: ColorsManager + .primaryColorWithOpacity, + size: 20, + ) + : const Icon( + Icons.arrow_forward_ios, + size: 16, + color: ColorsManager.textGray, + ), onTap: () { - setState(() { - selectedFunction = function.code; - selectedValue = null; - }); + if (isSelected) { + selectedValues.remove(function.code); + selectedFunctions.removeWhere( + (f) => f.function == function.code); + } + (context as Element).markNeedsBuild(); }, ); }, ), ), - if (selectedFunction != null) - Expanded( - child: Builder( - builder: (context) { - final selectedFn = switchFunctions.firstWhere( - (f) => f.code == selectedFunction) - as BaseSwitchFunction; - final values = - selectedFn.getOperationalValues(); - return ListView.builder( - shrinkWrap: false, - physics: - const AlwaysScrollableScrollPhysics(), - itemCount: values.length, - itemBuilder: (context, index) { - final value = values[index]; - return ListTile( - leading: SvgPicture.asset( - value.icon, - width: 24, - height: 24, - ), - title: Text( - value.description, - style: context.textTheme.bodyMedium, - ), - trailing: Radio( - value: value.value, - groupValue: selectedValue, - onChanged: (newValue) { - setState(() { - selectedValue = newValue; - }); - }, - ), + // Right side: Value selector + Expanded( + child: Builder( + builder: (context) { + final selectedFn = switchFunctions.firstWhere( + (f) => selectedValues.containsKey(f.code), + orElse: () => switchFunctions.first, + ) as BaseSwitchFunction; + + if (selectedFn is TwoGangCountdown1Function || + selectedFn is TwoGangCountdown2Function) { + return _buildCountDownSelector( + context, + setState, + selectedValues[selectedFn.code] ?? 0, + selectedCondition, + selectedConditions, + (value) { + selectedValues[selectedFn.code] = value; + final functionData = DeviceFunctionData( + entityId: selectedFn.deviceId, + function: selectedFn.code, + operationName: selectedFn.operationName, + value: value, + condition: selectedCondition, + valueDescription: '${value} sec', ); + + final existingIndex = + selectedFunctions.indexWhere((f) => + f.function == selectedFn.code); + if (existingIndex != -1) { + selectedFunctions[existingIndex] = + functionData; + } else { + selectedFunctions.add(functionData); + } + (context as Element).markNeedsBuild(); + }, + (condition) { + setState(() { + selectedCondition = condition; + }); }, ); - }, - ), + } + + final values = + selectedFn.getOperationalValues(); + return ListView.builder( + itemCount: values.length, + itemBuilder: (context, index) { + final value = values[index]; + return ListTile( + leading: SvgPicture.asset( + value.icon, + width: 24, + height: 24, + ), + title: Text( + value.description, + style: context.textTheme.bodyMedium, + ), + trailing: Radio( + value: value.value, + groupValue: + selectedValues[selectedFn.code], + onChanged: (newValue) { + selectedValues[selectedFn.code] = + newValue; + final functionData = + DeviceFunctionData( + entityId: selectedFn.deviceId, + function: selectedFn.code, + operationName: + selectedFn.operationName, + value: newValue, + valueDescription: value.description, + ); + + final existingIndex = + selectedFunctions.indexWhere( + (f) => + f.function == + selectedFn.code); + if (existingIndex != -1) { + selectedFunctions[existingIndex] = + functionData; + } else { + selectedFunctions.add(functionData); + } + (context as Element).markNeedsBuild(); + }, + ), + ); + }, + ); + }, ), + ), ], ), ), @@ -144,55 +218,35 @@ class TwoGangSwitchHelper { Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - GestureDetector( - onTap: () { - Navigator.pop(context); - }, - child: Container( - height: 50, - width: selectedFunction != null ? 299 : 179, - decoration: const BoxDecoration( - border: Border( - right: - BorderSide(color: ColorsManager.greyColor), - ), - ), - child: Center( - child: Text( - 'Cancel', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: ColorsManager.greyColor), - ), - ), + TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + 'Cancel', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: ColorsManager.greyColor), ), ), - GestureDetector( - onTap: () { - if (selectedFunction != null && - selectedValue != null) { - Navigator.pop(context, { - 'function': selectedFunction, - 'value': selectedValue, - }); - } - }, - child: SizedBox( - height: 50, - width: selectedFunction != null ? 299 : 179, - child: Center( - child: Text( - 'Confirm', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith( - color: - ColorsManager.primaryColorWithOpacity, - ), - ), - ), + TextButton( + onPressed: selectedFunctions.isNotEmpty + ? () { + for (final function in selectedFunctions) { + context + .read() + .add(AddFunction(function)); + } + Navigator.pop(context, true); + } + : null, + child: Text( + 'Confirm', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: ColorsManager.primaryColorWithOpacity, + ), ), ), ], @@ -206,4 +260,78 @@ class TwoGangSwitchHelper { }, ); } + + /// Build countdown selector for switch functions dialog + static Widget _buildCountDownSelector( + BuildContext context, + StateSetter setState, + dynamic selectedValue, + String? selectedCondition, + List selectedConditions, + Function(dynamic) onValueSelected, + Function(String?) onConditionSelected, + ) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildConditionToggle( + context, + setState, + selectedConditions, + onConditionSelected, + ), + const SizedBox(height: 20), + Text( + '${selectedValue.toString()} sec', + style: Theme.of(context).textTheme.headlineMedium, + ), + const SizedBox(height: 20), + Slider( + value: selectedValue.toDouble(), + min: 0, + max: 300, // 5 minutes in seconds + divisions: 300, + onChanged: (value) { + setState(() { + onValueSelected(value.toInt()); + }); + }, + ), + ], + ); + } + + /// Build condition toggle for AC functions dialog + static Widget _buildConditionToggle( + BuildContext context, + StateSetter setState, + List selectedConditions, + Function(String?) onConditionSelected, + ) { + return ToggleButtons( + onPressed: (int index) { + setState(() { + for (int i = 0; i < selectedConditions.length; i++) { + selectedConditions[i] = i == index; + } + onConditionSelected(index == 0 + ? "<" + : index == 1 + ? "==" + : ">"); + }); + }, + 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: selectedConditions, + children: const [Text("<"), Text("="), Text(">")], + ); + } } diff --git a/lib/pages/routiens/models/device_functions.dart b/lib/pages/routiens/models/device_functions.dart index fa6ac673..303fe48c 100644 --- a/lib/pages/routiens/models/device_functions.dart +++ b/lib/pages/routiens/models/device_functions.dart @@ -15,3 +15,47 @@ abstract class DeviceFunction { T execute(T currentStatus, dynamic newValue); } + +class DeviceFunctionData { + final String entityId; + final String actionExecutor; + final String function; + final String operationName; + final dynamic value; + final String? condition; + final String? valueDescription; + + DeviceFunctionData({ + required this.entityId, + this.actionExecutor = 'function', + required this.function, + required this.operationName, + required this.value, + this.condition, + this.valueDescription, + }); + + Map toJson() { + return { + 'entityId': entityId, + 'actionExecutor': actionExecutor, + 'function': function, + 'operationName': operationName, + 'value': value, + if (condition != null) 'condition': condition, + if (valueDescription != null) 'valueDescription': valueDescription, + }; + } + + factory DeviceFunctionData.fromJson(Map json) { + return DeviceFunctionData( + entityId: json['entityId'], + actionExecutor: json['actionExecutor'] ?? 'function', + function: json['function'], + operationName: json['operationName'], + value: json['value'], + condition: json['condition'], + valueDescription: json['valueDescription'], + ); + } +} diff --git a/lib/pages/routiens/widgets/dragable_card.dart b/lib/pages/routiens/widgets/dragable_card.dart index 8ed4afff..7ee078ff 100644 --- a/lib/pages/routiens/widgets/dragable_card.dart +++ b/lib/pages/routiens/widgets/dragable_card.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; @@ -46,51 +48,110 @@ class DraggableCard extends StatelessWidget { } Widget _buildCardContent(BuildContext context) { - return Card( - color: ColorsManager.whiteColors, - child: SizedBox( - height: 123, - width: 90, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - height: 50, - width: 50, - decoration: BoxDecoration( - color: ColorsManager.CircleImageBackground, - borderRadius: BorderRadius.circular(90), - border: Border.all( - color: ColorsManager.graysColor, + return BlocBuilder( + builder: (context, state) { + // Filter functions for this device + final deviceFunctions = state.selectedFunctions + .where((f) => f.entityId == deviceData?['deviceId']) + .toList(); + + return Card( + color: ColorsManager.whiteColors, + child: SizedBox( + width: 90, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + height: 123, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + height: 50, + width: 50, + decoration: BoxDecoration( + color: ColorsManager.CircleImageBackground, + borderRadius: BorderRadius.circular(90), + border: Border.all( + color: ColorsManager.graysColor, + ), + ), + padding: const EdgeInsets.all(8), + child: imagePath.contains('.svg') + ? SvgPicture.asset( + imagePath, + ) + : Image.network(imagePath), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 3), + child: Text( + title, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + maxLines: 2, + style: context.textTheme.bodySmall?.copyWith( + color: titleColor ?? ColorsManager.blackColor, + fontSize: 12, + ), + ), + ), + ], + ), ), - ), - padding: const EdgeInsets.all(8), - child: imagePath.contains('.svg') - ? SvgPicture.asset( - imagePath, - ) - : Image.network(imagePath), + if (deviceFunctions.isNotEmpty) ...[ + const Divider(height: 1), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: + const EdgeInsets.symmetric(vertical: 4, horizontal: 4), + itemCount: deviceFunctions.length, + itemBuilder: (context, index) { + final function = deviceFunctions[index]; + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: Text( + '${function.operationName}: ${function.valueDescription}', + style: context.textTheme.bodySmall?.copyWith( + fontSize: 9, + color: ColorsManager.textGray, + height: 1.2, + ), + overflow: TextOverflow.ellipsis, + ), + ), + InkWell( + onTap: () { + context.read().add( + RemoveFunction(function), + ); + }, + child: const Padding( + padding: EdgeInsets.all(2), + child: Icon( + Icons.close, + size: 12, + color: ColorsManager.textGray, + ), + ), + ), + ], + ); + }, + ), + ], + ], ), - const SizedBox( - height: 8, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 3), - child: Text( - title, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - maxLines: 2, - style: context.textTheme.bodySmall?.copyWith( - color: titleColor ?? ColorsManager.blackColor, - fontSize: 12, - ), - ), - ), - ], - ), - ), + ), + ); + }, ); } From fb4a4d4d6cb69623060932cbd01792dba524e112 Mon Sep 17 00:00:00 2001 From: ashrafzarkanisala Date: Fri, 22 Nov 2024 19:55:16 +0300 Subject: [PATCH 08/23] push value notifers --- lib/main.dart | 12 +- lib/pages/routiens/helper/ac_helper.dart | 481 +++++++++++------- .../helper/one_gang_switch_helper.dart | 426 +++++++--------- .../helper/two_gang_switch_helper.dart | 428 +++++++--------- .../view/create_new_routine_view.dart | 125 +++-- lib/pages/routiens/view/routines_view.dart | 107 ++-- .../conditions_routines_devices_view.dart | 170 ++++--- lib/pages/routiens/widgets/dialog_footer.dart | 76 +++ lib/pages/routiens/widgets/dialog_header.dart | 31 ++ lib/pages/routiens/widgets/dragable_card.dart | 235 ++++----- lib/pages/routiens/widgets/if_container.dart | 1 + .../routiens/widgets/routine_devices.dart | 59 ++- .../widgets/scenes_and_automations.dart | 67 ++- .../routiens/widgets/then_container.dart | 1 + 14 files changed, 1173 insertions(+), 1046 deletions(-) create mode 100644 lib/pages/routiens/widgets/dialog_footer.dart create mode 100644 lib/pages/routiens/widgets/dialog_header.dart diff --git a/lib/main.dart b/lib/main.dart index c544f227..ac002f85 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:go_router/go_router.dart'; import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart'; import 'package:syncrow_web/pages/home/bloc/home_bloc.dart'; import 'package:syncrow_web/pages/home/bloc/home_event.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart'; import 'package:syncrow_web/services/locator.dart'; import 'package:syncrow_web/utils/app_routes.dart'; @@ -14,7 +15,8 @@ import 'package:syncrow_web/utils/theme/theme.dart'; Future main() async { try { - const environment = String.fromEnvironment('FLAVOR', defaultValue: 'development'); + const environment = + String.fromEnvironment('FLAVOR', defaultValue: 'development'); await dotenv.load(fileName: '.env.$environment'); WidgetsFlutterBinding.ensureInitialized(); initialSetup(); @@ -46,10 +48,14 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MultiBlocProvider( providers: [ - BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())), + BlocProvider( + create: (context) => HomeBloc()..add(const FetchUserInfo())), BlocProvider( create: (context) => VisitorPasswordBloc(), - ) + ), + BlocProvider( + create: (context) => RoutineBloc(), + ), ], child: MaterialApp.router( debugShowCheckedModeBanner: false, diff --git a/lib/pages/routiens/helper/ac_helper.dart b/lib/pages/routiens/helper/ac_helper.dart index b14acd3a..9ba0b224 100644 --- a/lib/pages/routiens/helper/ac_helper.dart +++ b/lib/pages/routiens/helper/ac_helper.dart @@ -1,219 +1,330 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/models/ac/ac_function.dart'; import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class ACHelper { - static Future showACFunctionsDialog( + static Future?> showACFunctionsDialog( BuildContext context, List> functions, ) async { List acFunctions = functions.whereType().toList(); - // Track multiple selections using a map - Map selectedValues = {}; - List selectedFunctions = []; + String? selectedFunction; + dynamic selectedValue = 20; + String? selectedCondition = "=="; + List _selectedConditions = [false, true, false]; - await showDialog( + return showDialog?>( context: context, builder: (BuildContext context) { - return Dialog( - child: Container( - width: 600, - height: 450, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(20), - ), - padding: const EdgeInsets.only(top: 20), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'AC Functions', - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: ColorsManager.primaryColorWithOpacity, - fontWeight: FontWeight.bold, - ), - ), - Padding( - padding: - const EdgeInsets.symmetric(vertical: 15, horizontal: 50), - child: Container( - height: 1, - width: double.infinity, - color: ColorsManager.greyColor, - ), - ), - Expanded( - child: Row( - children: [ - Expanded( - child: ListView.separated( - itemCount: acFunctions.length, - separatorBuilder: (_, __) => const Divider( - color: ColorsManager.dividerColor, - ), - itemBuilder: (context, index) { - final function = acFunctions[index]; - final isSelected = - selectedValues.containsKey(function.code); - return ListTile( - tileColor: - isSelected ? Colors.grey.shade100 : null, - leading: SvgPicture.asset( - function.icon, - width: 24, - height: 24, - ), - title: Text( - function.operationName, - style: context.textTheme.bodyMedium, - ), - trailing: isSelected - ? Icon( - Icons.check_circle, - color: - ColorsManager.primaryColorWithOpacity, - size: 20, - ) - : const Icon( - Icons.arrow_forward_ios, - size: 16, - color: ColorsManager.textGray, - ), - onTap: () { - if (isSelected) { - selectedValues.remove(function.code); - selectedFunctions.removeWhere( - (f) => f.function == function.code); - } - (context as Element).markNeedsBuild(); - }, - ); - }, - ), - ), - Expanded( - child: Builder( - builder: (context) { - final selectedFunction = acFunctions.firstWhere( - (f) => selectedValues.containsKey(f.code), - orElse: () => acFunctions.first, - ); - return _buildValueSelector( - context, - selectedFunction, - selectedValues[selectedFunction.code], - (value) { - selectedValues[selectedFunction.code] = value; - // Update or add the function data - final functionData = DeviceFunctionData( - entityId: selectedFunction.deviceId, - function: selectedFunction.code, - operationName: selectedFunction.operationName, - value: value, - valueDescription: _getValueDescription( - selectedFunction, value), - ); - - final existingIndex = - selectedFunctions.indexWhere((f) => - f.function == selectedFunction.code); - if (existingIndex != -1) { - selectedFunctions[existingIndex] = - functionData; - } else { - selectedFunctions.add(functionData); - } - - (context as Element).markNeedsBuild(); - }, - ); - }, - ), - ), - ], - ), - ), - Container( - height: 1, - width: double.infinity, - color: ColorsManager.greyColor, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text( - 'Cancel', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: ColorsManager.greyColor), - ), - ), - TextButton( - onPressed: selectedFunctions.isNotEmpty - ? () { - // Add all selected functions to the bloc - for (final function in selectedFunctions) { - context - .read() - .add(AddFunction(function)); - } - Navigator.pop(context, true); - } - : null, - child: Text( - 'Confirm', - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: ColorsManager.primaryColorWithOpacity, - ), - ), - ), - ], - ), - ], - ), - ), + return StatefulBuilder( + builder: (context, setState) { + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: _buildDialogContent( + context, + setState, + acFunctions, + selectedFunction, + selectedValue, + selectedCondition, + _selectedConditions, + (fn) => selectedFunction = fn, + (val) => selectedValue = val, + (cond) => selectedCondition = cond, + ), + ); + }, ); }, ); } - static Widget _buildValueSelector( + /// Build dialog content for AC functions dialog + static Widget _buildDialogContent( BuildContext context, - ACFunction function, + StateSetter setState, + List acFunctions, + String? selectedFunction, dynamic selectedValue, + String? selectedCondition, + List selectedConditions, + Function(String?) onFunctionSelected, Function(dynamic) onValueSelected, + Function(String?) onConditionSelected, ) { - final values = function.getOperationalValues(); return Container( - height: 200, - padding: const EdgeInsets.symmetric(horizontal: 20), - child: ListView.builder( - itemCount: values.length, + 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('AC Functions'), + Flexible( + child: Row( + children: [ + _buildFunctionsList( + context, + setState, + acFunctions, + selectedFunction, + onFunctionSelected, + ), + if (selectedFunction != null) + _buildValueSelector( + context, + setState, + selectedFunction, + selectedValue, + selectedCondition, + selectedConditions, + onValueSelected, + onConditionSelected, + acFunctions, + ), + ], + ), + ), + DialogFooter( + onCancel: () => Navigator.pop(context), + onConfirm: selectedFunction != null && selectedValue != null + ? () => Navigator.pop(context, { + 'function': selectedFunction, + 'value': selectedValue, + 'condition': selectedCondition ?? "==", + }) + : null, + isConfirmEnabled: selectedFunction != null && selectedValue != null, + ), + ], + ), + ); + } + + /// Build functions list for AC functions dialog + static Widget _buildFunctionsList( + BuildContext context, + StateSetter setState, + List acFunctions, + String? selectedFunction, + Function(String?) onFunctionSelected, + ) { + return Expanded( + child: ListView.separated( + shrinkWrap: false, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: acFunctions.length, + separatorBuilder: (context, index) => const Divider( + color: ColorsManager.dividerColor, + ), itemBuilder: (context, index) { - final value = values[index]; - return RadioListTile( - value: value.value, - groupValue: selectedValue, - onChanged: onValueSelected, - title: Text(value.description), + final function = acFunctions[index]; + return ListTile( + leading: SvgPicture.asset( + function.icon, + 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: () => setState(() => onFunctionSelected(function.code)), ); }, ), ); } - static String _getValueDescription(ACFunction function, dynamic value) { - final values = function.getOperationalValues(); - final selectedValue = values.firstWhere((v) => v.value == value); - return selectedValue.description; + /// Build value selector for AC functions dialog + static Widget _buildValueSelector( + BuildContext context, + StateSetter setState, + String selectedFunction, + dynamic selectedValue, + String? selectedCondition, + List selectedConditions, + Function(dynamic) onValueSelected, + Function(String?) onConditionSelected, + List acFunctions, + ) { + if (selectedFunction == 'temp_set' || selectedFunction == 'temp_current') { + return Expanded( + child: _buildTemperatureSelector( + context, + setState, + selectedValue, + selectedCondition, + selectedConditions, + onValueSelected, + onConditionSelected, + ), + ); + } + + final selectedFn = + acFunctions.firstWhere((f) => f.code == selectedFunction); + final values = selectedFn.getOperationalValues(); + return Expanded( + child: _buildOperationalValuesList( + context, + setState, + values, + selectedValue, + onValueSelected, + ), + ); + } + + /// Build temperature selector for AC functions dialog + static Widget _buildTemperatureSelector( + BuildContext context, + StateSetter setState, + dynamic selectedValue, + String? selectedCondition, + List selectedConditions, + Function(dynamic) onValueSelected, + Function(String?) onConditionSelected, + ) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildConditionToggle( + context, + setState, + selectedConditions, + onConditionSelected, + ), + const SizedBox(height: 20), + _buildTemperatureDisplay(context, selectedValue), + const SizedBox(height: 20), + _buildTemperatureSlider( + context, + setState, + selectedValue, + onValueSelected, + ), + ], + ); + } + + /// Build condition toggle for AC functions dialog + static Widget _buildConditionToggle( + BuildContext context, + StateSetter setState, + List selectedConditions, + Function(String?) onConditionSelected, + ) { + return ToggleButtons( + onPressed: (int index) { + setState(() { + for (int i = 0; i < selectedConditions.length; i++) { + selectedConditions[i] = i == index; + } + onConditionSelected(index == 0 + ? "<" + : index == 1 + ? "==" + : ">"); + }); + }, + 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: selectedConditions, + children: const [Text("<"), Text("="), Text(">")], + ); + } + + /// Build temperature display for AC functions dialog + static Widget _buildTemperatureDisplay( + BuildContext context, dynamic selectedValue) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + decoration: BoxDecoration( + color: ColorsManager.primaryColorWithOpacity.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + '${selectedValue ?? 20}°C', + style: context.textTheme.headlineMedium!.copyWith( + color: ColorsManager.primaryColorWithOpacity, + ), + ), + ); + } + + static Widget _buildTemperatureSlider( + BuildContext context, + StateSetter setState, + dynamic selectedValue, + Function(dynamic) onValueSelected, + ) { + final currentValue = selectedValue is int ? selectedValue.toDouble() : 20.0; + return Slider( + value: currentValue, + min: 16, + max: 30, + divisions: 14, + label: '${currentValue.toInt()}°C', + onChanged: (value) { + setState(() => onValueSelected(value.toInt())); + }, + ); + } + + static Widget _buildOperationalValuesList( + BuildContext context, + StateSetter setState, + List values, + dynamic selectedValue, + Function(dynamic) onValueSelected, + ) { + return ListView.builder( + shrinkWrap: false, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: values.length, + itemBuilder: (context, index) { + final value = values[index]; + return ListTile( + leading: SvgPicture.asset( + value.icon, + width: 24, + height: 24, + ), + title: Text( + value.description, + style: context.textTheme.bodyMedium, + ), + trailing: Radio( + value: value.value, + groupValue: selectedValue, + onChanged: (newValue) { + setState(() => onValueSelected(newValue)); + }, + ), + ); + }, + ); } } diff --git a/lib/pages/routiens/helper/one_gang_switch_helper.dart b/lib/pages/routiens/helper/one_gang_switch_helper.dart index c9a79377..0837829b 100644 --- a/lib/pages/routiens/helper/one_gang_switch_helper.dart +++ b/lib/pages/routiens/helper/one_gang_switch_helper.dart @@ -1,34 +1,38 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart'; import 'package:syncrow_web/pages/routiens/models/gang_switches/one_gang_switch/one_gang_switch.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class OneGangSwitchHelper { - static Future showSwitchFunctionsDialog( - BuildContext context, List> functions) async { + static Future?> showSwitchFunctionsDialog( + BuildContext context, + List> functions, + ) async { List> switchFunctions = functions .where( (f) => f is OneGangSwitchFunction || f is OneGangCountdownFunction) .toList(); - Map selectedValues = {}; - List selectedFunctions = []; - String? selectedCondition = "<"; - List selectedConditions = [true, false, false]; + final selectedFunctionNotifier = ValueNotifier(null); + final selectedValueNotifier = ValueNotifier(null); + final selectedConditionNotifier = ValueNotifier("<"); + final selectedConditionsNotifier = + ValueNotifier>([true, false, false]); - await showDialog( + return showDialog?>( context: context, builder: (BuildContext context) { - return StatefulBuilder( - builder: (context, setState) { + return ValueListenableBuilder( + valueListenable: selectedFunctionNotifier, + builder: (context, selectedFunction, _) { return AlertDialog( contentPadding: EdgeInsets.zero, content: Container( - width: 600, + width: selectedFunction != null ? 600 : 360, height: 450, decoration: BoxDecoration( color: Colors.white, @@ -38,22 +42,7 @@ class OneGangSwitchHelper { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text( - '1 Gang Light Switch Condition', - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: ColorsManager.primaryColorWithOpacity, - fontWeight: FontWeight.bold, - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 15, horizontal: 50), - child: Container( - height: 1, - width: double.infinity, - color: ColorsManager.greyColor, - ), - ), + const DialogHeader('1 Gang Light Switch Condition'), Expanded( child: Row( children: [ @@ -66,143 +55,67 @@ class OneGangSwitchHelper { ), itemBuilder: (context, index) { final function = switchFunctions[index]; - final isSelected = - selectedValues.containsKey(function.code); - return ListTile( - tileColor: - isSelected ? Colors.grey.shade100 : null, - leading: SvgPicture.asset( - function.icon, - width: 24, - height: 24, - ), - title: Text( - function.operationName, - style: context.textTheme.bodyMedium, - ), - trailing: isSelected - ? Icon( - Icons.check_circle, - color: ColorsManager - .primaryColorWithOpacity, - size: 20, - ) - : const Icon( - Icons.arrow_forward_ios, - size: 16, - color: ColorsManager.textGray, - ), - onTap: () { - if (isSelected) { - selectedValues.remove(function.code); - selectedFunctions.removeWhere( - (f) => f.function == function.code); - } - (context as Element).markNeedsBuild(); - }, - ); - }, - ), - ), - // Right side: Value selector - Expanded( - child: Builder( - builder: (context) { - final selectedFn = switchFunctions.firstWhere( - (f) => selectedValues.containsKey(f.code), - orElse: () => switchFunctions.first, - ) as BaseSwitchFunction; - - if (selectedFn is OneGangCountdownFunction) { - return _buildCountDownSelector( - context, - setState, - selectedValues[selectedFn.code] ?? 0, - selectedCondition, - selectedConditions, - (value) { - selectedValues[selectedFn.code] = value; - final functionData = DeviceFunctionData( - entityId: selectedFn.deviceId, - function: selectedFn.code, - operationName: selectedFn.operationName, - value: value, - condition: selectedCondition, - valueDescription: '${value} sec', - ); - - final existingIndex = - selectedFunctions.indexWhere((f) => - f.function == selectedFn.code); - if (existingIndex != -1) { - selectedFunctions[existingIndex] = - functionData; - } else { - selectedFunctions.add(functionData); - } - (context as Element).markNeedsBuild(); - }, - (condition) { - setState(() { - selectedCondition = condition; - }); - }, - ); - } - - final values = - selectedFn.getOperationalValues(); - return ListView.builder( - itemCount: values.length, - itemBuilder: (context, index) { - final value = values[index]; + return ValueListenableBuilder( + valueListenable: selectedFunctionNotifier, + builder: (context, selectedFunction, _) { + final isSelected = + selectedFunction == function.code; return ListTile( + tileColor: isSelected + ? Colors.grey.shade100 + : null, leading: SvgPicture.asset( - value.icon, + function.icon, width: 24, height: 24, ), title: Text( - value.description, + function.operationName, style: context.textTheme.bodyMedium, ), - trailing: Radio( - value: value.value, - groupValue: - selectedValues[selectedFn.code], - onChanged: (newValue) { - selectedValues[selectedFn.code] = - newValue; - final functionData = - DeviceFunctionData( - entityId: selectedFn.deviceId, - function: selectedFn.code, - operationName: - selectedFn.operationName, - value: newValue, - valueDescription: value.description, - ); - - final existingIndex = - selectedFunctions.indexWhere( - (f) => - f.function == - selectedFn.code); - if (existingIndex != -1) { - selectedFunctions[existingIndex] = - functionData; - } else { - selectedFunctions.add(functionData); - } - (context as Element).markNeedsBuild(); - }, + trailing: const Icon( + Icons.arrow_forward_ios, + size: 16, + color: ColorsManager.textGray, ), + onTap: () { + selectedFunctionNotifier.value = + function.code; + selectedValueNotifier.value = + function is OneGangCountdownFunction + ? 0 + : null; + }, ); }, ); }, ), ), + // Right side: Value selector + if (selectedFunction != null) + ValueListenableBuilder( + valueListenable: selectedFunctionNotifier, + builder: (context, selectedFunction, _) { + final selectedFn = switchFunctions.firstWhere( + (f) => f.code == selectedFunction, + ); + return Expanded( + child: selectedFn is OneGangCountdownFunction + ? _buildCountDownSelector( + context, + selectedValueNotifier, + selectedConditionNotifier, + selectedConditionsNotifier, + ) + : _buildOperationalValuesList( + context, + selectedFn as BaseSwitchFunction, + selectedValueNotifier, + ), + ); + }, + ), ], ), ), @@ -211,41 +124,36 @@ class OneGangSwitchHelper { width: double.infinity, color: ColorsManager.greyColor, ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text( - 'Cancel', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: ColorsManager.greyColor), - ), - ), - TextButton( - onPressed: selectedFunctions.isNotEmpty - ? () { - for (final function in selectedFunctions) { - context - .read() - .add(AddFunction(function)); - } - Navigator.pop(context, true); - } - : null, - child: Text( - 'Confirm', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith( - color: ColorsManager.primaryColorWithOpacity, - ), - ), - ), - ], + DialogFooter( + onCancel: () => Navigator.pop(context), + onConfirm: selectedFunctionNotifier.value != null && + selectedValueNotifier.value != null + ? () { + final selectedFn = switchFunctions.firstWhere( + (f) => f.code == selectedFunctionNotifier.value, + ); + final value = selectedValueNotifier.value; + final functionData = DeviceFunctionData( + entityId: selectedFn.deviceId, + function: selectedFn.code, + operationName: selectedFn.operationName, + value: value, + condition: selectedConditionNotifier.value, + valueDescription: + selectedFn is OneGangCountdownFunction + ? '${value} sec' + : ((selectedFn as BaseSwitchFunction) + .getOperationalValues() + .firstWhere((v) => v.value == value) + .description), + ); + Navigator.pop( + context, {selectedFn.code: functionData}); + } + : null, + isConfirmEnabled: + selectedFunctionNotifier.value != null && + selectedValueNotifier.value != null, ), ], ), @@ -257,77 +165,101 @@ class OneGangSwitchHelper { ); } - /// Build countdown selector for switch functions dialog static Widget _buildCountDownSelector( BuildContext context, - StateSetter setState, - dynamic selectedValue, - String? selectedCondition, - List selectedConditions, - Function(dynamic) onValueSelected, - Function(String?) onConditionSelected, + ValueNotifier valueNotifier, + ValueNotifier conditionNotifier, + ValueNotifier> conditionsNotifier, ) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildConditionToggle( - context, - setState, - selectedConditions, - onConditionSelected, - ), - const SizedBox(height: 20), - Text( - '${selectedValue.toString()} sec', - style: Theme.of(context).textTheme.headlineMedium, - ), - const SizedBox(height: 20), - Slider( - value: selectedValue.toDouble(), - min: 0, - max: 300, // 5 minutes in seconds - divisions: 300, - onChanged: (value) { - setState(() { - onValueSelected(value.toInt()); - }); - }, - ), - ], + return ValueListenableBuilder( + valueListenable: valueNotifier, + builder: (context, value, _) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ValueListenableBuilder>( + valueListenable: conditionsNotifier, + builder: (context, selectedConditions, _) { + return ToggleButtons( + onPressed: (int index) { + final newConditions = List.filled(3, false); + newConditions[index] = true; + conditionsNotifier.value = newConditions; + conditionNotifier.value = index == 0 + ? "<" + : index == 1 + ? "==" + : ">"; + }, + 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: selectedConditions, + children: const [Text("<"), Text("="), Text(">")], + ); + }, + ), + const SizedBox(height: 20), + Text( + '${value ?? 0} sec', + style: Theme.of(context).textTheme.headlineMedium, + ), + const SizedBox(height: 20), + Slider( + value: (value ?? 0).toDouble(), + min: 0, + max: 300, + divisions: 300, + onChanged: (newValue) { + valueNotifier.value = newValue.toInt(); + }, + ), + ], + ); + }, ); } - /// Build condition toggle for AC functions dialog - static Widget _buildConditionToggle( + static Widget _buildOperationalValuesList( BuildContext context, - StateSetter setState, - List selectedConditions, - Function(String?) onConditionSelected, + BaseSwitchFunction function, + ValueNotifier valueNotifier, ) { - return ToggleButtons( - onPressed: (int index) { - setState(() { - for (int i = 0; i < selectedConditions.length; i++) { - selectedConditions[i] = i == index; - } - onConditionSelected(index == 0 - ? "<" - : index == 1 - ? "==" - : ">"); - }); + final values = function.getOperationalValues(); + return ValueListenableBuilder( + valueListenable: valueNotifier, + builder: (context, selectedValue, _) { + return ListView.builder( + itemCount: values.length, + itemBuilder: (context, index) { + final value = values[index]; + return ListTile( + leading: SvgPicture.asset( + value.icon, + width: 24, + height: 24, + ), + title: Text( + value.description, + style: context.textTheme.bodyMedium, + ), + trailing: Radio( + value: value.value, + groupValue: selectedValue, + onChanged: (newValue) { + valueNotifier.value = newValue; + }, + ), + ); + }, + ); }, - 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: selectedConditions, - children: const [Text("<"), Text("="), Text(">")], ); } } diff --git a/lib/pages/routiens/helper/two_gang_switch_helper.dart b/lib/pages/routiens/helper/two_gang_switch_helper.dart index 6f2febc7..ae6abac4 100644 --- a/lib/pages/routiens/helper/two_gang_switch_helper.dart +++ b/lib/pages/routiens/helper/two_gang_switch_helper.dart @@ -5,6 +5,8 @@ import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart'; import 'package:syncrow_web/pages/routiens/models/gang_switches/two_gang_switch/two_gang_switch.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; @@ -18,20 +20,23 @@ class TwoGangSwitchHelper { f is TwoGangCountdown1Function || f is TwoGangCountdown2Function) .toList(); - Map selectedValues = {}; - List selectedFunctions = []; - String? selectedCondition = "<"; - List selectedConditions = [true, false, false]; + + final selectedFunctionNotifier = ValueNotifier(null); + final selectedValueNotifier = ValueNotifier(null); + final selectedConditionNotifier = ValueNotifier('<'); + final selectedConditionsNotifier = + ValueNotifier>([true, false, false]); await showDialog( context: context, builder: (BuildContext context) { - return StatefulBuilder( - builder: (context, setState) { + return ValueListenableBuilder( + valueListenable: selectedFunctionNotifier, + builder: (context, selectedFunction, _) { return AlertDialog( contentPadding: EdgeInsets.zero, content: Container( - width: 600, + width: selectedFunction != null ? 600 : 300, height: 450, decoration: BoxDecoration( color: Colors.white, @@ -41,22 +46,7 @@ class TwoGangSwitchHelper { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text( - '2 Gangs Light Switch Condition', - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: ColorsManager.primaryColorWithOpacity, - fontWeight: FontWeight.bold, - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 15, horizontal: 50), - child: Container( - height: 1, - width: double.infinity, - color: ColorsManager.greyColor, - ), - ), + const DialogHeader('2 Gangs Light Switch Condition'), Expanded( child: Row( children: [ @@ -69,187 +59,105 @@ class TwoGangSwitchHelper { ), itemBuilder: (context, index) { final function = switchFunctions[index]; - final isSelected = - selectedValues.containsKey(function.code); - return ListTile( - tileColor: - isSelected ? Colors.grey.shade100 : null, - leading: SvgPicture.asset( - function.icon, - width: 24, - height: 24, - ), - title: Text( - function.operationName, - style: context.textTheme.bodyMedium, - ), - trailing: isSelected - ? Icon( - Icons.check_circle, - color: ColorsManager - .primaryColorWithOpacity, - size: 20, - ) - : const Icon( - Icons.arrow_forward_ios, - size: 16, - color: ColorsManager.textGray, - ), - onTap: () { - if (isSelected) { - selectedValues.remove(function.code); - selectedFunctions.removeWhere( - (f) => f.function == function.code); - } - (context as Element).markNeedsBuild(); - }, - ); - }, - ), - ), - // Right side: Value selector - Expanded( - child: Builder( - builder: (context) { - final selectedFn = switchFunctions.firstWhere( - (f) => selectedValues.containsKey(f.code), - orElse: () => switchFunctions.first, - ) as BaseSwitchFunction; - - if (selectedFn is TwoGangCountdown1Function || - selectedFn is TwoGangCountdown2Function) { - return _buildCountDownSelector( - context, - setState, - selectedValues[selectedFn.code] ?? 0, - selectedCondition, - selectedConditions, - (value) { - selectedValues[selectedFn.code] = value; - final functionData = DeviceFunctionData( - entityId: selectedFn.deviceId, - function: selectedFn.code, - operationName: selectedFn.operationName, - value: value, - condition: selectedCondition, - valueDescription: '${value} sec', - ); - - final existingIndex = - selectedFunctions.indexWhere((f) => - f.function == selectedFn.code); - if (existingIndex != -1) { - selectedFunctions[existingIndex] = - functionData; - } else { - selectedFunctions.add(functionData); - } - (context as Element).markNeedsBuild(); - }, - (condition) { - setState(() { - selectedCondition = condition; - }); - }, - ); - } - - final values = - selectedFn.getOperationalValues(); - return ListView.builder( - itemCount: values.length, - itemBuilder: (context, index) { - final value = values[index]; + return ValueListenableBuilder( + valueListenable: selectedFunctionNotifier, + builder: (context, selectedFunction, _) { + final isSelected = + selectedFunction == function.code; return ListTile( + tileColor: isSelected + ? Colors.grey.shade100 + : null, leading: SvgPicture.asset( - value.icon, + function.icon, width: 24, height: 24, ), title: Text( - value.description, + function.operationName, style: context.textTheme.bodyMedium, ), - trailing: Radio( - value: value.value, - groupValue: - selectedValues[selectedFn.code], - onChanged: (newValue) { - selectedValues[selectedFn.code] = - newValue; - final functionData = - DeviceFunctionData( - entityId: selectedFn.deviceId, - function: selectedFn.code, - operationName: - selectedFn.operationName, - value: newValue, - valueDescription: value.description, - ); - - final existingIndex = - selectedFunctions.indexWhere( - (f) => - f.function == - selectedFn.code); - if (existingIndex != -1) { - selectedFunctions[existingIndex] = - functionData; - } else { - selectedFunctions.add(functionData); - } - (context as Element).markNeedsBuild(); - }, + trailing: const Icon( + Icons.arrow_forward_ios, + size: 16, + color: ColorsManager.textGray, ), + onTap: () { + selectedFunctionNotifier.value = + function.code; + selectedValueNotifier.value = function + is TwoGangCountdown1Function || + function + is TwoGangCountdown2Function + ? 0 + : null; + }, ); }, ); }, ), ), + // Right side: Value selector + if (selectedFunction != null) + Expanded( + child: ValueListenableBuilder( + valueListenable: selectedValueNotifier, + builder: (context, selectedValue, _) { + final selectedFn = switchFunctions.firstWhere( + (f) => f.code == selectedFunction, + ); + + if (selectedFn is TwoGangCountdown1Function || + selectedFn is TwoGangCountdown2Function) { + return _buildCountDownSelector( + context, + selectedValueNotifier, + selectedConditionNotifier, + selectedConditionsNotifier, + ); + } + + return _buildOperationalValuesList( + context, + selectedFn as BaseSwitchFunction, + selectedValueNotifier, + ); + }, + ), + ), ], ), ), - Container( - height: 1, - width: double.infinity, - color: ColorsManager.greyColor, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text( - 'Cancel', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: ColorsManager.greyColor), - ), - ), - TextButton( - onPressed: selectedFunctions.isNotEmpty - ? () { - for (final function in selectedFunctions) { - context - .read() - .add(AddFunction(function)); - } - Navigator.pop(context, true); - } - : null, - child: Text( - 'Confirm', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith( - color: ColorsManager.primaryColorWithOpacity, - ), - ), - ), - ], + DialogFooter( + onCancel: () => Navigator.pop(context), + onConfirm: selectedFunction != null && + selectedValueNotifier.value != null + ? () { + final selectedFn = switchFunctions.firstWhere( + (f) => f.code == selectedFunction, + ); + final value = selectedValueNotifier.value; + final functionData = DeviceFunctionData( + entityId: selectedFn.deviceId, + function: selectedFn.code, + operationName: selectedFn.operationName, + value: value, + condition: selectedConditionNotifier.value, + valueDescription: selectedFn + is TwoGangCountdown1Function || + selectedFn is TwoGangCountdown2Function + ? '${value} sec' + : ((selectedFn as BaseSwitchFunction) + .getOperationalValues() + .firstWhere((v) => v.value == value) + .description), + ); + Navigator.pop( + context, {selectedFn.code: functionData}); + } + : null, + isConfirmEnabled: selectedFunction != null, ), ], ), @@ -261,77 +169,101 @@ class TwoGangSwitchHelper { ); } - /// Build countdown selector for switch functions dialog - static Widget _buildCountDownSelector( + static Widget _buildOperationalValuesList( BuildContext context, - StateSetter setState, - dynamic selectedValue, - String? selectedCondition, - List selectedConditions, - Function(dynamic) onValueSelected, - Function(String?) onConditionSelected, + BaseSwitchFunction function, + ValueNotifier valueNotifier, ) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildConditionToggle( - context, - setState, - selectedConditions, - onConditionSelected, - ), - const SizedBox(height: 20), - Text( - '${selectedValue.toString()} sec', - style: Theme.of(context).textTheme.headlineMedium, - ), - const SizedBox(height: 20), - Slider( - value: selectedValue.toDouble(), - min: 0, - max: 300, // 5 minutes in seconds - divisions: 300, - onChanged: (value) { - setState(() { - onValueSelected(value.toInt()); - }); + final values = function.getOperationalValues(); + return ValueListenableBuilder( + valueListenable: valueNotifier, + builder: (context, selectedValue, _) { + return ListView.builder( + itemCount: values.length, + itemBuilder: (context, index) { + final value = values[index]; + return ListTile( + leading: SvgPicture.asset( + value.icon, + width: 24, + height: 24, + ), + title: Text( + value.description, + style: context.textTheme.bodyMedium, + ), + trailing: Radio( + value: value.value, + groupValue: selectedValue, + onChanged: (newValue) { + valueNotifier.value = newValue; + }, + ), + ); }, - ), - ], + ); + }, ); } - /// Build condition toggle for AC functions dialog - static Widget _buildConditionToggle( + static Widget _buildCountDownSelector( BuildContext context, - StateSetter setState, - List selectedConditions, - Function(String?) onConditionSelected, + ValueNotifier valueNotifier, + ValueNotifier conditionNotifier, + ValueNotifier> conditionsNotifier, ) { - return ToggleButtons( - onPressed: (int index) { - setState(() { - for (int i = 0; i < selectedConditions.length; i++) { - selectedConditions[i] = i == index; - } - onConditionSelected(index == 0 - ? "<" - : index == 1 - ? "==" - : ">"); - }); + return ValueListenableBuilder( + valueListenable: valueNotifier, + builder: (context, value, _) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ValueListenableBuilder>( + valueListenable: conditionsNotifier, + builder: (context, selectedConditions, _) { + return ToggleButtons( + onPressed: (int index) { + final newConditions = List.filled(3, false); + newConditions[index] = true; + conditionsNotifier.value = newConditions; + conditionNotifier.value = index == 0 + ? "<" + : index == 1 + ? "==" + : ">"; + }, + 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: selectedConditions, + children: const [Text("<"), Text("="), Text(">")], + ); + }, + ), + const SizedBox(height: 20), + Text( + '${value ?? 0} sec', + style: Theme.of(context).textTheme.headlineMedium, + ), + const SizedBox(height: 20), + Slider( + value: (value ?? 0).toDouble(), + min: 0, + max: 300, + divisions: 300, + onChanged: (newValue) { + valueNotifier.value = newValue.toInt(); + }, + ), + ], + ); }, - 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: selectedConditions, - children: const [Text("<"), Text("="), Text(">")], ); } } diff --git a/lib/pages/routiens/view/create_new_routine_view.dart b/lib/pages/routiens/view/create_new_routine_view.dart index 99b4df71..c81e878f 100644 --- a/lib/pages/routiens/view/create_new_routine_view.dart +++ b/lib/pages/routiens/view/create_new_routine_view.dart @@ -12,82 +12,79 @@ class CreateNewRoutineView extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => RoutineBloc(), - child: Container( - alignment: Alignment.topLeft, - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const RoutineSearchAndButtons(), - const SizedBox(height: 20), - Flexible( - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: Card( - child: Container( - decoration: BoxDecoration( - color: ColorsManager.whiteColors, - borderRadius: BorderRadius.circular(15), - ), - child: const ConditionsRoutinesDevicesView()), - ), + return Container( + alignment: Alignment.topLeft, + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const RoutineSearchAndButtons(), + const SizedBox(height: 20), + Flexible( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: Card( + child: Container( + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(15), + ), + child: const ConditionsRoutinesDevicesView()), ), - const SizedBox( - width: 10, - ), - Expanded( - child: Column( - children: [ - /// IF Container - Expanded( - child: Card( - margin: EdgeInsets.zero, - child: Container( - decoration: const BoxDecoration( - color: ColorsManager.whiteColors, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(15), - topRight: Radius.circular(15), - ), + ), + const SizedBox( + width: 10, + ), + Expanded( + child: Column( + children: [ + /// IF Container + Expanded( + child: Card( + margin: EdgeInsets.zero, + child: Container( + decoration: const BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(15), + topRight: Radius.circular(15), ), - child: const IfContainer(), ), + child: const IfContainer(), ), ), - Container( - height: 2, - width: double.infinity, - color: ColorsManager.dialogBlueTitle, - ), + ), + Container( + height: 2, + width: double.infinity, + color: ColorsManager.dialogBlueTitle, + ), - /// THEN Container - Expanded( - child: Card( - margin: EdgeInsets.zero, - child: Container( - decoration: const BoxDecoration( - color: ColorsManager.boxColor, - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(15), - bottomRight: Radius.circular(15), - ), + /// THEN Container + Expanded( + child: Card( + margin: EdgeInsets.zero, + child: Container( + decoration: const BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(15), + bottomRight: Radius.circular(15), ), - child: const ThenContainer(), ), + child: const ThenContainer(), ), ), - ], - ), + ), + ], ), - ], - ), + ), + ], ), - ], - ), + ), + ], ), ); } diff --git a/lib/pages/routiens/view/routines_view.dart b/lib/pages/routiens/view/routines_view.dart index 5a19db08..b229dd62 100644 --- a/lib/pages/routiens/view/routines_view.dart +++ b/lib/pages/routiens/view/routines_view.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/view/create_new_routine_view.dart'; import 'package:syncrow_web/utils/color_manager.dart'; class RoutinesView extends StatelessWidget { @@ -8,54 +10,67 @@ class RoutinesView extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text("Create New Routines", - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - color: ColorsManager.grayColor, - )), - SizedBox( - height: 200, - width: 150, - child: GestureDetector( - onTap: () { - BlocProvider.of(context).add( - const CreateNewRoutineViewEvent(true), - ); - }, - child: Card( - elevation: 3, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - color: ColorsManager.whiteColors, - child: Center( - child: Container( - decoration: BoxDecoration( - color: ColorsManager.graysColor, - borderRadius: BorderRadius.circular(120), - border: Border.all(color: ColorsManager.greyColor, width: 2.0), - ), - height: 70, - width: 70, - child: Icon( - Icons.add, - color: ColorsManager.dialogBlueTitle, - size: 40, - ), - )), + return BlocBuilder( + builder: (context, state) { + if (state is ShowCreateRoutineState && state.showCreateRoutine) { + return const CreateNewRoutineView(); + } + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + "Create New Routines", + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: ColorsManager.grayColor, + ), ), - ), + SizedBox( + height: 200, + width: 150, + child: GestureDetector( + onTap: () { + BlocProvider.of(context).add( + const CreateNewRoutineViewEvent(true), + ); + }, + child: Card( + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + color: ColorsManager.whiteColors, + child: Center( + child: Container( + decoration: BoxDecoration( + color: ColorsManager.graysColor, + borderRadius: BorderRadius.circular(120), + border: Border.all( + color: ColorsManager.greyColor, + width: 2.0, + ), + ), + height: 70, + width: 70, + child: Icon( + Icons.add, + color: ColorsManager.dialogBlueTitle, + size: 40, + ), + ), + ), + ), + ), + ), + const Spacer(), + ], ), - const Spacer(), - ], - ), + ); + }, ); } } diff --git a/lib/pages/routiens/widgets/conditions_routines_devices_view.dart b/lib/pages/routiens/widgets/conditions_routines_devices_view.dart index 91100220..2728d329 100644 --- a/lib/pages/routiens/widgets/conditions_routines_devices_view.dart +++ b/lib/pages/routiens/widgets/conditions_routines_devices_view.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; import 'package:syncrow_web/pages/routiens/widgets/routine_devices.dart'; import 'package:syncrow_web/pages/routiens/widgets/routines_title_widget.dart'; @@ -11,87 +13,107 @@ class ConditionsRoutinesDevicesView extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: EdgeInsets.symmetric(horizontal: 8.0), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const ConditionTitleAndSearchBar(), - const SizedBox( - height: 10, - ), - const Wrap( - spacing: 10, - runSpacing: 10, + return BlocBuilder( + builder: (context, state) { + return const Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - DraggableCard( - imagePath: Assets.tabToRun, - title: 'Tab to run', + ConditionTitleAndSearchBar(), + SizedBox(height: 10), + Wrap( + spacing: 10, + runSpacing: 10, + children: [ + DraggableCard( + imagePath: Assets.tabToRun, + title: 'Tab to run', + deviceData: { + 'deviceId': 'tab_to_run', + 'type': 'trigger', + 'name': 'Tab to run', + }, + ), + DraggableCard( + imagePath: Assets.map, + title: 'Location', + deviceData: { + 'deviceId': 'location', + 'type': 'trigger', + 'name': 'Location', + }, + ), + DraggableCard( + imagePath: Assets.weather, + title: 'Weather', + deviceData: { + 'deviceId': 'weather', + 'type': 'trigger', + 'name': 'Weather', + }, + ), + DraggableCard( + imagePath: Assets.schedule, + title: 'Schedule', + deviceData: { + 'deviceId': 'schedule', + 'type': 'trigger', + 'name': 'Schedule', + }, + ), + ], ), - DraggableCard( - imagePath: Assets.map, - title: 'Location', + const SizedBox(height: 10), + const TitleRoutine( + title: 'Conditions', + subtitle: '(THEN)', ), - DraggableCard( - imagePath: Assets.weather, - title: 'Weather', + const SizedBox(height: 10), + const Wrap( + spacing: 10, + runSpacing: 10, + children: [ + DraggableCard( + imagePath: Assets.notification, + title: 'Send Notification', + deviceData: { + 'deviceId': 'notification', + 'type': 'action', + 'name': 'Send Notification', + }, + ), + DraggableCard( + imagePath: Assets.delay, + title: 'Delay the action', + deviceData: { + 'deviceId': 'delay', + 'type': 'action', + 'name': 'Delay the action', + }, + ), + ], ), - DraggableCard( - imagePath: Assets.schedule, - title: 'Schedule', + const SizedBox(height: 10), + const TitleRoutine( + title: 'Routines', + subtitle: '(THEN)', ), + const SizedBox(height: 10), + const ScenesAndAutomations(), + const SizedBox(height: 10), + const TitleRoutine( + title: 'Devices', + subtitle: '', + ), + const SizedBox(height: 10), + const RoutineDevices(), ], ), - const SizedBox( - height: 10, - ), - const TitleRoutine( - title: 'Conditions', - subtitle: '(THEN)', - ), - const SizedBox( - height: 10, - ), - const Wrap( - spacing: 10, - runSpacing: 10, - children: [ - DraggableCard( - imagePath: Assets.notification, - title: 'Send Notification', - ), - DraggableCard( - imagePath: Assets.delay, - title: 'Delay the action', - ), - ], - ), - const SizedBox( - height: 10, - ), - const TitleRoutine( - title: 'Routines', - subtitle: '(THEN)', - ), - const SizedBox( - height: 10, - ), - const ScenesAndAutomations(), - const SizedBox( - height: 10, - ), - const TitleRoutine( - title: 'Devices', - subtitle: '', - ), - const SizedBox( - height: 10, - ), - const RoutineDevices(), - ], - ), - ), + ), + ); + }, ); } } diff --git a/lib/pages/routiens/widgets/dialog_footer.dart b/lib/pages/routiens/widgets/dialog_footer.dart new file mode 100644 index 00000000..90c6baec --- /dev/null +++ b/lib/pages/routiens/widgets/dialog_footer.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class DialogFooter extends StatelessWidget { + final VoidCallback onCancel; + final VoidCallback? onConfirm; + final bool isConfirmEnabled; + + const DialogFooter({ + Key? key, + required this.onCancel, + required this.onConfirm, + required this.isConfirmEnabled, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + decoration: const BoxDecoration( + border: Border( + top: BorderSide( + color: ColorsManager.greyColor, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildFooterButton( + context, + 'Cancel', + onCancel, + width: isConfirmEnabled ? 299 : 179, + ), + if (isConfirmEnabled) + Row( + children: [ + Container(width: 1, height: 50, color: ColorsManager.greyColor), + _buildFooterButton( + context, + 'Confirm', + onConfirm, + width: 299, + ), + ], + ), + ], + ), + ); + } + + Widget _buildFooterButton( + BuildContext context, + String text, + VoidCallback? onTap, { + required double width, + }) { + return GestureDetector( + onTap: onTap, + child: SizedBox( + height: 50, + width: width, + child: Center( + child: Text( + text, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: text == 'Confirm' + ? ColorsManager.primaryColorWithOpacity + : ColorsManager.textGray, + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/routiens/widgets/dialog_header.dart b/lib/pages/routiens/widgets/dialog_header.dart new file mode 100644 index 00000000..6701b5b0 --- /dev/null +++ b/lib/pages/routiens/widgets/dialog_header.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class DialogHeader extends StatelessWidget { + final String title; + + const DialogHeader(this.title, {super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text( + title, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.primaryColorWithOpacity, + fontWeight: FontWeight.bold, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 50), + child: Container( + height: 1, + width: double.infinity, + color: ColorsManager.greyColor, + ), + ), + ], + ); + } +} diff --git a/lib/pages/routiens/widgets/dragable_card.dart b/lib/pages/routiens/widgets/dragable_card.dart index 7ee078ff..227e440b 100644 --- a/lib/pages/routiens/widgets/dragable_card.dart +++ b/lib/pages/routiens/widgets/dragable_card.dart @@ -2,156 +2,139 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class DraggableCard extends StatelessWidget { + final String imagePath; + final String title; + final Map deviceData; + const DraggableCard({ super.key, required this.imagePath, required this.title, - this.titleColor, - this.isDragged = false, - this.isDisabled = false, - this.deviceData, + required this.deviceData, }); - final String imagePath; - final String title; - final Color? titleColor; - final bool isDragged; - final bool isDisabled; - final Map? deviceData; - @override Widget build(BuildContext context) { - Widget card = Draggable>( - data: deviceData ?? - { - 'key': UniqueKey().toString(), - 'imagePath': imagePath, - 'title': title, - }, - feedback: Transform.rotate( - angle: -0.1, - child: _buildCardContent(context), - ), - childWhenDragging: _buildGreyContainer(), - child: _buildCardContent(context), - ); - - if (isDisabled) { - card = AbsorbPointer(child: card); - } - - return card; - } - - Widget _buildCardContent(BuildContext context) { return BlocBuilder( builder: (context, state) { - // Filter functions for this device final deviceFunctions = state.selectedFunctions - .where((f) => f.entityId == deviceData?['deviceId']) + .where((f) => f.entityId == deviceData['deviceId']) .toList(); - return Card( - color: ColorsManager.whiteColors, - child: SizedBox( - width: 90, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox( - height: 123, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - height: 50, - width: 50, - decoration: BoxDecoration( - color: ColorsManager.CircleImageBackground, - borderRadius: BorderRadius.circular(90), - border: Border.all( - color: ColorsManager.graysColor, - ), - ), - padding: const EdgeInsets.all(8), - child: imagePath.contains('.svg') - ? SvgPicture.asset( - imagePath, - ) - : Image.network(imagePath), + return Draggable>( + data: deviceData, + feedback: Transform.rotate( + angle: -0.1, + child: _buildCardContent(context, deviceFunctions), + ), + childWhenDragging: _buildGreyContainer(), + child: _buildCardContent(context, deviceFunctions), + ); + }, + ); + } + + Widget _buildCardContent( + BuildContext context, List deviceFunctions) { + return Card( + color: ColorsManager.whiteColors, + child: SizedBox( + width: 90, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + height: 123, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + height: 50, + width: 50, + decoration: BoxDecoration( + color: ColorsManager.CircleImageBackground, + borderRadius: BorderRadius.circular(90), + border: Border.all( + color: ColorsManager.graysColor, ), - const SizedBox(height: 8), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 3), + ), + padding: const EdgeInsets.all(8), + child: imagePath.contains('.svg') + ? SvgPicture.asset( + imagePath, + ) + : Image.network(imagePath), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 3), + child: Text( + title, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + maxLines: 2, + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.blackColor, + fontSize: 12, + ), + ), + ), + ], + ), + ), + if (deviceFunctions.isNotEmpty) ...[ + const Divider(height: 1), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 4), + itemCount: deviceFunctions.length, + itemBuilder: (context, index) { + final function = deviceFunctions[index]; + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( child: Text( - title, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - maxLines: 2, + '${function.operationName}: ${function.valueDescription}', style: context.textTheme.bodySmall?.copyWith( - color: titleColor ?? ColorsManager.blackColor, - fontSize: 12, + fontSize: 9, + color: ColorsManager.textGray, + height: 1.2, + ), + overflow: TextOverflow.ellipsis, + ), + ), + InkWell( + onTap: () { + context.read().add( + RemoveFunction(function), + ); + }, + child: const Padding( + padding: EdgeInsets.all(2), + child: Icon( + Icons.close, + size: 12, + color: ColorsManager.textGray, ), ), ), ], - ), - ), - if (deviceFunctions.isNotEmpty) ...[ - const Divider(height: 1), - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - padding: - const EdgeInsets.symmetric(vertical: 4, horizontal: 4), - itemCount: deviceFunctions.length, - itemBuilder: (context, index) { - final function = deviceFunctions[index]; - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: Text( - '${function.operationName}: ${function.valueDescription}', - style: context.textTheme.bodySmall?.copyWith( - fontSize: 9, - color: ColorsManager.textGray, - height: 1.2, - ), - overflow: TextOverflow.ellipsis, - ), - ), - InkWell( - onTap: () { - context.read().add( - RemoveFunction(function), - ); - }, - child: const Padding( - padding: EdgeInsets.all(2), - child: Icon( - Icons.close, - size: 12, - color: ColorsManager.textGray, - ), - ), - ), - ], - ); - }, - ), - ], - ], - ), - ), - ); - }, + ); + }, + ), + ], + ], + ), + ), ); } diff --git a/lib/pages/routiens/widgets/if_container.dart b/lib/pages/routiens/widgets/if_container.dart index ba0d67ab..af1394c6 100644 --- a/lib/pages/routiens/widgets/if_container.dart +++ b/lib/pages/routiens/widgets/if_container.dart @@ -31,6 +31,7 @@ class IfContainer extends StatelessWidget { key: Key(item['key']!), imagePath: item['imagePath']!, title: item['title']!, + deviceData: item, )) .toList(), ), diff --git a/lib/pages/routiens/widgets/routine_devices.dart b/lib/pages/routiens/widgets/routine_devices.dart index c845af19..7d97de18 100644 --- a/lib/pages/routiens/widgets/routine_devices.dart +++ b/lib/pages/routiens/widgets/routine_devices.dart @@ -1,20 +1,19 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; class RoutineDevices extends StatelessWidget { - const RoutineDevices({ - super.key, - }); + const RoutineDevices({super.key}); @override Widget build(BuildContext context) { + // Get the RoutineBloc instance from the parent + final routineBloc = context.read(); + return BlocProvider( - create: (context) => DeviceManagementBloc() - ..add( - FetchDevices(), - ), + create: (context) => DeviceManagementBloc()..add(FetchDevices()), child: BlocBuilder( builder: (context, state) { if (state is DeviceManagementLoaded) { @@ -25,24 +24,34 @@ class RoutineDevices extends StatelessWidget { device.productType == '2G' || device.productType == '3G') .toList(); - return Wrap( - spacing: 10, - runSpacing: 10, - children: deviceList.asMap().entries.map((entry) { - final device = entry.value; - return DraggableCard( - imagePath: device.getDefaultIcon(device.productType), - title: device.name ?? '', - deviceData: { - 'key': UniqueKey().toString(), - 'imagePath': device.getDefaultIcon(device.productType), - 'title': device.name ?? '', - 'deviceId': device.uuid, - 'productType': device.productType, - 'functions': device.functions, - }, - ); - }).toList(), + + // Provide the RoutineBloc to the child widgets + return BlocProvider.value( + value: routineBloc, + child: BlocBuilder( + builder: (context, routineState) { + return Wrap( + spacing: 10, + runSpacing: 10, + children: deviceList.asMap().entries.map((entry) { + final device = entry.value; + return DraggableCard( + imagePath: device.getDefaultIcon(device.productType), + title: device.name ?? '', + deviceData: { + 'key': UniqueKey().toString(), + 'imagePath': + device.getDefaultIcon(device.productType), + 'title': device.name ?? '', + 'deviceId': device.uuid, + 'productType': device.productType, + 'functions': device.functions, + }, + ); + }).toList(), + ); + }, + ), ); } return const Center(child: CircularProgressIndicator()); diff --git a/lib/pages/routiens/widgets/scenes_and_automations.dart b/lib/pages/routiens/widgets/scenes_and_automations.dart index b455b2f5..ef40b605 100644 --- a/lib/pages/routiens/widgets/scenes_and_automations.dart +++ b/lib/pages/routiens/widgets/scenes_and_automations.dart @@ -4,40 +4,51 @@ import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; -class ScenesAndAutomations extends StatelessWidget { +class ScenesAndAutomations extends StatefulWidget { const ScenesAndAutomations({ super.key, }); + @override + State createState() => _ScenesAndAutomationsState(); +} + +class _ScenesAndAutomationsState extends State { + @override + void initState() { + super.initState(); + context.read() + ..add(const LoadScenes(spaceId)) + ..add(const LoadAutomation(spaceId)); + } + @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => RoutineBloc() - ..add( - const LoadScenes(spaceId), - ) - ..add( - const LoadAutomation(spaceId), - ), - child: BlocBuilder( - builder: (context, state) { - if (state.scenes.isNotEmpty || state.automations.isNotEmpty) { - var scenes = [...state.scenes, ...state.automations]; - return Wrap( - spacing: 10, - runSpacing: 10, - children: scenes.asMap().entries.map((entry) { - final scene = entry.value; - return DraggableCard( - imagePath: Assets.logo, - title: scene.name, - ); - }).toList(), - ); - } - return const Center(child: CircularProgressIndicator()); - }, - ), + return BlocBuilder( + builder: (context, state) { + if (state.scenes.isNotEmpty || state.automations.isNotEmpty) { + var scenes = [...state.scenes, ...state.automations]; + return Wrap( + spacing: 10, + runSpacing: 10, + children: scenes.asMap().entries.map((entry) { + final scene = entry.value; + return DraggableCard( + imagePath: Assets.logo, + title: scene.name, + deviceData: { + 'deviceId': scene.id, + 'name': scene.name, + 'status': scene.status, + 'type': scene.type, + 'icon': scene.icon, + }, + ); + }).toList(), + ); + } + return const Center(child: CircularProgressIndicator()); + }, ); } } diff --git a/lib/pages/routiens/widgets/then_container.dart b/lib/pages/routiens/widgets/then_container.dart index 9bd7fd00..385ec4cb 100644 --- a/lib/pages/routiens/widgets/then_container.dart +++ b/lib/pages/routiens/widgets/then_container.dart @@ -33,6 +33,7 @@ class ThenContainer extends StatelessWidget { key: Key(item['key']!), imagePath: item['imagePath']!, title: item['title']!, + deviceData: item, )) .toList(), ), From a52604360fd656dfb249ae9cadb4902665b7e165 Mon Sep 17 00:00:00 2001 From: ashrafzarkanisala Date: Fri, 22 Nov 2024 20:07:41 +0300 Subject: [PATCH 09/23] dispose value notfiers --- .../helper/one_gang_switch_helper.dart | 12 +- .../helper/three_gang_switch_helper.dart | 445 ++++++++---------- .../helper/two_gang_switch_helper.dart | 10 +- .../view/create_new_routine_view.dart | 2 - lib/pages/routiens/view/routines_view.dart | 1 - 5 files changed, 208 insertions(+), 262 deletions(-) diff --git a/lib/pages/routiens/helper/one_gang_switch_helper.dart b/lib/pages/routiens/helper/one_gang_switch_helper.dart index 0837829b..014e41f9 100644 --- a/lib/pages/routiens/helper/one_gang_switch_helper.dart +++ b/lib/pages/routiens/helper/one_gang_switch_helper.dart @@ -151,9 +151,7 @@ class OneGangSwitchHelper { context, {selectedFn.code: functionData}); } : null, - isConfirmEnabled: - selectedFunctionNotifier.value != null && - selectedValueNotifier.value != null, + isConfirmEnabled: selectedFunctionNotifier.value != null, ), ], ), @@ -162,7 +160,13 @@ class OneGangSwitchHelper { }, ); }, - ); + ).then((value) { + selectedFunctionNotifier.dispose(); + selectedValueNotifier.dispose(); + selectedConditionNotifier.dispose(); + selectedConditionsNotifier.dispose(); + return value; + }); } static Widget _buildCountDownSelector( diff --git a/lib/pages/routiens/helper/three_gang_switch_helper.dart b/lib/pages/routiens/helper/three_gang_switch_helper.dart index 56d1f3bb..1e62f5c4 100644 --- a/lib/pages/routiens/helper/three_gang_switch_helper.dart +++ b/lib/pages/routiens/helper/three_gang_switch_helper.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart'; import 'package:syncrow_web/pages/routiens/models/gang_switches/three_gang_switch/three_gang_switch.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; @@ -20,20 +20,23 @@ class ThreeGangSwitchHelper { f is ThreeGangCountdown2Function || f is ThreeGangCountdown3Function) .toList(); - Map selectedValues = {}; - List selectedFunctions = []; - String? selectedCondition = "<"; - List selectedConditions = [true, false, false]; + + final selectedFunctionNotifier = ValueNotifier(null); + final selectedValueNotifier = ValueNotifier(null); + final selectedConditionNotifier = ValueNotifier('<'); + final selectedConditionsNotifier = + ValueNotifier>([true, false, false]); await showDialog( context: context, builder: (BuildContext context) { - return StatefulBuilder( - builder: (context, setState) { + return ValueListenableBuilder( + valueListenable: selectedFunctionNotifier, + builder: (context, selectedFunction, _) { return AlertDialog( contentPadding: EdgeInsets.zero, content: Container( - width: 600, + width: selectedFunction != null ? 600 : 300, height: 450, decoration: BoxDecoration( color: Colors.white, @@ -43,22 +46,7 @@ class ThreeGangSwitchHelper { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text( - '3 Gangs Light Switch Condition', - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: ColorsManager.primaryColorWithOpacity, - fontWeight: FontWeight.bold, - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 15, horizontal: 50), - child: Container( - height: 1, - width: double.infinity, - color: ColorsManager.greyColor, - ), - ), + const DialogHeader('3 Gangs Light Switch Condition'), Expanded( child: Row( children: [ @@ -71,188 +59,111 @@ class ThreeGangSwitchHelper { ), itemBuilder: (context, index) { final function = switchFunctions[index]; - final isSelected = - selectedValues.containsKey(function.code); - return ListTile( - tileColor: - isSelected ? Colors.grey.shade100 : null, - leading: SvgPicture.asset( - function.icon, - width: 24, - height: 24, - ), - title: Text( - function.operationName, - style: context.textTheme.bodyMedium, - ), - trailing: isSelected - ? Icon( - Icons.check_circle, - color: ColorsManager - .primaryColorWithOpacity, - size: 20, - ) - : const Icon( - Icons.arrow_forward_ios, - size: 16, - color: ColorsManager.textGray, - ), - onTap: () { - if (isSelected) { - selectedValues.remove(function.code); - selectedFunctions.removeWhere( - (f) => f.function == function.code); - } - (context as Element).markNeedsBuild(); - }, - ); - }, - ), - ), - // Right side: Value selector - Expanded( - child: Builder( - builder: (context) { - final selectedFn = switchFunctions.firstWhere( - (f) => selectedValues.containsKey(f.code), - orElse: () => switchFunctions.first, - ) as BaseSwitchFunction; - - if (selectedFn is ThreeGangCountdown1Function || - selectedFn is ThreeGangCountdown2Function || - selectedFn is ThreeGangCountdown3Function) { - return _buildCountDownSelector( - context, - setState, - selectedValues[selectedFn.code] ?? 0, - selectedCondition, - selectedConditions, - (value) { - selectedValues[selectedFn.code] = value; - final functionData = DeviceFunctionData( - entityId: selectedFn.deviceId, - function: selectedFn.code, - operationName: selectedFn.operationName, - value: value, - condition: selectedCondition, - valueDescription: '${value} sec', - ); - - final existingIndex = - selectedFunctions.indexWhere((f) => - f.function == selectedFn.code); - if (existingIndex != -1) { - selectedFunctions[existingIndex] = - functionData; - } else { - selectedFunctions.add(functionData); - } - (context as Element).markNeedsBuild(); - }, - (condition) { - setState(() { - selectedCondition = condition; - }); - }, - ); - } - - final values = - selectedFn.getOperationalValues(); - return ListView.builder( - itemCount: values.length, - itemBuilder: (context, index) { - final value = values[index]; + return ValueListenableBuilder( + valueListenable: selectedFunctionNotifier, + builder: (context, selectedFunction, _) { + final isSelected = + selectedFunction == function.code; return ListTile( + tileColor: isSelected + ? Colors.grey.shade100 + : null, leading: SvgPicture.asset( - value.icon, + function.icon, width: 24, height: 24, ), title: Text( - value.description, + function.operationName, style: context.textTheme.bodyMedium, ), - trailing: Radio( - value: value.value, - groupValue: - selectedValues[selectedFn.code], - onChanged: (newValue) { - selectedValues[selectedFn.code] = - newValue; - final functionData = - DeviceFunctionData( - entityId: selectedFn.deviceId, - function: selectedFn.code, - operationName: - selectedFn.operationName, - value: newValue, - valueDescription: value.description, - ); - - final existingIndex = - selectedFunctions.indexWhere( - (f) => - f.function == - selectedFn.code); - if (existingIndex != -1) { - selectedFunctions[existingIndex] = - functionData; - } else { - selectedFunctions.add(functionData); - } - (context as Element).markNeedsBuild(); - }, + trailing: const Icon( + Icons.arrow_forward_ios, + size: 16, + color: ColorsManager.textGray, ), + onTap: () { + selectedFunctionNotifier.value = + function.code; + selectedValueNotifier.value = function + is ThreeGangCountdown1Function || + function + is ThreeGangCountdown2Function || + function + is ThreeGangCountdown3Function + ? 0 + : null; + }, ); }, ); }, ), ), + // Right side: Value selector + if (selectedFunction != null) + Expanded( + child: ValueListenableBuilder( + valueListenable: selectedValueNotifier, + builder: (context, selectedValue, _) { + final selectedFn = switchFunctions.firstWhere( + (f) => f.code == selectedFunction, + ); + return selectedFn + is ThreeGangCountdown1Function || + selectedFn + is ThreeGangCountdown2Function || + selectedFn + is ThreeGangCountdown3Function + ? _buildCountDownSelector( + context, + selectedValueNotifier, + selectedConditionNotifier, + selectedConditionsNotifier, + ) + : _buildOperationalValuesList( + context, + selectedFn as BaseSwitchFunction, + selectedValueNotifier, + ); + }, + ), + ), ], ), ), - Container( - height: 1, - width: double.infinity, - color: ColorsManager.greyColor, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text( - 'Cancel', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: ColorsManager.greyColor), - ), - ), - TextButton( - onPressed: selectedFunctions.isNotEmpty - ? () { - for (final function in selectedFunctions) { - context - .read() - .add(AddFunction(function)); - } - Navigator.pop(context, true); - } - : null, - child: Text( - 'Confirm', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith( - color: ColorsManager.primaryColorWithOpacity, - ), - ), - ), - ], + DialogFooter( + onCancel: () => Navigator.pop(context), + onConfirm: selectedFunctionNotifier.value != null && + selectedValueNotifier.value != null + ? () { + final selectedFn = switchFunctions.firstWhere( + (f) => f.code == selectedFunctionNotifier.value, + ); + final value = selectedValueNotifier.value; + final functionData = DeviceFunctionData( + entityId: selectedFn.deviceId, + function: selectedFn.code, + operationName: selectedFn.operationName, + value: value, + condition: selectedConditionNotifier.value, + valueDescription: selectedFn + is ThreeGangCountdown1Function || + selectedFn + is ThreeGangCountdown2Function || + selectedFn + is ThreeGangCountdown3Function + ? '${value} sec' + : ((selectedFn as BaseSwitchFunction) + .getOperationalValues() + .firstWhere((v) => v.value == value) + .description), + ); + Navigator.pop( + context, {selectedFn.code: functionData}); + } + : null, + isConfirmEnabled: selectedFunctionNotifier.value != null, ), ], ), @@ -261,80 +172,110 @@ class ThreeGangSwitchHelper { }, ); }, - ); + ).then((value) { + selectedFunctionNotifier.dispose(); + selectedValueNotifier.dispose(); + selectedConditionNotifier.dispose(); + selectedConditionsNotifier.dispose(); + return value; + }); } - /// Build countdown selector for switch functions dialog static Widget _buildCountDownSelector( BuildContext context, - StateSetter setState, - dynamic selectedValue, - String? selectedCondition, - List selectedConditions, - Function(dynamic) onValueSelected, - Function(String?) onConditionSelected, + ValueNotifier valueNotifier, + ValueNotifier conditionNotifier, + ValueNotifier> conditionsNotifier, ) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildConditionToggle( - context, - setState, - selectedConditions, - onConditionSelected, - ), - const SizedBox(height: 20), - Text( - '${selectedValue.toString()} sec', - style: Theme.of(context).textTheme.headlineMedium, - ), - const SizedBox(height: 20), - Slider( - value: selectedValue.toDouble(), - min: 0, - max: 300, // 5 minutes in seconds - divisions: 300, - onChanged: (value) { - setState(() { - onValueSelected(value.toInt()); - }); - }, - ), - ], + return ValueListenableBuilder( + valueListenable: valueNotifier, + builder: (context, value, _) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ValueListenableBuilder>( + valueListenable: conditionsNotifier, + builder: (context, selectedConditions, _) { + return ToggleButtons( + onPressed: (int index) { + final newConditions = List.filled(3, false); + newConditions[index] = true; + conditionsNotifier.value = newConditions; + conditionNotifier.value = index == 0 + ? "<" + : index == 1 + ? "==" + : ">"; + }, + 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: selectedConditions, + children: const [Text("<"), Text("="), Text(">")], + ); + }, + ), + const SizedBox(height: 20), + Text( + '${value ?? 0} sec', + style: Theme.of(context).textTheme.headlineMedium, + ), + const SizedBox(height: 20), + Slider( + value: (value ?? 0).toDouble(), + min: 0, + max: 300, + divisions: 300, + onChanged: (newValue) { + valueNotifier.value = newValue.toInt(); + }, + ), + ], + ); + }, ); } - /// Build condition toggle for AC functions dialog - static Widget _buildConditionToggle( + static Widget _buildOperationalValuesList( BuildContext context, - StateSetter setState, - List selectedConditions, - Function(String?) onConditionSelected, + BaseSwitchFunction function, + ValueNotifier valueNotifier, ) { - return ToggleButtons( - onPressed: (int index) { - setState(() { - for (int i = 0; i < selectedConditions.length; i++) { - selectedConditions[i] = i == index; - } - onConditionSelected(index == 0 - ? "<" - : index == 1 - ? "==" - : ">"); - }); + final values = function.getOperationalValues(); + return ValueListenableBuilder( + valueListenable: valueNotifier, + builder: (context, selectedValue, _) { + return ListView.builder( + itemCount: values.length, + itemBuilder: (context, index) { + final value = values[index]; + return ListTile( + leading: SvgPicture.asset( + value.icon, + width: 24, + height: 24, + ), + title: Text( + value.description, + style: context.textTheme.bodyMedium, + ), + trailing: Radio( + value: value.value, + groupValue: selectedValue, + onChanged: (newValue) { + valueNotifier.value = newValue; + }, + ), + ); + }, + ); }, - 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: selectedConditions, - children: const [Text("<"), Text("="), Text(">")], ); } } diff --git a/lib/pages/routiens/helper/two_gang_switch_helper.dart b/lib/pages/routiens/helper/two_gang_switch_helper.dart index ae6abac4..8a7e5580 100644 --- a/lib/pages/routiens/helper/two_gang_switch_helper.dart +++ b/lib/pages/routiens/helper/two_gang_switch_helper.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart'; import 'package:syncrow_web/pages/routiens/models/gang_switches/two_gang_switch/two_gang_switch.dart'; @@ -166,7 +164,13 @@ class TwoGangSwitchHelper { }, ); }, - ); + ).then((value) { + selectedFunctionNotifier.dispose(); + selectedValueNotifier.dispose(); + selectedConditionNotifier.dispose(); + selectedConditionsNotifier.dispose(); + return value; + }); } static Widget _buildOperationalValuesList( diff --git a/lib/pages/routiens/view/create_new_routine_view.dart b/lib/pages/routiens/view/create_new_routine_view.dart index c81e878f..38750991 100644 --- a/lib/pages/routiens/view/create_new_routine_view.dart +++ b/lib/pages/routiens/view/create_new_routine_view.dart @@ -1,6 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/widgets/conditions_routines_devices_view.dart'; import 'package:syncrow_web/pages/routiens/widgets/if_container.dart'; import 'package:syncrow_web/pages/routiens/widgets/routine_search_and_buttons.dart'; diff --git a/lib/pages/routiens/view/routines_view.dart b/lib/pages/routiens/view/routines_view.dart index b229dd62..27690a9c 100644 --- a/lib/pages/routiens/view/routines_view.dart +++ b/lib/pages/routiens/view/routines_view.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_bloc.dart'; -import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/view/create_new_routine_view.dart'; import 'package:syncrow_web/utils/color_manager.dart'; From 7e2f6054660996e44108066d07248176a7d29dd4 Mon Sep 17 00:00:00 2001 From: ashrafzarkanisala Date: Fri, 22 Nov 2024 21:38:39 +0300 Subject: [PATCH 10/23] push design changes --- lib/pages/routiens/helper/ac_helper.dart | 446 ++++++++++++----------- 1 file changed, 228 insertions(+), 218 deletions(-) diff --git a/lib/pages/routiens/helper/ac_helper.dart b/lib/pages/routiens/helper/ac_helper.dart index 9ba0b224..d940f4e4 100644 --- a/lib/pages/routiens/helper/ac_helper.dart +++ b/lib/pages/routiens/helper/ac_helper.dart @@ -13,210 +13,203 @@ class ACHelper { List> functions, ) async { List acFunctions = functions.whereType().toList(); - String? selectedFunction; - dynamic selectedValue = 20; - String? selectedCondition = "=="; - List _selectedConditions = [false, true, false]; + final selectedFunctionNotifier = ValueNotifier(null); + final selectedValueNotifier = ValueNotifier(null); + final selectedConditionNotifier = ValueNotifier('=='); + final selectedConditionsNotifier = + ValueNotifier>([false, true, false]); return showDialog?>( context: context, builder: (BuildContext context) { - return StatefulBuilder( - builder: (context, setState) { + return ValueListenableBuilder( + valueListenable: selectedFunctionNotifier, + builder: (context, selectedFunction, _) { return AlertDialog( contentPadding: EdgeInsets.zero, - content: _buildDialogContent( - context, - setState, - acFunctions, - selectedFunction, - selectedValue, - selectedCondition, - _selectedConditions, - (fn) => selectedFunction = fn, - (val) => selectedValue = val, - (cond) => selectedCondition = cond, + content: 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('AC Functions'), + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Function list + SizedBox( + width: selectedFunction != null ? 320 : 360, + child: _buildFunctionsList( + context, + acFunctions, + selectedFunctionNotifier, + ), + ), + // Value selector + if (selectedFunction != null) + Expanded( + child: _buildValueSelector( + context, + selectedFunction, + selectedValueNotifier, + selectedConditionNotifier, + selectedConditionsNotifier, + acFunctions, + ), + ), + ], + ), + ), + DialogFooter( + onCancel: () { + selectedFunctionNotifier.dispose(); + selectedValueNotifier.dispose(); + selectedConditionNotifier.dispose(); + selectedConditionsNotifier.dispose(); + Navigator.pop(context); + }, + onConfirm: selectedFunctionNotifier.value != null && + selectedValueNotifier.value != null + ? () { + selectedFunctionNotifier.dispose(); + selectedValueNotifier.dispose(); + selectedConditionNotifier.dispose(); + selectedConditionsNotifier.dispose(); + Navigator.pop(context, { + 'function': selectedFunctionNotifier.value, + 'value': selectedValueNotifier.value, + 'condition': + selectedConditionNotifier.value ?? "==", + }); + } + : null, + isConfirmEnabled: selectedFunction != null, + ), + ], + ), ), ); }, ); }, - ); - } - - /// Build dialog content for AC functions dialog - static Widget _buildDialogContent( - BuildContext context, - StateSetter setState, - List acFunctions, - String? selectedFunction, - dynamic selectedValue, - String? selectedCondition, - List selectedConditions, - Function(String?) onFunctionSelected, - Function(dynamic) onValueSelected, - Function(String?) onConditionSelected, - ) { - 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('AC Functions'), - Flexible( - child: Row( - children: [ - _buildFunctionsList( - context, - setState, - acFunctions, - selectedFunction, - onFunctionSelected, - ), - if (selectedFunction != null) - _buildValueSelector( - context, - setState, - selectedFunction, - selectedValue, - selectedCondition, - selectedConditions, - onValueSelected, - onConditionSelected, - acFunctions, - ), - ], - ), - ), - DialogFooter( - onCancel: () => Navigator.pop(context), - onConfirm: selectedFunction != null && selectedValue != null - ? () => Navigator.pop(context, { - 'function': selectedFunction, - 'value': selectedValue, - 'condition': selectedCondition ?? "==", - }) - : null, - isConfirmEnabled: selectedFunction != null && selectedValue != null, - ), - ], - ), - ); + ).then((value) { + selectedFunctionNotifier.dispose(); + selectedValueNotifier.dispose(); + selectedConditionNotifier.dispose(); + selectedConditionsNotifier.dispose(); + return value; + }); } /// Build functions list for AC functions dialog static Widget _buildFunctionsList( BuildContext context, - StateSetter setState, List acFunctions, - String? selectedFunction, - Function(String?) onFunctionSelected, + ValueNotifier selectedFunctionNotifier, ) { - return Expanded( - child: ListView.separated( - shrinkWrap: false, - physics: const AlwaysScrollableScrollPhysics(), - itemCount: acFunctions.length, - separatorBuilder: (context, index) => const Divider( + return ListView.separated( + shrinkWrap: false, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: acFunctions.length, + separatorBuilder: (context, index) => const Padding( + padding: EdgeInsets.symmetric(horizontal: 40.0), + child: Divider( color: ColorsManager.dividerColor, ), - itemBuilder: (context, index) { - final function = acFunctions[index]; - return ListTile( - leading: SvgPicture.asset( - function.icon, + ), + itemBuilder: (context, index) { + final function = acFunctions[index]; + return ListTile( + leading: SvgPicture.asset( + function.icon, + width: 24, + height: 24, + placeholderBuilder: (BuildContext context) => Container( width: 24, height: 24, + color: Colors.transparent, ), - title: Text( - function.operationName, - style: context.textTheme.bodyMedium, - ), - trailing: const Icon( - Icons.arrow_forward_ios, - size: 16, - color: ColorsManager.textGray, - ), - onTap: () => setState(() => onFunctionSelected(function.code)), - ); - }, - ), + ), + title: Text( + function.operationName, + style: context.textTheme.bodyMedium, + ), + trailing: const Icon( + Icons.arrow_forward_ios, + size: 16, + color: ColorsManager.textGray, + ), + onTap: () => selectedFunctionNotifier.value = function.code, + ); + }, ); } /// Build value selector for AC functions dialog static Widget _buildValueSelector( BuildContext context, - StateSetter setState, String selectedFunction, - dynamic selectedValue, - String? selectedCondition, - List selectedConditions, - Function(dynamic) onValueSelected, - Function(String?) onConditionSelected, + ValueNotifier selectedValueNotifier, + ValueNotifier selectedConditionNotifier, + ValueNotifier> selectedConditionsNotifier, List acFunctions, ) { + // Handle temperature functions if (selectedFunction == 'temp_set' || selectedFunction == 'temp_current') { - return Expanded( - child: _buildTemperatureSelector( - context, - setState, - selectedValue, - selectedCondition, - selectedConditions, - onValueSelected, - onConditionSelected, - ), + // Initialize with 20°C (200 in internal representation) + if (selectedValueNotifier.value == null || + selectedValueNotifier.value is! int) { + selectedValueNotifier.value = 200; + } + return _buildTemperatureSelector( + context, + selectedValueNotifier, + selectedConditionNotifier, + selectedConditionsNotifier, ); } + // Handle other functions final selectedFn = acFunctions.firstWhere((f) => f.code == selectedFunction); final values = selectedFn.getOperationalValues(); - return Expanded( - child: _buildOperationalValuesList( - context, - setState, - values, - selectedValue, - onValueSelected, - ), + + // Don't set any default value for non-temperature functions + return _buildOperationalValuesList( + context, + values, + selectedValueNotifier, ); } /// Build temperature selector for AC functions dialog static Widget _buildTemperatureSelector( BuildContext context, - StateSetter setState, - dynamic selectedValue, - String? selectedCondition, - List selectedConditions, - Function(dynamic) onValueSelected, - Function(String?) onConditionSelected, + ValueNotifier selectedValueNotifier, + ValueNotifier selectedConditionNotifier, + ValueNotifier> selectedConditionsNotifier, ) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildConditionToggle( context, - setState, - selectedConditions, - onConditionSelected, + selectedConditionNotifier, + selectedConditionsNotifier, ), const SizedBox(height: 20), - _buildTemperatureDisplay(context, selectedValue), + _buildTemperatureDisplay(context, selectedValueNotifier), const SizedBox(height: 20), _buildTemperatureSlider( context, - setState, - selectedValue, - onValueSelected, + selectedValueNotifier, ), ], ); @@ -225,104 +218,121 @@ class ACHelper { /// Build condition toggle for AC functions dialog static Widget _buildConditionToggle( BuildContext context, - StateSetter setState, - List selectedConditions, - Function(String?) onConditionSelected, + ValueNotifier selectedConditionNotifier, + ValueNotifier> selectedConditionsNotifier, ) { - return ToggleButtons( - onPressed: (int index) { - setState(() { - for (int i = 0; i < selectedConditions.length; i++) { - selectedConditions[i] = i == index; - } - onConditionSelected(index == 0 - ? "<" - : index == 1 - ? "==" - : ">"); - }); + return ValueListenableBuilder>( + valueListenable: selectedConditionsNotifier, + builder: (context, selectedConditions, _) { + return ToggleButtons( + onPressed: (int index) { + final newConditions = [false, false, false]; + newConditions[index] = true; + selectedConditionsNotifier.value = newConditions; + selectedConditionNotifier.value = index == 0 + ? "<" + : index == 1 + ? "==" + : ">"; + }, + 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: selectedConditions, + children: const [Text("<"), Text("="), Text(">")], + ); }, - 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: selectedConditions, - children: const [Text("<"), Text("="), Text(">")], ); } /// Build temperature display for AC functions dialog static Widget _buildTemperatureDisplay( - BuildContext context, dynamic selectedValue) { + BuildContext context, ValueNotifier selectedValueNotifier) { return Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), decoration: BoxDecoration( color: ColorsManager.primaryColorWithOpacity.withOpacity(0.1), borderRadius: BorderRadius.circular(10), ), - child: Text( - '${selectedValue ?? 20}°C', - style: context.textTheme.headlineMedium!.copyWith( - color: ColorsManager.primaryColorWithOpacity, - ), + child: ValueListenableBuilder( + valueListenable: selectedValueNotifier, + builder: (context, selectedValue, child) { + return Text( + '${(selectedValue ?? 200) / 10}°C', + style: context.textTheme.headlineMedium!.copyWith( + color: ColorsManager.primaryColorWithOpacity, + ), + ); + }, ), ); } static Widget _buildTemperatureSlider( BuildContext context, - StateSetter setState, - dynamic selectedValue, - Function(dynamic) onValueSelected, + ValueNotifier selectedValueNotifier, ) { - final currentValue = selectedValue is int ? selectedValue.toDouble() : 20.0; - return Slider( - value: currentValue, - min: 16, - max: 30, - divisions: 14, - label: '${currentValue.toInt()}°C', - onChanged: (value) { - setState(() => onValueSelected(value.toInt())); + return ValueListenableBuilder( + valueListenable: selectedValueNotifier, + builder: (context, selectedValue, child) { + final currentValue = + selectedValue is int ? selectedValue.toDouble() : 200.0; + return Slider( + value: currentValue, + min: 160, + max: 300, + divisions: 14, + label: '${(currentValue / 10).toInt()}°C', + onChanged: (value) => selectedValueNotifier.value = value.toInt(), + ); }, ); } static Widget _buildOperationalValuesList( BuildContext context, - StateSetter setState, List values, - dynamic selectedValue, - Function(dynamic) onValueSelected, + ValueNotifier selectedValueNotifier, ) { - return ListView.builder( - shrinkWrap: false, - physics: const AlwaysScrollableScrollPhysics(), - itemCount: values.length, - itemBuilder: (context, index) { - final value = values[index]; - return ListTile( - leading: SvgPicture.asset( - value.icon, - width: 24, - height: 24, - ), - title: Text( - value.description, - style: context.textTheme.bodyMedium, - ), - trailing: Radio( - value: value.value, - groupValue: selectedValue, - onChanged: (newValue) { - setState(() => onValueSelected(newValue)); - }, - ), + return ValueListenableBuilder( + valueListenable: selectedValueNotifier, + builder: (context, selectedValue, _) { + return ListView.builder( + shrinkWrap: false, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: values.length, + itemBuilder: (context, index) { + final value = values[index]; + return ListTile( + leading: SvgPicture.asset( + value.icon, + width: 24, + height: 24, + placeholderBuilder: (BuildContext context) => Container( + width: 24, + height: 24, + color: Colors.transparent, + ), + ), + title: Text( + value.description, + style: context.textTheme.bodyMedium, + ), + trailing: Radio( + value: value.value, + groupValue: selectedValue, + onChanged: (_) => selectedValueNotifier.value = value.value, + activeColor: ColorsManager.primaryColorWithOpacity, + ), + onTap: () => selectedValueNotifier.value = value.value, + ); + }, ); }, ); From 53eb18c075a4d24e5ca23edda3dadb8ef46a44fb Mon Sep 17 00:00:00 2001 From: ashrafzarkanisala Date: Sat, 23 Nov 2024 00:54:52 +0300 Subject: [PATCH 11/23] push ac function state selection --- lib/main.dart | 2 +- .../functions_bloc/functions_bloc_bloc.dart | 148 +++++++ .../functions_bloc/functions_bloc_event.dart | 72 ++++ .../functions_bloc/functions_bloc_state.dart | 24 ++ .../bloc/{ => routine_bloc}/routine_bloc.dart | 0 .../{ => routine_bloc}/routine_event.dart | 0 .../{ => routine_bloc}/routine_state.dart | 0 lib/pages/routiens/helper/ac_helper.dart | 388 +++++++++--------- .../routiens/models/device_functions.dart | 25 ++ .../conditions_routines_devices_view.dart | 2 +- lib/pages/routiens/widgets/dragable_card.dart | 2 +- lib/pages/routiens/widgets/if_container.dart | 2 +- .../routiens/widgets/routine_devices.dart | 2 +- .../widgets/scenes_and_automations.dart | 2 +- .../routiens/widgets/then_container.dart | 2 +- 15 files changed, 475 insertions(+), 196 deletions(-) create mode 100644 lib/pages/routiens/bloc/functions_bloc/functions_bloc_bloc.dart create mode 100644 lib/pages/routiens/bloc/functions_bloc/functions_bloc_event.dart create mode 100644 lib/pages/routiens/bloc/functions_bloc/functions_bloc_state.dart rename lib/pages/routiens/bloc/{ => routine_bloc}/routine_bloc.dart (100%) rename lib/pages/routiens/bloc/{ => routine_bloc}/routine_event.dart (100%) rename lib/pages/routiens/bloc/{ => routine_bloc}/routine_state.dart (100%) diff --git a/lib/main.dart b/lib/main.dart index ac002f85..2040d175 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,7 +6,7 @@ import 'package:go_router/go_router.dart'; import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart'; import 'package:syncrow_web/pages/home/bloc/home_bloc.dart'; import 'package:syncrow_web/pages/home/bloc/home_event.dart'; -import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart'; import 'package:syncrow_web/services/locator.dart'; import 'package:syncrow_web/utils/app_routes.dart'; diff --git a/lib/pages/routiens/bloc/functions_bloc/functions_bloc_bloc.dart b/lib/pages/routiens/bloc/functions_bloc/functions_bloc_bloc.dart new file mode 100644 index 00000000..54b47cad --- /dev/null +++ b/lib/pages/routiens/bloc/functions_bloc/functions_bloc_bloc.dart @@ -0,0 +1,148 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; + +part 'functions_bloc_event.dart'; +part 'functions_bloc_state.dart'; + +class FunctionBloc extends Bloc { + FunctionBloc() : super(const FunctionBlocState()) { + on(_onInitializeFunctions); + on(_onAddFunction); + on(_onUpdateFunctions); + on(_onUpdateFunctionValue); + on(_onUpdateFunctionCondition); + on(_onRemoveFunction); + } + + void _onAddFunction(AddFunction event, Emitter emit) { + debugPrint('Adding function: ${event.functionData.function}'); + final functions = List.from(state.functions); + + // Find existing function data + final existingIndex = functions.indexWhere( + (f) => f.function == event.functionData.function, + ); + + // If function exists, preserve its value and condition + if (existingIndex != -1) { + final existingData = functions[existingIndex]; + functions[existingIndex] = DeviceFunctionData( + entityId: event.functionData.entityId, + function: event.functionData.function, + operationName: event.functionData.operationName, + value: existingData.value, // Preserve the existing value + condition: existingData.condition, // Preserve the existing condition + ); + } else { + functions.add(event.functionData); + } + + debugPrint('Functions after add: $functions'); + emit(state.copyWith( + functions: functions, + selectedFunction: event.functionData.function, + )); + } + + void _onUpdateFunctions( + UpdateFunction event, Emitter emit) { + final functions = state.functions.map((data) { + return data.function == event.functionData.function + ? event.functionData + : data; + }).toList(); + + emit(state.copyWith(functions: functions)); + } + + void _onUpdateFunctionValue( + UpdateFunctionValue event, + Emitter emit, + ) { + debugPrint('Updating function value: ${event.function} -> ${event.value}'); + + // Create a new list to ensure state immutability + final functions = List.from(state.functions); + + // Find the index of the function to update + final functionIndex = functions.indexWhere( + (data) => data.function == event.function, + ); + + if (functionIndex != -1) { + // Update the existing function data while preserving other fields + final existingData = functions[functionIndex]; + functions[functionIndex] = DeviceFunctionData( + entityId: existingData.entityId, + function: existingData.function, + operationName: existingData.operationName, + value: event.value, + condition: existingData.condition, + ); + } else { + // If function doesn't exist, add it + functions.add(DeviceFunctionData( + entityId: '', + function: event.function, + operationName: '', + value: event.value, + )); + } + + debugPrint('Functions after update: $functions'); + emit(state.copyWith(functions: functions)); + } + + void _onUpdateFunctionCondition( + UpdateFunctionCondition event, + Emitter emit, + ) { + final functions = state.functions.map((data) { + if (data.function == event.function) { + return DeviceFunctionData( + entityId: data.entityId, + function: data.function, + operationName: data.operationName, + value: data.value, + condition: event.condition, + ); + } + return data; + }).toList(); + + emit(state.copyWith(functions: functions)); + } + + void _onRemoveFunction( + RemoveFunction event, Emitter emit) { + final functions = state.functions + .where((data) => data.function != event.functionCode) + .toList(); + + emit(state.copyWith( + functions: functions, + selectedFunction: functions.isEmpty ? null : state.selectedFunction, + )); + } + + void _onInitializeFunctions( + InitializeFunctions event, + Emitter emit, + ) { + emit(state.copyWith(functions: event.functions)); + } + + DeviceFunctionData? getFunction(String functionCode) { + return state.functions.firstWhere( + (data) => data.function == functionCode, + orElse: () => DeviceFunctionData( + entityId: '', + function: functionCode, + operationName: '', + value: null, + ), + ); + } +} diff --git a/lib/pages/routiens/bloc/functions_bloc/functions_bloc_event.dart b/lib/pages/routiens/bloc/functions_bloc/functions_bloc_event.dart new file mode 100644 index 00000000..cceb6d21 --- /dev/null +++ b/lib/pages/routiens/bloc/functions_bloc/functions_bloc_event.dart @@ -0,0 +1,72 @@ +part of 'functions_bloc_bloc.dart'; + +abstract class FunctionBlocEvent extends Equatable { + const FunctionBlocEvent(); + + @override + List get props => []; +} + +class AddFunction extends FunctionBlocEvent { + final DeviceFunctionData functionData; + + const AddFunction({ + required this.functionData, + }); + + @override + List get props => [functionData]; +} + +class UpdateFunction extends FunctionBlocEvent { + final DeviceFunctionData functionData; + + const UpdateFunction(this.functionData); + + @override + List get props => [functionData]; +} + +class UpdateFunctionValue extends FunctionBlocEvent { + final String function; + final dynamic value; + + const UpdateFunctionValue({ + required this.function, + required this.value, + }); + + @override + List get props => [function, value]; +} + +class UpdateFunctionCondition extends FunctionBlocEvent { + final String function; + final String condition; + + const UpdateFunctionCondition({ + required this.function, + required this.condition, + }); + + @override + List get props => [function, condition]; +} + +class RemoveFunction extends FunctionBlocEvent { + final String functionCode; + + const RemoveFunction(this.functionCode); + + @override + List get props => [functionCode]; +} + +class InitializeFunctions extends FunctionBlocEvent { + final List functions; + + const InitializeFunctions(this.functions); + + @override + List get props => [functions]; +} diff --git a/lib/pages/routiens/bloc/functions_bloc/functions_bloc_state.dart b/lib/pages/routiens/bloc/functions_bloc/functions_bloc_state.dart new file mode 100644 index 00000000..af854b8a --- /dev/null +++ b/lib/pages/routiens/bloc/functions_bloc/functions_bloc_state.dart @@ -0,0 +1,24 @@ +part of 'functions_bloc_bloc.dart'; + +class FunctionBlocState extends Equatable { + final List functions; + final String? selectedFunction; + + const FunctionBlocState({ + this.functions = const [], + this.selectedFunction, + }); + + FunctionBlocState copyWith({ + List? functions, + String? selectedFunction, + }) { + return FunctionBlocState( + functions: functions ?? this.functions, + selectedFunction: selectedFunction ?? this.selectedFunction, + ); + } + + @override + List get props => [functions, selectedFunction]; +} diff --git a/lib/pages/routiens/bloc/routine_bloc.dart b/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart similarity index 100% rename from lib/pages/routiens/bloc/routine_bloc.dart rename to lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart diff --git a/lib/pages/routiens/bloc/routine_event.dart b/lib/pages/routiens/bloc/routine_bloc/routine_event.dart similarity index 100% rename from lib/pages/routiens/bloc/routine_event.dart rename to lib/pages/routiens/bloc/routine_bloc/routine_event.dart diff --git a/lib/pages/routiens/bloc/routine_state.dart b/lib/pages/routiens/bloc/routine_bloc/routine_state.dart similarity index 100% rename from lib/pages/routiens/bloc/routine_state.dart rename to lib/pages/routiens/bloc/routine_bloc/routine_state.dart diff --git a/lib/pages/routiens/helper/ac_helper.dart b/lib/pages/routiens/helper/ac_helper.dart index d940f4e4..327d0787 100644 --- a/lib/pages/routiens/helper/ac_helper.dart +++ b/lib/pages/routiens/helper/ac_helper.dart @@ -6,6 +6,8 @@ import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart'; import 'package:syncrow_web/pages/routiens/widgets/dialog_header.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/routiens/bloc/functions_bloc/functions_bloc_bloc.dart'; class ACHelper { static Future?> showACFunctionsDialog( @@ -13,97 +15,122 @@ class ACHelper { List> functions, ) async { List acFunctions = functions.whereType().toList(); - final selectedFunctionNotifier = ValueNotifier(null); - final selectedValueNotifier = ValueNotifier(null); - final selectedConditionNotifier = ValueNotifier('=='); - final selectedConditionsNotifier = - ValueNotifier>([false, true, false]); + + // Initialize the FunctionBloc with existing functions + final initialFunctions = acFunctions + .map((f) => DeviceFunctionData( + entityId: '', + function: f.code, + operationName: f.operationName, + value: null, + )) + .toList(); return showDialog?>( context: context, builder: (BuildContext context) { - return ValueListenableBuilder( - valueListenable: selectedFunctionNotifier, - builder: (context, selectedFunction, _) { - return AlertDialog( - contentPadding: EdgeInsets.zero, - content: 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('AC Functions'), - Expanded( - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // Function list - SizedBox( - width: selectedFunction != null ? 320 : 360, - child: _buildFunctionsList( - context, - acFunctions, - selectedFunctionNotifier, - ), - ), - // Value selector - if (selectedFunction != null) - Expanded( - child: _buildValueSelector( + return BlocProvider( + create: (_) => + FunctionBloc()..add(InitializeFunctions(initialFunctions)), + child: AlertDialog( + contentPadding: EdgeInsets.zero, + content: BlocBuilder( + builder: (context, state) { + debugPrint( + 'Current state - Selected: ${state.selectedFunction}, Functions: ${state.functions}'); + + final selectedFunction = state.selectedFunction; + final selectedFunctionData = selectedFunction != null + ? state.functions.firstWhere( + (f) => f.function == selectedFunction, + orElse: () => DeviceFunctionData( + entityId: '', + function: selectedFunction, + operationName: '', + value: null, + ), + ) + : null; + + 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('AC Functions'), + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Function list + SizedBox( + width: selectedFunction != null ? 320 : 360, + child: _buildFunctionsList( context, - selectedFunction, - selectedValueNotifier, - selectedConditionNotifier, - selectedConditionsNotifier, acFunctions, + (functionCode) => + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: '', + function: functionCode, + operationName: '', + value: null, + )), + ), ), ), - ], + // Value selector + if (selectedFunction != null) + Expanded( + child: _buildValueSelector( + context, + selectedFunction, + selectedFunctionData, + (value) => context.read().add( + UpdateFunctionValue( + function: selectedFunction, + value: value, + ), + ), + (condition) => + context.read().add( + UpdateFunctionCondition( + function: selectedFunction, + condition: condition, + ), + ), + acFunctions, + ), + ), + ], + ), ), - ), - DialogFooter( - onCancel: () { - selectedFunctionNotifier.dispose(); - selectedValueNotifier.dispose(); - selectedConditionNotifier.dispose(); - selectedConditionsNotifier.dispose(); - Navigator.pop(context); - }, - onConfirm: selectedFunctionNotifier.value != null && - selectedValueNotifier.value != null - ? () { - selectedFunctionNotifier.dispose(); - selectedValueNotifier.dispose(); - selectedConditionNotifier.dispose(); - selectedConditionsNotifier.dispose(); - Navigator.pop(context, { - 'function': selectedFunctionNotifier.value, - 'value': selectedValueNotifier.value, - 'condition': - selectedConditionNotifier.value ?? "==", - }); - } - : null, - isConfirmEnabled: selectedFunction != null, - ), - ], - ), - ), - ); - }, + DialogFooter( + onCancel: () { + Navigator.pop(context); + }, + onConfirm: selectedFunction != null && + selectedFunctionData?.value != null + ? () {} + : null, + isConfirmEnabled: selectedFunction != null, + ), + ], + ), + ); + }, + ), + ), ); }, ).then((value) { - selectedFunctionNotifier.dispose(); - selectedValueNotifier.dispose(); - selectedConditionNotifier.dispose(); - selectedConditionsNotifier.dispose(); return value; }); } @@ -112,7 +139,7 @@ class ACHelper { static Widget _buildFunctionsList( BuildContext context, List acFunctions, - ValueNotifier selectedFunctionNotifier, + Function(String) onFunctionSelected, ) { return ListView.separated( shrinkWrap: false, @@ -146,7 +173,7 @@ class ACHelper { size: 16, color: ColorsManager.textGray, ), - onTap: () => selectedFunctionNotifier.value = function.code, + onTap: () => onFunctionSelected(function.code), ); }, ); @@ -156,60 +183,57 @@ class ACHelper { static Widget _buildValueSelector( BuildContext context, String selectedFunction, - ValueNotifier selectedValueNotifier, - ValueNotifier selectedConditionNotifier, - ValueNotifier> selectedConditionsNotifier, + DeviceFunctionData? selectedFunctionData, + Function(dynamic) onValueChanged, + Function(String) onConditionChanged, List acFunctions, ) { - // Handle temperature functions if (selectedFunction == 'temp_set' || selectedFunction == 'temp_current') { - // Initialize with 20°C (200 in internal representation) - if (selectedValueNotifier.value == null || - selectedValueNotifier.value is! int) { - selectedValueNotifier.value = 200; - } + final initialValue = selectedFunctionData?.value ?? 200; return _buildTemperatureSelector( context, - selectedValueNotifier, - selectedConditionNotifier, - selectedConditionsNotifier, + initialValue, + selectedFunctionData?.condition, + onValueChanged, + onConditionChanged, ); } - // Handle other functions final selectedFn = acFunctions.firstWhere((f) => f.code == selectedFunction); final values = selectedFn.getOperationalValues(); - // Don't set any default value for non-temperature functions return _buildOperationalValuesList( context, values, - selectedValueNotifier, + selectedFunctionData?.value, + onValueChanged, ); } /// Build temperature selector for AC functions dialog static Widget _buildTemperatureSelector( BuildContext context, - ValueNotifier selectedValueNotifier, - ValueNotifier selectedConditionNotifier, - ValueNotifier> selectedConditionsNotifier, + dynamic initialValue, + String? currentCondition, + Function(dynamic) onValueChanged, + Function(String) onConditionChanged, ) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildConditionToggle( context, - selectedConditionNotifier, - selectedConditionsNotifier, + currentCondition, + onConditionChanged, ), const SizedBox(height: 20), - _buildTemperatureDisplay(context, selectedValueNotifier), + _buildTemperatureDisplay(context, initialValue), const SizedBox(height: 20), _buildTemperatureSlider( context, - selectedValueNotifier, + initialValue, + onValueChanged, ), ], ); @@ -218,79 +242,61 @@ class ACHelper { /// Build condition toggle for AC functions dialog static Widget _buildConditionToggle( BuildContext context, - ValueNotifier selectedConditionNotifier, - ValueNotifier> selectedConditionsNotifier, + String? currentCondition, + Function(String) onConditionChanged, ) { - return ValueListenableBuilder>( - valueListenable: selectedConditionsNotifier, - builder: (context, selectedConditions, _) { - return ToggleButtons( - onPressed: (int index) { - final newConditions = [false, false, false]; - newConditions[index] = true; - selectedConditionsNotifier.value = newConditions; - selectedConditionNotifier.value = index == 0 - ? "<" - : index == 1 - ? "==" - : ">"; - }, - 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: selectedConditions, - children: const [Text("<"), Text("="), Text(">")], - ); + final conditions = ["<", "==", ">"]; + + return ToggleButtons( + onPressed: (int index) { + onConditionChanged(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(), ); } /// Build temperature display for AC functions dialog static Widget _buildTemperatureDisplay( - BuildContext context, ValueNotifier selectedValueNotifier) { + BuildContext context, dynamic initialValue) { return Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), decoration: BoxDecoration( color: ColorsManager.primaryColorWithOpacity.withOpacity(0.1), borderRadius: BorderRadius.circular(10), ), - child: ValueListenableBuilder( - valueListenable: selectedValueNotifier, - builder: (context, selectedValue, child) { - return Text( - '${(selectedValue ?? 200) / 10}°C', - style: context.textTheme.headlineMedium!.copyWith( - color: ColorsManager.primaryColorWithOpacity, - ), - ); - }, + child: Text( + '${(initialValue ?? 200) / 10}°C', + style: context.textTheme.headlineMedium!.copyWith( + color: ColorsManager.primaryColorWithOpacity, + ), ), ); } static Widget _buildTemperatureSlider( BuildContext context, - ValueNotifier selectedValueNotifier, + dynamic initialValue, + Function(dynamic) onValueChanged, ) { - return ValueListenableBuilder( - valueListenable: selectedValueNotifier, - builder: (context, selectedValue, child) { - final currentValue = - selectedValue is int ? selectedValue.toDouble() : 200.0; - return Slider( - value: currentValue, - min: 160, - max: 300, - divisions: 14, - label: '${(currentValue / 10).toInt()}°C', - onChanged: (value) => selectedValueNotifier.value = value.toInt(), - ); + return Slider( + value: initialValue is int ? initialValue.toDouble() : 200.0, + min: 160, + max: 300, + divisions: 14, + label: '${((initialValue ?? 200) / 10).toInt()}°C', + onChanged: (value) { + onValueChanged(value.toInt()); }, ); } @@ -298,40 +304,44 @@ class ACHelper { static Widget _buildOperationalValuesList( BuildContext context, List values, - ValueNotifier selectedValueNotifier, + dynamic selectedValue, + Function(dynamic) onValueChanged, ) { - return ValueListenableBuilder( - valueListenable: selectedValueNotifier, - builder: (context, selectedValue, _) { - return ListView.builder( - shrinkWrap: false, - physics: const AlwaysScrollableScrollPhysics(), - itemCount: values.length, - itemBuilder: (context, index) { - final value = values[index]; - return ListTile( - leading: SvgPicture.asset( - value.icon, - width: 24, - height: 24, - placeholderBuilder: (BuildContext context) => Container( - width: 24, - height: 24, - color: Colors.transparent, - ), - ), - title: Text( - value.description, - style: context.textTheme.bodyMedium, - ), - trailing: Radio( - value: value.value, - groupValue: selectedValue, - onChanged: (_) => selectedValueNotifier.value = value.value, - activeColor: ColorsManager.primaryColorWithOpacity, - ), - onTap: () => selectedValueNotifier.value = value.value, - ); + return ListView.builder( + shrinkWrap: false, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: values.length, + itemBuilder: (context, index) { + final value = values[index]; + final isSelected = selectedValue == value.value; + return ListTile( + leading: SvgPicture.asset( + value.icon, + width: 24, + height: 24, + placeholderBuilder: (BuildContext context) => Container( + width: 24, + height: 24, + color: Colors.transparent, + ), + ), + title: Text( + value.description, + style: context.textTheme.bodyMedium, + ), + trailing: Icon( + isSelected + ? Icons.radio_button_checked + : Icons.radio_button_unchecked, + size: 24, + color: isSelected + ? ColorsManager.primaryColorWithOpacity + : ColorsManager.textGray, + ), + onTap: () { + if (!isSelected) { + onValueChanged(value.value); + } }, ); }, diff --git a/lib/pages/routiens/models/device_functions.dart b/lib/pages/routiens/models/device_functions.dart index 303fe48c..4234c812 100644 --- a/lib/pages/routiens/models/device_functions.dart +++ b/lib/pages/routiens/models/device_functions.dart @@ -58,4 +58,29 @@ class DeviceFunctionData { valueDescription: json['valueDescription'], ); } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is DeviceFunctionData && + other.entityId == entityId && + other.actionExecutor == actionExecutor && + other.function == function && + other.operationName == operationName && + other.value == value && + other.condition == condition && + other.valueDescription == valueDescription; + } + + @override + int get hashCode { + return entityId.hashCode ^ + actionExecutor.hashCode ^ + function.hashCode ^ + operationName.hashCode ^ + value.hashCode ^ + condition.hashCode ^ + valueDescription.hashCode; + } } diff --git a/lib/pages/routiens/widgets/conditions_routines_devices_view.dart b/lib/pages/routiens/widgets/conditions_routines_devices_view.dart index 2728d329..7bda1d51 100644 --- a/lib/pages/routiens/widgets/conditions_routines_devices_view.dart +++ b/lib/pages/routiens/widgets/conditions_routines_devices_view.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; import 'package:syncrow_web/pages/routiens/widgets/routine_devices.dart'; import 'package:syncrow_web/pages/routiens/widgets/routines_title_widget.dart'; diff --git a/lib/pages/routiens/widgets/dragable_card.dart b/lib/pages/routiens/widgets/dragable_card.dart index 227e440b..016b82c1 100644 --- a/lib/pages/routiens/widgets/dragable_card.dart +++ b/lib/pages/routiens/widgets/dragable_card.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; diff --git a/lib/pages/routiens/widgets/if_container.dart b/lib/pages/routiens/widgets/if_container.dart index af1394c6..779453f1 100644 --- a/lib/pages/routiens/widgets/if_container.dart +++ b/lib/pages/routiens/widgets/if_container.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/helper/dialog_helper/device_dialog_helper.dart'; import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; diff --git a/lib/pages/routiens/widgets/routine_devices.dart b/lib/pages/routiens/widgets/routine_devices.dart index 7d97de18..0dc6e35e 100644 --- a/lib/pages/routiens/widgets/routine_devices.dart +++ b/lib/pages/routiens/widgets/routine_devices.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart'; -import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; class RoutineDevices extends StatelessWidget { diff --git a/lib/pages/routiens/widgets/scenes_and_automations.dart b/lib/pages/routiens/widgets/scenes_and_automations.dart index ef40b605..a74634ba 100644 --- a/lib/pages/routiens/widgets/scenes_and_automations.dart +++ b/lib/pages/routiens/widgets/scenes_and_automations.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; diff --git a/lib/pages/routiens/widgets/then_container.dart b/lib/pages/routiens/widgets/then_container.dart index 385ec4cb..e4df6d8b 100644 --- a/lib/pages/routiens/widgets/then_container.dart +++ b/lib/pages/routiens/widgets/then_container.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/routiens/bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/helper/dialog_helper/device_dialog_helper.dart'; import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; From 5e91d7c03ade0016c0de5fbb66a9c4ce8ce35134 Mon Sep 17 00:00:00 2001 From: ashrafzarkanisala Date: Sat, 23 Nov 2024 01:39:05 +0300 Subject: [PATCH 12/23] push adding the selected functiond and device to then container --- .../bloc/routine_bloc/routine_bloc.dart | 4 ++-- .../bloc/routine_bloc/routine_event.dart | 4 ++-- lib/pages/routiens/helper/ac_helper.dart | 17 +++++++++++---- .../dialog_helper/device_dialog_helper.dart | 21 +++++++++++-------- .../helper/three_gang_switch_helper.dart | 2 +- .../helper/two_gang_switch_helper.dart | 2 +- .../routiens/widgets/then_container.dart | 2 +- 7 files changed, 32 insertions(+), 20 deletions(-) diff --git a/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart b/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart index d69df552..20a3c393 100644 --- a/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart +++ b/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart @@ -15,7 +15,7 @@ class RoutineBloc extends Bloc { on(_onAddToThenContainer); on(_onLoadScenes); on(_onLoadAutomation); - on(_onAddFunction); + on(_onAddFunction); on(_onRemoveFunction); on(_onClearFunctions); } @@ -37,7 +37,7 @@ class RoutineBloc extends Bloc { } } - void _onAddFunction(AddFunction event, Emitter emit) { + void _onAddFunction(AddFunctionToRoutine event, Emitter emit) { final functions = List.from(state.selectedFunctions); functions.add(event.function); emit(state.copyWith(selectedFunctions: functions)); diff --git a/lib/pages/routiens/bloc/routine_bloc/routine_event.dart b/lib/pages/routiens/bloc/routine_bloc/routine_event.dart index d3af8134..3321af5e 100644 --- a/lib/pages/routiens/bloc/routine_bloc/routine_event.dart +++ b/lib/pages/routiens/bloc/routine_bloc/routine_event.dart @@ -43,9 +43,9 @@ class LoadAutomation extends RoutineEvent { List get props => [unitId]; } -class AddFunction extends RoutineEvent { +class AddFunctionToRoutine extends RoutineEvent { final DeviceFunctionData function; - const AddFunction(this.function); + const AddFunctionToRoutine(this.function); @override List get props => [function]; } diff --git a/lib/pages/routiens/helper/ac_helper.dart b/lib/pages/routiens/helper/ac_helper.dart index 327d0787..4a42c43d 100644 --- a/lib/pages/routiens/helper/ac_helper.dart +++ b/lib/pages/routiens/helper/ac_helper.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/models/ac/ac_function.dart'; import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart'; @@ -36,9 +37,6 @@ class ACHelper { contentPadding: EdgeInsets.zero, content: BlocBuilder( builder: (context, state) { - debugPrint( - 'Current state - Selected: ${state.selectedFunction}, Functions: ${state.functions}'); - final selectedFunction = state.selectedFunction; final selectedFunctionData = selectedFunction != null ? state.functions.firstWhere( @@ -118,7 +116,18 @@ class ACHelper { }, onConfirm: selectedFunction != null && selectedFunctionData?.value != null - ? () {} + ? () { + /// add the functions to the routine bloc + for (var function in state.functions) { + context.read().add( + AddFunctionToRoutine( + function, + ), + ); + } + // Return the device data to be added to the container + Navigator.pop(context); + } : null, isConfirmEnabled: selectedFunction != null, ), diff --git a/lib/pages/routiens/helper/dialog_helper/device_dialog_helper.dart b/lib/pages/routiens/helper/dialog_helper/device_dialog_helper.dart index ea4743b7..2b21d83d 100644 --- a/lib/pages/routiens/helper/dialog_helper/device_dialog_helper.dart +++ b/lib/pages/routiens/helper/dialog_helper/device_dialog_helper.dart @@ -13,11 +13,15 @@ class DeviceDialogHelper { final functions = data['functions'] as List; try { - await _getDialogForDeviceType( + final result = await _getDialogForDeviceType( context, data['productType'], functions, ); + + if (result != null) { + return result; + } } catch (e) { debugPrint('Error: $e'); } @@ -25,23 +29,22 @@ class DeviceDialogHelper { return null; } - static Future _getDialogForDeviceType( + static Future?> _getDialogForDeviceType( BuildContext context, String productType, List functions, ) async { switch (productType) { case 'AC': - await ACHelper.showACFunctionsDialog(context, functions); - break; + return ACHelper.showACFunctionsDialog(context, functions); case '1G': - await OneGangSwitchHelper.showSwitchFunctionsDialog(context, functions); - break; + return OneGangSwitchHelper.showSwitchFunctionsDialog( + context, functions); case '2G': - await TwoGangSwitchHelper.showSwitchFunctionsDialog(context, functions); - break; + return TwoGangSwitchHelper.showSwitchFunctionsDialog( + context, functions); case '3G': - await ThreeGangSwitchHelper.showSwitchFunctionsDialog( + return ThreeGangSwitchHelper.showSwitchFunctionsDialog( context, functions); break; } diff --git a/lib/pages/routiens/helper/three_gang_switch_helper.dart b/lib/pages/routiens/helper/three_gang_switch_helper.dart index 1e62f5c4..ecbe81f2 100644 --- a/lib/pages/routiens/helper/three_gang_switch_helper.dart +++ b/lib/pages/routiens/helper/three_gang_switch_helper.dart @@ -9,7 +9,7 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class ThreeGangSwitchHelper { - static Future showSwitchFunctionsDialog( + static Future?> showSwitchFunctionsDialog( BuildContext context, List> functions) async { List> switchFunctions = functions .where((f) => diff --git a/lib/pages/routiens/helper/two_gang_switch_helper.dart b/lib/pages/routiens/helper/two_gang_switch_helper.dart index 8a7e5580..f0e3026a 100644 --- a/lib/pages/routiens/helper/two_gang_switch_helper.dart +++ b/lib/pages/routiens/helper/two_gang_switch_helper.dart @@ -9,7 +9,7 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class TwoGangSwitchHelper { - static Future showSwitchFunctionsDialog( + static Future?> showSwitchFunctionsDialog( BuildContext context, List> functions) async { List> switchFunctions = functions .where((f) => diff --git a/lib/pages/routiens/widgets/then_container.dart b/lib/pages/routiens/widgets/then_container.dart index e4df6d8b..633a1ad1 100644 --- a/lib/pages/routiens/widgets/then_container.dart +++ b/lib/pages/routiens/widgets/then_container.dart @@ -46,7 +46,7 @@ class ThenContainer extends StatelessWidget { final result = await DeviceDialogHelper.showDeviceDialog(context, data); if (result != null) { - context.read().add(AddToThenContainer(result)); + context.read().add(AddToThenContainer(data)); } else if (!['AC', '1G', '2G', '3G'] .contains(data['productType'])) { context.read().add(AddToThenContainer(data)); From e84df8b7a6a5d476270c91dd4e042b4e1a239b40 Mon Sep 17 00:00:00 2001 From: Abdullah Alassaf Date: Sat, 23 Nov 2024 21:44:47 +0300 Subject: [PATCH 13/23] Added scene setting --- .../effective_period/effect_period_bloc.dart | 125 ++++++ .../effective_period/effect_period_event.dart | 50 +++ .../effective_period/effect_period_state.dart | 54 +++ .../bloc/setting_bloc/setting_bloc.dart | 53 +++ .../bloc/setting_bloc/setting_event.dart | 32 ++ .../bloc/setting_bloc/setting_state.dart | 56 +++ .../helper/effictive_period_helper.dart | 151 +++++++ lib/pages/routiens/helper/setting_helper.dart | 384 ++++++++++++++++++ lib/pages/routiens/models/icon_model.dart | 39 ++ .../routiens/view/effective_period_view.dart | 50 +++ lib/pages/routiens/widgets/period_option.dart | 97 +++++ lib/pages/routiens/widgets/repeat_days.dart | 83 ++++ .../widgets/routine_search_and_buttons.dart | 10 +- lib/services/routines_api.dart | 16 + lib/utils/constants/api_const.dart | 7 +- lib/utils/constants/app_enum.dart | 8 + pubspec.lock | 32 +- pubspec.yaml | 1 + 18 files changed, 1231 insertions(+), 17 deletions(-) create mode 100644 lib/pages/routiens/bloc/effective_period/effect_period_bloc.dart create mode 100644 lib/pages/routiens/bloc/effective_period/effect_period_event.dart create mode 100644 lib/pages/routiens/bloc/effective_period/effect_period_state.dart create mode 100644 lib/pages/routiens/bloc/setting_bloc/setting_bloc.dart create mode 100644 lib/pages/routiens/bloc/setting_bloc/setting_event.dart create mode 100644 lib/pages/routiens/bloc/setting_bloc/setting_state.dart create mode 100644 lib/pages/routiens/helper/effictive_period_helper.dart create mode 100644 lib/pages/routiens/helper/setting_helper.dart create mode 100644 lib/pages/routiens/models/icon_model.dart create mode 100644 lib/pages/routiens/view/effective_period_view.dart create mode 100644 lib/pages/routiens/widgets/period_option.dart create mode 100644 lib/pages/routiens/widgets/repeat_days.dart diff --git a/lib/pages/routiens/bloc/effective_period/effect_period_bloc.dart b/lib/pages/routiens/bloc/effective_period/effect_period_bloc.dart new file mode 100644 index 00000000..05f63123 --- /dev/null +++ b/lib/pages/routiens/bloc/effective_period/effect_period_bloc.dart @@ -0,0 +1,125 @@ +import 'dart:async'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_event.dart'; +import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_state.dart'; +import 'package:syncrow_web/utils/constants/app_enum.dart'; + +class EffectPeriodBloc extends Bloc { + final daysMap = { + 'Sun': 'S', + 'Mon': 'M', + 'Tue': 'T', + 'Wed': 'W', + 'Thu': 'T', + 'Fri': 'F', + 'Sat': 'S', + }; + + EffectPeriodBloc() : super(EffectPeriodState.initial()) { + on(_onSetPeriod); + on(_onToggleDay); + on(_onSetCustomTime); + on(_onResetEffectivePeriod); + on(_onResetDays); + on(_setAllDays); + } + + void _onSetPeriod(SetPeriod event, Emitter emit) { + String startTime = ''; + String endTime = ''; + + switch (event.period) { + case EnumEffectivePeriodOptions.allDay: + startTime = '00:00'; + endTime = '23:59'; + break; + case EnumEffectivePeriodOptions.daytime: + startTime = '06:00'; + endTime = '18:00'; + break; + case EnumEffectivePeriodOptions.night: + startTime = '18:00'; + endTime = '06:00'; + break; + case EnumEffectivePeriodOptions.custom: + startTime = state.customStartTime ?? '00:00'; + endTime = state.customEndTime ?? '23:59'; + break; + default: + break; + } + + // BlocProvider.of(NavigationService.navigatorKey.currentContext!).add( + // EffectiveTimePeriodEvent( + // EffectiveTime(start: startTime, end: endTime, loops: state.selectedDaysBinary))); + + emit(state.copyWith( + selectedPeriod: event.period, customStartTime: startTime, customEndTime: endTime)); + } + + void _onToggleDay(ToggleDay event, Emitter emit) { + final daysList = state.selectedDaysBinary.split(''); + final dayIndex = getDayIndex(event.day); + if (daysList[dayIndex] == '1') { + daysList[dayIndex] = '0'; + } else { + daysList[dayIndex] = '1'; + } + final newDaysBinary = daysList.join(); + emit(state.copyWith(selectedDaysBinary: newDaysBinary)); + + // BlocProvider.of(NavigationService.navigatorKey.currentContext!).add( + // EffectiveTimePeriodEvent(EffectiveTime( + // start: state.customStartTime ?? '00:00', + // end: state.customEndTime ?? '23:59', + // loops: newDaysBinary))); + } + + void _onSetCustomTime(SetCustomTime event, Emitter emit) { + String startTime = event.startTime; + String endTime = event.endTime; + EnumEffectivePeriodOptions period; + + // Determine the period based on start and end times + if (startTime == '00:00' && endTime == '23:59') { + period = EnumEffectivePeriodOptions.allDay; + } else if (startTime == '06:00' && endTime == '18:00') { + period = EnumEffectivePeriodOptions.daytime; + } else if (startTime == '18:00' && endTime == '06:00') { + period = EnumEffectivePeriodOptions.night; + } else { + period = EnumEffectivePeriodOptions.custom; + } + + emit( + state.copyWith(customStartTime: startTime, customEndTime: endTime, selectedPeriod: period)); + + // BlocProvider.of(NavigationService.navigatorKey.currentContext!).add( + // EffectiveTimePeriodEvent( + // EffectiveTime(start: startTime, end: endTime, loops: state.selectedDaysBinary))); + } + + void _onResetEffectivePeriod(ResetEffectivePeriod event, Emitter emit) { + emit(state.copyWith( + selectedPeriod: EnumEffectivePeriodOptions.allDay, + customStartTime: '00:00', + customEndTime: '23:59', + selectedDaysBinary: '1111111')); + + // BlocProvider.of(NavigationService.navigatorKey.currentContext!).add( + // EffectiveTimePeriodEvent(EffectiveTime(start: '00:00', end: '23:59', loops: '1111111'))); + } + + void _onResetDays(ResetDays event, Emitter emit) { + emit(state.copyWith(selectedDaysBinary: '1111111')); + } + + int getDayIndex(String day) { + const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + return days.indexOf(day); + } + + FutureOr _setAllDays(SetDays event, Emitter emit) { + emit(state.copyWith(selectedDaysBinary: event.daysBinary)); + } +} diff --git a/lib/pages/routiens/bloc/effective_period/effect_period_event.dart b/lib/pages/routiens/bloc/effective_period/effect_period_event.dart new file mode 100644 index 00000000..e1a86915 --- /dev/null +++ b/lib/pages/routiens/bloc/effective_period/effect_period_event.dart @@ -0,0 +1,50 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/utils/constants/app_enum.dart'; + +abstract class EffectPeriodEvent extends Equatable { + const EffectPeriodEvent(); + + @override + List get props => []; +} + +class SetPeriod extends EffectPeriodEvent { + final EnumEffectivePeriodOptions period; + + const SetPeriod(this.period); + + @override + List get props => [period]; +} + +class ToggleDay extends EffectPeriodEvent { + final String day; + + const ToggleDay(this.day); + + @override + List get props => [day]; +} + +class SetCustomTime extends EffectPeriodEvent { + final String startTime; + final String endTime; + + const SetCustomTime(this.startTime, this.endTime); + + @override + List get props => [startTime, endTime]; +} + +class ResetEffectivePeriod extends EffectPeriodEvent {} + +class ResetDays extends EffectPeriodEvent { + @override + List get props => []; +} + +class SetDays extends EffectPeriodEvent { + final String daysBinary; + + const SetDays(this.daysBinary); +} diff --git a/lib/pages/routiens/bloc/effective_period/effect_period_state.dart b/lib/pages/routiens/bloc/effective_period/effect_period_state.dart new file mode 100644 index 00000000..2f8b66c8 --- /dev/null +++ b/lib/pages/routiens/bloc/effective_period/effect_period_state.dart @@ -0,0 +1,54 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/utils/constants/app_enum.dart'; + +class EffectPeriodState extends Equatable { + final EnumEffectivePeriodOptions selectedPeriod; + final String selectedDaysBinary; + final String? customStartTime; + final String? customEndTime; + + const EffectPeriodState({ + required this.selectedPeriod, + required this.selectedDaysBinary, + this.customStartTime, + this.customEndTime, + }); + + factory EffectPeriodState.initial() { + return const EffectPeriodState( + selectedPeriod: EnumEffectivePeriodOptions.allDay, + selectedDaysBinary: "1111111", // All days selected + customStartTime: "00:00", + customEndTime: "23:59", + ); + } + + EffectPeriodState copyWith({ + EnumEffectivePeriodOptions? selectedPeriod, + String? selectedDaysBinary, + String? customStartTime, + String? customEndTime, + }) { + return EffectPeriodState( + selectedPeriod: selectedPeriod ?? this.selectedPeriod, + selectedDaysBinary: selectedDaysBinary ?? this.selectedDaysBinary, + customStartTime: customStartTime ?? this.customStartTime, + customEndTime: customEndTime ?? this.customEndTime, + ); + } + + EnumEffectivePeriodOptions getEffectivePeriod() { + if (customStartTime == '00:00' && customEndTime == '23:59') { + return EnumEffectivePeriodOptions.allDay; + } else if (customStartTime == '06:00' && customEndTime == '18:00') { + return EnumEffectivePeriodOptions.daytime; + } else if (customStartTime == '18:00' && customEndTime == '06:00') { + return EnumEffectivePeriodOptions.night; + } else { + return EnumEffectivePeriodOptions.custom; + } + } + + @override + List get props => [selectedPeriod, selectedDaysBinary, customStartTime, customEndTime]; +} diff --git a/lib/pages/routiens/bloc/setting_bloc/setting_bloc.dart b/lib/pages/routiens/bloc/setting_bloc/setting_bloc.dart new file mode 100644 index 00000000..f70aed34 --- /dev/null +++ b/lib/pages/routiens/bloc/setting_bloc/setting_bloc.dart @@ -0,0 +1,53 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/setting_bloc/setting_event.dart'; +import 'package:syncrow_web/pages/routiens/bloc/setting_bloc/setting_state.dart'; +import 'package:syncrow_web/pages/routiens/models/icon_model.dart'; +import 'package:syncrow_web/services/routines_api.dart'; + +class SettingBloc extends Bloc { + bool isExpanded = false; + String selectedIcon = ''; + List iconModelList = []; + + SettingBloc() : super(const InitialState()) { + on(_initialSetting); + on(_fetchIcons); + on(_selectIcon); + } + + void _initialSetting(InitialEvent event, Emitter emit) async { + try { + emit(const LoadingState()); + selectedIcon = event.selectedIcon; + emit(TabToRunSettingLoaded( + showInDevice: true, selectedIcon: event.selectedIcon, iconList: iconModelList)); + } catch (e) { + emit(const FailedState(error: 'Something went wrong')); + } + } + + void _fetchIcons(FetchIcons event, Emitter emit) async { + try { + isExpanded = event.expanded; + emit(const LoadingState()); + if (isExpanded) { + iconModelList = await SceneApi.getIcon(); + emit(TabToRunSettingLoaded( + showInDevice: true, selectedIcon: selectedIcon, iconList: iconModelList)); + } + } catch (e) { + emit(const FailedState(error: 'Something went wrong')); + } + } + + void _selectIcon(SelectIcon event, Emitter emit) async { + try { + emit(const LoadingState()); + selectedIcon = event.iconId; + emit(TabToRunSettingLoaded( + showInDevice: true, selectedIcon: event.iconId, iconList: iconModelList)); + } catch (e) { + emit(const FailedState(error: 'Something went wrong')); + } + } +} diff --git a/lib/pages/routiens/bloc/setting_bloc/setting_event.dart b/lib/pages/routiens/bloc/setting_bloc/setting_event.dart new file mode 100644 index 00000000..0580e51e --- /dev/null +++ b/lib/pages/routiens/bloc/setting_bloc/setting_event.dart @@ -0,0 +1,32 @@ +import 'package:equatable/equatable.dart'; + +abstract class SettingEvent extends Equatable { + const SettingEvent(); + + @override + List get props => []; +} + +class InitialEvent extends SettingEvent { + final String selectedIcon; + const InitialEvent({required this.selectedIcon}); + + @override + List get props => [selectedIcon]; +} + +class FetchIcons extends SettingEvent { + final bool expanded; + const FetchIcons({required this.expanded}); + + @override + List get props => [expanded]; +} + +class SelectIcon extends SettingEvent { + final String iconId; + const SelectIcon({required this.iconId}); + + @override + List get props => [iconId]; +} diff --git a/lib/pages/routiens/bloc/setting_bloc/setting_state.dart b/lib/pages/routiens/bloc/setting_bloc/setting_state.dart new file mode 100644 index 00000000..7c88d67c --- /dev/null +++ b/lib/pages/routiens/bloc/setting_bloc/setting_state.dart @@ -0,0 +1,56 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/routiens/models/icon_model.dart'; + +abstract class SettingState extends Equatable { + const SettingState(); + + @override + List get props => []; +} + +class LoadingState extends SettingState { + const LoadingState(); + + @override + List get props => []; +} + +class InitialState extends SettingState { + const InitialState(); + + @override + List get props => []; +} + +class IconLoadedState extends SettingState { + final List status; + + const IconLoadedState(this.status); + + @override + List get props => [status]; +} + +class TabToRunSettingLoaded extends SettingState { + final String selectedIcon; + final List iconList; + final bool showInDevice; + + const TabToRunSettingLoaded({ + required this.selectedIcon, + required this.iconList, + required this.showInDevice, + }); + + @override + List get props => [selectedIcon, iconList, showInDevice]; +} + +class FailedState extends SettingState { + final String error; + + const FailedState({required this.error}); + + @override + List get props => [error]; +} diff --git a/lib/pages/routiens/helper/effictive_period_helper.dart b/lib/pages/routiens/helper/effictive_period_helper.dart new file mode 100644 index 00000000..70a6776d --- /dev/null +++ b/lib/pages/routiens/helper/effictive_period_helper.dart @@ -0,0 +1,151 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_event.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/app_enum.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:time_picker_spinner/time_picker_spinner.dart'; + +class EffectPeriodHelper { + static Future?> showCustomTimePicker(BuildContext context) async { + String selectedStartTime = "00:00"; + String selectedEndTime = "23:59"; + PageController pageController = PageController(initialPage: 0); + + DateTime startDateTime = DateTime(2022, 1, 1, 0, 0); + DateTime endDateTime = DateTime(2022, 1, 1, 23, 59); + + context.customAlertDialog( + alertBody: SizedBox( + height: 250, + child: PageView( + controller: pageController, + physics: const NeverScrollableScrollPhysics(), + children: [ + _buildTimePickerPage( + context: context, + pageController: pageController, + isStartTime: true, + time: startDateTime, + onTimeChange: (time) { + selectedStartTime = + "${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}"; + }, + ), + _buildTimePickerPage( + context: context, + pageController: pageController, + isStartTime: false, + time: endDateTime, + onTimeChange: (time) { + selectedEndTime = + "${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}"; + }, + ), + ], + ), + ), + title: "Custom", + onConfirm: () { + context.read().add( + SetCustomTime(selectedStartTime, selectedEndTime), + ); + context.read().add( + const SetPeriod(EnumEffectivePeriodOptions.custom), + ); + + Navigator.of(context).pop(); + }, + ); + return null; + } + + static Widget _buildTimePickerPage({ + required BuildContext context, + required PageController pageController, + required bool isStartTime, + required DateTime time, + required Function(DateTime) onTimeChange, + }) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (!isStartTime) + TextButton( + onPressed: () { + pageController.previousPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeIn, + ); + }, + child: const Text('Start'), + ), + TextButton( + onPressed: () {}, + child: Text(isStartTime ? "Start" : "End", + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 10)), + ), + if (isStartTime) + TextButton( + onPressed: () { + pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeIn, + ); + }, + child: Text('End', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 10)), + ), + ], + ), + ), + TimePickerSpinner( + is24HourMode: false, + normalTextStyle: const TextStyle( + fontSize: 24, + color: Colors.grey, + ), + highlightedTextStyle: const TextStyle( + fontSize: 24, + color: ColorsManager.primaryColor, + ), + spacing: 20, + itemHeight: 50, + isForce2Digits: true, + time: time, + onTimeChange: onTimeChange, + ), + const SizedBox(height: 16), + ], + ); + } + + static String formatEnumValue(EnumEffectivePeriodOptions value) { + switch (value) { + case EnumEffectivePeriodOptions.allDay: + return "All Day"; + case EnumEffectivePeriodOptions.daytime: + return "Daytime"; + case EnumEffectivePeriodOptions.night: + return "Night"; + case EnumEffectivePeriodOptions.custom: + return "Custom"; + case EnumEffectivePeriodOptions.none: + return "None"; + default: + return ""; + } + } +} diff --git a/lib/pages/routiens/helper/setting_helper.dart b/lib/pages/routiens/helper/setting_helper.dart new file mode 100644 index 00000000..ae8a1bc8 --- /dev/null +++ b/lib/pages/routiens/helper/setting_helper.dart @@ -0,0 +1,384 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/setting_bloc/setting_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/setting_bloc/setting_event.dart'; +import 'package:syncrow_web/pages/routiens/bloc/setting_bloc/setting_state.dart'; +import 'package:syncrow_web/pages/routiens/models/icon_model.dart'; +import 'package:syncrow_web/pages/routiens/view/effective_period_view.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:flutter/cupertino.dart'; + +class SettingHelper { + static Future?> showSettingDialog( + {required BuildContext context, String? iconId, required bool isAutomation}) async { + return showDialog?>( + context: context, + builder: (BuildContext context) { + return BlocProvider( + create: (_) => SettingBloc()..add(InitialEvent(selectedIcon: iconId ?? '')), + child: AlertDialog( + contentPadding: EdgeInsets.zero, + content: BlocBuilder( + builder: (context, state) { + String selectedIcon = ''; + List list = []; + if (state is TabToRunSettingLoaded) { + selectedIcon = state.selectedIcon; + list = state.iconList; + } + return Container( + width: context.read().isExpanded ? 800 : 400, + height: context.read().isExpanded && isAutomation ? 500 : 300, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.only(top: 20), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const DialogHeader('Settings'), + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 400, + child: isAutomation + ? Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.only( + top: 10, left: 10, right: 10, bottom: 10), + child: Column( + children: [ + InkWell( + onTap: () {}, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Validity', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 14), + ), + const Icon( + Icons.arrow_forward_ios_outlined, + color: ColorsManager.textGray, + size: 15, + ) + ], + ), + ), + const SizedBox( + height: 5, + ), + const Divider( + color: ColorsManager.graysColor, + ), + const SizedBox( + height: 5, + ), + InkWell( + onTap: () { + BlocProvider.of(context).add( + FetchIcons( + expanded: !context + .read() + .isExpanded)); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Effective Period', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 14), + ), + const Icon( + Icons.arrow_forward_ios_outlined, + color: ColorsManager.textGray, + size: 15, + ) + ], + ), + ), + const SizedBox( + height: 5, + ), + const Divider( + color: ColorsManager.graysColor, + ), + const SizedBox( + height: 5, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Executed by', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 14), + ), + Text('Cloud', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: ColorsManager.textGray, + fontWeight: FontWeight.w400, + fontSize: 14)), + ], + ), + ], + )), + ], + ) + : Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.only( + top: 10, left: 10, right: 10, bottom: 10), + child: Column( + children: [ + InkWell( + onTap: () { + BlocProvider.of(context).add( + FetchIcons( + expanded: !context + .read() + .isExpanded)); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Icons', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 14), + ), + const Icon( + Icons.arrow_forward_ios_outlined, + color: ColorsManager.textGray, + size: 15, + ) + ], + ), + ), + const SizedBox( + height: 5, + ), + const Divider( + color: ColorsManager.graysColor, + ), + const SizedBox( + height: 5, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Show on devices page', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 14), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + height: 30, + width: 1, + color: ColorsManager.graysColor, + ), + Transform.scale( + scale: .8, + child: CupertinoSwitch( + value: true, + onChanged: (value) {}, + applyTheme: true, + ), + ), + ], + ) + ], + ), + const SizedBox( + height: 5, + ), + const Divider( + color: ColorsManager.graysColor, + ), + const SizedBox( + height: 5, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Executed by', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 14), + ), + Text('Cloud', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: ColorsManager.textGray, + fontWeight: FontWeight.w400, + fontSize: 14)), + ], + ), + ], + )), + ], + ), + ), + if (context.read().isExpanded && !isAutomation) + SizedBox( + width: 400, + height: 150, + child: state is LoadingState + ? const Center(child: CircularProgressIndicator()) + : GridView.builder( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 6, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + shrinkWrap: true, + itemCount: list.length, + itemBuilder: (context, index) { + final iconModel = list[index]; + return SizedBox( + width: 35, + height: 35, + child: InkWell( + onTap: () { + BlocProvider.of(context) + .add(SelectIcon( + iconId: iconModel.uuid, + )); + selectedIcon = iconModel.uuid; + }, + child: SizedBox( + child: ClipOval( + child: Container( + padding: const EdgeInsets.all(1), + decoration: BoxDecoration( + border: Border.all( + color: selectedIcon == iconModel.uuid + ? ColorsManager.primaryColorWithOpacity + : Colors.transparent, + width: 2, + ), + shape: BoxShape.circle, + ), + child: Image.memory( + iconModel.iconBytes, + ), + ), + ), + ), + ), + ); + }, + )), + if (context.read().isExpanded && isAutomation) + const SizedBox(height: 350, width: 400, child: EffectivePeriodView()) + ], + ), + Container( + width: MediaQuery.sizeOf(context).width, + decoration: const BoxDecoration( + border: Border( + top: BorderSide( + color: ColorsManager.greyColor, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: InkWell( + onTap: () { + Navigator.of(context).pop(); + }, + child: Container( + alignment: AlignmentDirectional.center, + child: Text( + 'Cancel', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.textGray, + ), + ), + ), + ), + ), + Container(width: 1, height: 50, color: ColorsManager.greyColor), + Expanded( + child: InkWell( + onTap: () { + Navigator.of(context).pop(selectedIcon); + }, + child: Container( + alignment: AlignmentDirectional.center, + child: Text( + 'Confirm', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.primaryColorWithOpacity, + ), + ), + ), + ), + ), + ], + ), + ) + ], + ), + ); + }, + ), + ), + ); + }, + ); + } +} diff --git a/lib/pages/routiens/models/icon_model.dart b/lib/pages/routiens/models/icon_model.dart new file mode 100644 index 00000000..70f15e8c --- /dev/null +++ b/lib/pages/routiens/models/icon_model.dart @@ -0,0 +1,39 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +class IconModel { + final String uuid; + final DateTime createdAt; + final DateTime updatedAt; + final String iconBase64; + + IconModel({ + required this.uuid, + required this.createdAt, + required this.updatedAt, + required this.iconBase64, + }); + + // Method to decode the icon from Base64 and return as Uint8List + Uint8List get iconBytes => base64Decode(iconBase64); + + // Factory constructor to create an instance from JSON + factory IconModel.fromJson(Map json) { + return IconModel( + uuid: json['uuid'] as String, + createdAt: DateTime.parse(json['createdAt'] as String), + updatedAt: DateTime.parse(json['updatedAt'] as String), + iconBase64: json['icon'] as String, + ); + } + + // Method to convert an instance back to JSON + Map toJson() { + return { + 'uuid': uuid, + 'createdAt': createdAt.toIso8601String(), + 'updatedAt': updatedAt.toIso8601String(), + 'icon': iconBase64, + }; + } +} diff --git a/lib/pages/routiens/view/effective_period_view.dart b/lib/pages/routiens/view/effective_period_view.dart new file mode 100644 index 00000000..5bcd9ff0 --- /dev/null +++ b/lib/pages/routiens/view/effective_period_view.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_bloc.dart'; +import 'package:syncrow_web/pages/routiens/helper/effictive_period_helper.dart'; +import 'package:syncrow_web/pages/routiens/widgets/period_option.dart'; +import 'package:syncrow_web/pages/routiens/widgets/repeat_days.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class EffectivePeriodView extends StatelessWidget { + const EffectivePeriodView({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => EffectPeriodBloc(), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: ListView( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Spacer(), + Expanded( + child: Text( + 'Effective Period', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 14), + ), + ), + const Spacer(), + ], + ), + const Divider( + color: ColorsManager.backgroundColor, + ), + const PeriodOptions( + showCustomTimePicker: EffectPeriodHelper.showCustomTimePicker, + ), + const SizedBox(height: 16), + const RepeatDays(), + const SizedBox(height: 24), + ], + ), + ), + ); + } +} diff --git a/lib/pages/routiens/widgets/period_option.dart b/lib/pages/routiens/widgets/period_option.dart new file mode 100644 index 00000000..acb69231 --- /dev/null +++ b/lib/pages/routiens/widgets/period_option.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_event.dart'; +import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_state.dart'; +import 'package:syncrow_web/pages/routiens/helper/effictive_period_helper.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/app_enum.dart'; + +class PeriodOptions extends StatelessWidget { + final Future?> Function(BuildContext) showCustomTimePicker; + + const PeriodOptions({ + required this.showCustomTimePicker, + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Column( + children: [ + _buildRadioOption(context, EnumEffectivePeriodOptions.allDay, '24 Hours'), + _buildRadioOption(context, EnumEffectivePeriodOptions.daytime, 'Sunrise to Sunset'), + _buildRadioOption(context, EnumEffectivePeriodOptions.night, 'Sunset to Sunrise'), + ListTile( + contentPadding: EdgeInsets.zero, + onTap: () => showCustomTimePicker(context), + title: Text( + 'Custom', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 14), + ), + subtitle: state.customStartTime != null && state.customEndTime != null + ? Text( + '${"${state.customStartTime}"} - ${"${state.customEndTime}"}', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 10), + ) + : Text( + '00:00 - 23:59', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 10), + ), + trailing: Radio( + value: EnumEffectivePeriodOptions.custom, + groupValue: state.selectedPeriod, + onChanged: (value) async { + if (value != null) { + context.read().add(SetPeriod(value)); + } + showCustomTimePicker(context); + }, + ), + ), + ], + ); + }, + ); + } + + Widget _buildRadioOption( + BuildContext context, EnumEffectivePeriodOptions value, String subtitle) { + return BlocBuilder( + builder: (context, state) { + return ListTile( + contentPadding: EdgeInsets.zero, + onTap: () { + context.read().add(SetPeriod(value)); + }, + title: Text(EffectPeriodHelper.formatEnumValue(value)), + subtitle: Text( + subtitle, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.textPrimaryColor, fontWeight: FontWeight.w400, fontSize: 10), + ), + trailing: Radio( + value: value, + groupValue: state.selectedPeriod, + onChanged: (value) { + if (value != null) { + context.read().add(SetPeriod(value)); + } + }, + ), + ); + }, + ); + } +} diff --git a/lib/pages/routiens/widgets/repeat_days.dart b/lib/pages/routiens/widgets/repeat_days.dart new file mode 100644 index 00000000..8ee92367 --- /dev/null +++ b/lib/pages/routiens/widgets/repeat_days.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_event.dart'; +import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_state.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class RepeatDays extends StatelessWidget { + const RepeatDays({super.key}); + + @override + Widget build(BuildContext context) { + final effectiveBloc = context.read(); + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text('Repeat', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.textPrimaryColor, fontWeight: FontWeight.w400, fontSize: 14)), + const SizedBox(width: 8), + BlocBuilder( + builder: (context, state) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: effectiveBloc.daysMap.entries.map((entry) { + final day = entry.key; + final abbreviation = entry.value; + final dayIndex = effectiveBloc.getDayIndex(day); + final isSelected = state.selectedDaysBinary[dayIndex] == '1'; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 3.0), + child: GestureDetector( + onTap: () { + effectiveBloc.add(ToggleDay(day)); + }, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: isSelected ? Colors.grey : Colors.grey.shade300, + width: 1, + ), + ), + child: CircleAvatar( + radius: 15, + backgroundColor: Colors.white, + child: Text( + abbreviation, + style: TextStyle( + fontSize: 16, + color: isSelected ? Colors.grey : Colors.grey.shade300, + ), + ), + ), + ), + ), + ); + }).toList(), + ), + const SizedBox( + height: 8, + ), + if (state.selectedDaysBinary == '0000000') + Text( + 'At least one day must be selected', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 14), + ), + ], + ); + }, + ), + ], + ); + } +} diff --git a/lib/pages/routiens/widgets/routine_search_and_buttons.dart b/lib/pages/routiens/widgets/routine_search_and_buttons.dart index 1acce727..598905e1 100644 --- a/lib/pages/routiens/widgets/routine_search_and_buttons.dart +++ b/lib/pages/routiens/widgets/routine_search_and_buttons.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/common/buttons/default_button.dart'; import 'package:syncrow_web/pages/common/text_field/custom_text_field.dart'; +import 'package:syncrow_web/pages/routiens/helper/setting_helper.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/style.dart'; @@ -26,8 +27,8 @@ class RoutineSearchAndButtons extends StatelessWidget { crossAxisAlignment: WrapCrossAlignment.end, children: [ ConstrainedBox( - constraints: - BoxConstraints(maxWidth: constraints.maxWidth > 700 ? 450 : constraints.maxWidth - 32), + constraints: BoxConstraints( + maxWidth: constraints.maxWidth > 700 ? 450 : constraints.maxWidth - 32), child: StatefulTextField( title: 'Routine Name', height: 40, @@ -46,7 +47,10 @@ class RoutineSearchAndButtons extends StatelessWidget { width: 200, child: Center( child: DefaultButton( - onPressed: () {}, + onPressed: () async { + final result = await SettingHelper.showSettingDialog( + context: context, isAutomation: true); + }, borderRadius: 15, elevation: 0, borderColor: ColorsManager.greyColor, diff --git a/lib/services/routines_api.dart b/lib/services/routines_api.dart index edf865a3..7b001751 100644 --- a/lib/services/routines_api.dart +++ b/lib/services/routines_api.dart @@ -1,3 +1,4 @@ +import 'package:syncrow_web/pages/routiens/models/icon_model.dart'; import 'package:syncrow_web/pages/routiens/models/routine_model.dart'; import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/utils/constants/api_const.dart'; @@ -41,6 +42,21 @@ class SceneApi { // } // } + static Future> getIcon() async { + final response = await _httpService.get( + path: ApiEndpoints.getIconScene, + showServerMessage: false, + expectedResponseModel: (json) { + List iconsList = []; + json.forEach((element) { + iconsList.add(IconModel.fromJson(element)); + }); + return iconsList; + }, + ); + return response; + } + //get scene by unit id static Future> getScenesByUnitId(String unitId) async { diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index 759ef7d7..c7f7bccb 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -12,11 +12,13 @@ abstract class ApiEndpoints { static const String getDevices = '/visitor-password/devices'; static const String sendOnlineOneTime = '/visitor-password/temporary-password/online/one-time'; - static const String sendOnlineMultipleTime = '/visitor-password/temporary-password/online/multiple-time'; + static const String sendOnlineMultipleTime = + '/visitor-password/temporary-password/online/multiple-time'; //offline Password static const String sendOffLineOneTime = '/visitor-password/temporary-password/offline/one-time'; - static const String sendOffLineMultipleTime = '/visitor-password/temporary-password/offline/multiple-time'; + static const String sendOffLineMultipleTime = + '/visitor-password/temporary-password/offline/multiple-time'; static const String getUser = '/user/{userUuid}'; @@ -44,4 +46,5 @@ abstract class ApiEndpoints { static const String powerClamp = '/device/{powerClampUuid}/power-clamp/status'; static const String getSpaceScenes = '/scene/tap-to-run/{unitUuid}'; static const String getSpaceAutomation = '/automation/{unitUuid}'; + static const String getIconScene = '/scene/icon'; } diff --git a/lib/utils/constants/app_enum.dart b/lib/utils/constants/app_enum.dart index 4ca37d9b..feb5f55c 100644 --- a/lib/utils/constants/app_enum.dart +++ b/lib/utils/constants/app_enum.dart @@ -97,3 +97,11 @@ extension AccessStatusExtension on AccessStatus { enum TempModes { hot, cold, wind } enum FanSpeeds { auto, low, middle, high } + +enum EnumEffectivePeriodOptions { + allDay, + daytime, + night, + custom, + none, +} diff --git a/pubspec.lock b/pubspec.lock index 92a76b10..7c7270aa 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -300,18 +300,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.4" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.3" leak_tracker_testing: dependency: transitive description: @@ -348,18 +348,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.12.0" nested: dependency: transitive description: @@ -569,10 +569,18 @@ packages: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.0" + time_picker_spinner: + dependency: "direct main" + description: + name: time_picker_spinner + sha256: "53d824801d108890d22756501e7ade9db48b53dac1ec41580499dd4ebd128e3c" + url: "https://pub.dev" + source: hosted + version: "1.0.0" typed_data: dependency: transitive description: @@ -617,10 +625,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.2.1" web: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8dc6b6bc..c8f42ea5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,6 +50,7 @@ dependencies: dropdown_search: ^5.0.6 flutter_dotenv: ^5.1.0 fl_chart: ^0.69.0 + time_picker_spinner: ^1.0.0 dev_dependencies: flutter_test: From 7dccfb5a76ca44baddbc9636dacc07cedbfa195d Mon Sep 17 00:00:00 2001 From: ashrafzarkanisala Date: Sat, 23 Nov 2024 21:51:20 +0300 Subject: [PATCH 14/23] finished ac devices --- .../device_managment/ac/model/ac_model.dart | 5 +- .../batch_control_list/batch_ac_mode.dart | 14 +- .../batch_control_list/batch_fan_speed.dart | 17 +- .../ac/view/control_list/ac_mode.dart | 14 +- .../ac/view/control_list/fan_speed.dart | 17 +- .../functions_bloc/functions_bloc_bloc.dart | 126 +--- .../functions_bloc/functions_bloc_event.dart | 45 +- .../functions_bloc/functions_bloc_state.dart | 17 +- .../bloc/routine_bloc/routine_bloc.dart | 36 +- lib/pages/routiens/enum/ac_values_enum.dart | 9 - lib/pages/routiens/helper/ac_helper.dart | 250 ++++--- .../dialog_helper/device_dialog_helper.dart | 18 +- .../helper/one_gang_switch_helper.dart | 650 ++++++++++++------ .../helper/three_gang_switch_helper.dart | 2 +- .../helper/two_gang_switch_helper.dart | 2 +- lib/pages/routiens/models/ac/ac_function.dart | 90 +-- .../routiens/models/device_functions.dart | 21 +- .../gang_switches/base_switch_function.dart | 3 - .../one_gang_switch/one_gang_switch.dart | 10 - .../three_gang_switch/three_gang_switch.dart | 30 - .../two_gang_switch/two_gang_switch.dart | 24 +- lib/pages/routiens/widgets/dragable_card.dart | 109 ++- .../routiens/widgets/routine_devices.dart | 51 +- .../routiens/widgets/then_container.dart | 14 +- lib/utils/constants/app_enum.dart | 6 + 25 files changed, 810 insertions(+), 770 deletions(-) delete mode 100644 lib/pages/routiens/enum/ac_values_enum.dart diff --git a/lib/pages/device_managment/ac/model/ac_model.dart b/lib/pages/device_managment/ac/model/ac_model.dart index 2803e51e..1eb2145f 100644 --- a/lib/pages/device_managment/ac/model/ac_model.dart +++ b/lib/pages/device_managment/ac/model/ac_model.dart @@ -1,8 +1,5 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; - -enum TempModes { hot, cold, wind } - -enum FanSpeeds { auto, low, middle, high } +import 'package:syncrow_web/utils/constants/app_enum.dart'; class AcStatusModel { final String uuid; diff --git a/lib/pages/device_managment/ac/view/batch_control_list/batch_ac_mode.dart b/lib/pages/device_managment/ac/view/batch_control_list/batch_ac_mode.dart index 60d48256..81f0685d 100644 --- a/lib/pages/device_managment/ac/view/batch_control_list/batch_ac_mode.dart +++ b/lib/pages/device_managment/ac/view/batch_control_list/batch_ac_mode.dart @@ -3,9 +3,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_bloc.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_event.dart'; -import 'package:syncrow_web/pages/device_managment/ac/model/ac_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/app_enum.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; class BatchAcMode extends StatelessWidget { @@ -27,15 +27,19 @@ class BatchAcMode extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - _buildIconContainer(context, TempModes.cold, Assets.freezing, value == TempModes.cold), - _buildIconContainer(context, TempModes.hot, Assets.acSun, value == TempModes.hot), - _buildIconContainer(context, TempModes.wind, Assets.acAirConditioner, value == TempModes.wind), + _buildIconContainer(context, TempModes.cold, Assets.freezing, + value == TempModes.cold), + _buildIconContainer( + context, TempModes.hot, Assets.acSun, value == TempModes.hot), + _buildIconContainer(context, TempModes.wind, Assets.acAirConditioner, + value == TempModes.wind), ], ), ); } - Widget _buildIconContainer(BuildContext context, TempModes mode, String assetPath, bool isSelected) { + Widget _buildIconContainer( + BuildContext context, TempModes mode, String assetPath, bool isSelected) { return Flexible( child: GestureDetector( onTap: () { diff --git a/lib/pages/device_managment/ac/view/batch_control_list/batch_fan_speed.dart b/lib/pages/device_managment/ac/view/batch_control_list/batch_fan_speed.dart index ba49047a..4d7eb449 100644 --- a/lib/pages/device_managment/ac/view/batch_control_list/batch_fan_speed.dart +++ b/lib/pages/device_managment/ac/view/batch_control_list/batch_fan_speed.dart @@ -3,9 +3,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_bloc.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_event.dart'; -import 'package:syncrow_web/pages/device_managment/ac/model/ac_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/app_enum.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; class BatchFanSpeedControl extends StatelessWidget { @@ -30,8 +30,10 @@ class BatchFanSpeedControl extends StatelessWidget { runSpacing: 8, spacing: 8, children: [ - _buildIconContainer(context, FanSpeeds.auto, Assets.acFanAuto, value == FanSpeeds.auto), - _buildIconContainer(context, FanSpeeds.low, Assets.acFanLow, value == FanSpeeds.low), + _buildIconContainer(context, FanSpeeds.auto, Assets.acFanAuto, + value == FanSpeeds.auto), + _buildIconContainer(context, FanSpeeds.low, Assets.acFanLow, + value == FanSpeeds.low), ], ), const SizedBox(height: 8), @@ -39,8 +41,10 @@ class BatchFanSpeedControl extends StatelessWidget { runSpacing: 8, spacing: 8, children: [ - _buildIconContainer(context, FanSpeeds.middle, Assets.acFanMiddle, value == FanSpeeds.middle), - _buildIconContainer(context, FanSpeeds.high, Assets.acFanHigh, value == FanSpeeds.high), + _buildIconContainer(context, FanSpeeds.middle, Assets.acFanMiddle, + value == FanSpeeds.middle), + _buildIconContainer(context, FanSpeeds.high, Assets.acFanHigh, + value == FanSpeeds.high), ], ) ], @@ -48,7 +52,8 @@ class BatchFanSpeedControl extends StatelessWidget { ); } - Widget _buildIconContainer(BuildContext context, FanSpeeds speed, String assetPath, bool isSelected) { + Widget _buildIconContainer(BuildContext context, FanSpeeds speed, + String assetPath, bool isSelected) { return GestureDetector( onTap: () { context.read().add( diff --git a/lib/pages/device_managment/ac/view/control_list/ac_mode.dart b/lib/pages/device_managment/ac/view/control_list/ac_mode.dart index c6ffc052..91ca0f8c 100644 --- a/lib/pages/device_managment/ac/view/control_list/ac_mode.dart +++ b/lib/pages/device_managment/ac/view/control_list/ac_mode.dart @@ -3,9 +3,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_bloc.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_event.dart'; -import 'package:syncrow_web/pages/device_managment/ac/model/ac_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/app_enum.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; class AcMode extends StatelessWidget { @@ -27,15 +27,19 @@ class AcMode extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - _buildIconContainer(context, TempModes.cold, Assets.freezing, value == TempModes.cold), - _buildIconContainer(context, TempModes.hot, Assets.acSun, value == TempModes.hot), - _buildIconContainer(context, TempModes.wind, Assets.acAirConditioner, value == TempModes.wind), + _buildIconContainer(context, TempModes.cold, Assets.freezing, + value == TempModes.cold), + _buildIconContainer( + context, TempModes.hot, Assets.acSun, value == TempModes.hot), + _buildIconContainer(context, TempModes.wind, Assets.acAirConditioner, + value == TempModes.wind), ], ), ); } - Widget _buildIconContainer(BuildContext context, TempModes mode, String assetPath, bool isSelected) { + Widget _buildIconContainer( + BuildContext context, TempModes mode, String assetPath, bool isSelected) { return Flexible( child: GestureDetector( onTap: () { diff --git a/lib/pages/device_managment/ac/view/control_list/fan_speed.dart b/lib/pages/device_managment/ac/view/control_list/fan_speed.dart index 952e112b..09ca80cb 100644 --- a/lib/pages/device_managment/ac/view/control_list/fan_speed.dart +++ b/lib/pages/device_managment/ac/view/control_list/fan_speed.dart @@ -3,9 +3,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_bloc.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_event.dart'; -import 'package:syncrow_web/pages/device_managment/ac/model/ac_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/app_enum.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; class FanSpeedControl extends StatelessWidget { @@ -29,8 +29,10 @@ class FanSpeedControl extends StatelessWidget { runSpacing: 8, spacing: 8, children: [ - _buildIconContainer(context, FanSpeeds.auto, Assets.acFanAuto, value == FanSpeeds.auto), - _buildIconContainer(context, FanSpeeds.low, Assets.acFanLow, value == FanSpeeds.low), + _buildIconContainer(context, FanSpeeds.auto, Assets.acFanAuto, + value == FanSpeeds.auto), + _buildIconContainer(context, FanSpeeds.low, Assets.acFanLow, + value == FanSpeeds.low), ], ), const SizedBox(height: 8), @@ -38,8 +40,10 @@ class FanSpeedControl extends StatelessWidget { runSpacing: 8, spacing: 8, children: [ - _buildIconContainer(context, FanSpeeds.middle, Assets.acFanMiddle, value == FanSpeeds.middle), - _buildIconContainer(context, FanSpeeds.high, Assets.acFanHigh, value == FanSpeeds.high), + _buildIconContainer(context, FanSpeeds.middle, Assets.acFanMiddle, + value == FanSpeeds.middle), + _buildIconContainer(context, FanSpeeds.high, Assets.acFanHigh, + value == FanSpeeds.high), ], ) ], @@ -47,7 +51,8 @@ class FanSpeedControl extends StatelessWidget { ); } - Widget _buildIconContainer(BuildContext context, FanSpeeds speed, String assetPath, bool isSelected) { + Widget _buildIconContainer(BuildContext context, FanSpeeds speed, + String assetPath, bool isSelected) { return GestureDetector( onTap: () { context.read().add( diff --git a/lib/pages/routiens/bloc/functions_bloc/functions_bloc_bloc.dart b/lib/pages/routiens/bloc/functions_bloc/functions_bloc_bloc.dart index 54b47cad..760d5697 100644 --- a/lib/pages/routiens/bloc/functions_bloc/functions_bloc_bloc.dart +++ b/lib/pages/routiens/bloc/functions_bloc/functions_bloc_bloc.dart @@ -1,6 +1,7 @@ +import 'dart:async'; + import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; part 'functions_bloc_event.dart'; @@ -10,120 +11,32 @@ class FunctionBloc extends Bloc { FunctionBloc() : super(const FunctionBlocState()) { on(_onInitializeFunctions); on(_onAddFunction); - on(_onUpdateFunctions); - on(_onUpdateFunctionValue); - on(_onUpdateFunctionCondition); - on(_onRemoveFunction); + on(_onSelectFunction); } - void _onAddFunction(AddFunction event, Emitter emit) { - debugPrint('Adding function: ${event.functionData.function}'); - final functions = List.from(state.functions); - - // Find existing function data + final functions = List.from(state.addedFunctions); final existingIndex = functions.indexWhere( - (f) => f.function == event.functionData.function, + (f) => f.functionCode == event.functionData.functionCode, ); - // If function exists, preserve its value and condition if (existingIndex != -1) { final existingData = functions[existingIndex]; functions[existingIndex] = DeviceFunctionData( entityId: event.functionData.entityId, - function: event.functionData.function, + functionCode: event.functionData.functionCode, operationName: event.functionData.operationName, - value: existingData.value, // Preserve the existing value - condition: existingData.condition, // Preserve the existing condition + value: event.functionData.value ?? existingData.value, + valueDescription: event.functionData.valueDescription ?? + existingData.valueDescription, + condition: event.functionData.condition ?? existingData.condition, ); } else { functions.add(event.functionData); } - debugPrint('Functions after add: $functions'); emit(state.copyWith( - functions: functions, - selectedFunction: event.functionData.function, - )); - } - - void _onUpdateFunctions( - UpdateFunction event, Emitter emit) { - final functions = state.functions.map((data) { - return data.function == event.functionData.function - ? event.functionData - : data; - }).toList(); - - emit(state.copyWith(functions: functions)); - } - - void _onUpdateFunctionValue( - UpdateFunctionValue event, - Emitter emit, - ) { - debugPrint('Updating function value: ${event.function} -> ${event.value}'); - - // Create a new list to ensure state immutability - final functions = List.from(state.functions); - - // Find the index of the function to update - final functionIndex = functions.indexWhere( - (data) => data.function == event.function, - ); - - if (functionIndex != -1) { - // Update the existing function data while preserving other fields - final existingData = functions[functionIndex]; - functions[functionIndex] = DeviceFunctionData( - entityId: existingData.entityId, - function: existingData.function, - operationName: existingData.operationName, - value: event.value, - condition: existingData.condition, - ); - } else { - // If function doesn't exist, add it - functions.add(DeviceFunctionData( - entityId: '', - function: event.function, - operationName: '', - value: event.value, - )); - } - - debugPrint('Functions after update: $functions'); - emit(state.copyWith(functions: functions)); - } - - void _onUpdateFunctionCondition( - UpdateFunctionCondition event, - Emitter emit, - ) { - final functions = state.functions.map((data) { - if (data.function == event.function) { - return DeviceFunctionData( - entityId: data.entityId, - function: data.function, - operationName: data.operationName, - value: data.value, - condition: event.condition, - ); - } - return data; - }).toList(); - - emit(state.copyWith(functions: functions)); - } - - void _onRemoveFunction( - RemoveFunction event, Emitter emit) { - final functions = state.functions - .where((data) => data.function != event.functionCode) - .toList(); - - emit(state.copyWith( - functions: functions, - selectedFunction: functions.isEmpty ? null : state.selectedFunction, + addedFunctions: functions, + selectedFunction: event.functionData.functionCode, )); } @@ -131,18 +44,25 @@ class FunctionBloc extends Bloc { InitializeFunctions event, Emitter emit, ) { - emit(state.copyWith(functions: event.functions)); + emit(state.copyWith(addedFunctions: event.functions)); } DeviceFunctionData? getFunction(String functionCode) { - return state.functions.firstWhere( - (data) => data.function == functionCode, + return state.addedFunctions.firstWhere( + (data) => data.functionCode == functionCode, orElse: () => DeviceFunctionData( entityId: '', - function: functionCode, + functionCode: functionCode, operationName: '', value: null, ), ); } + + FutureOr _onSelectFunction( + SelectFunction event, Emitter emit) { + emit(state.copyWith( + selectedFunction: event.functionCode, + selectedOperationName: event.operationName)); + } } diff --git a/lib/pages/routiens/bloc/functions_bloc/functions_bloc_event.dart b/lib/pages/routiens/bloc/functions_bloc/functions_bloc_event.dart index cceb6d21..85ba62f4 100644 --- a/lib/pages/routiens/bloc/functions_bloc/functions_bloc_event.dart +++ b/lib/pages/routiens/bloc/functions_bloc/functions_bloc_event.dart @@ -18,48 +18,17 @@ class AddFunction extends FunctionBlocEvent { List get props => [functionData]; } -class UpdateFunction extends FunctionBlocEvent { - final DeviceFunctionData functionData; - - const UpdateFunction(this.functionData); - - @override - List get props => [functionData]; -} - -class UpdateFunctionValue extends FunctionBlocEvent { - final String function; - final dynamic value; - - const UpdateFunctionValue({ - required this.function, - required this.value, - }); - - @override - List get props => [function, value]; -} - -class UpdateFunctionCondition extends FunctionBlocEvent { - final String function; - final String condition; - - const UpdateFunctionCondition({ - required this.function, - required this.condition, - }); - - @override - List get props => [function, condition]; -} - -class RemoveFunction extends FunctionBlocEvent { +class SelectFunction extends FunctionBlocEvent { final String functionCode; + final String operationName; - const RemoveFunction(this.functionCode); + const SelectFunction({ + required this.functionCode, + required this.operationName, + }); @override - List get props => [functionCode]; + List get props => [functionCode, operationName]; } class InitializeFunctions extends FunctionBlocEvent { diff --git a/lib/pages/routiens/bloc/functions_bloc/functions_bloc_state.dart b/lib/pages/routiens/bloc/functions_bloc/functions_bloc_state.dart index af854b8a..2c6cd941 100644 --- a/lib/pages/routiens/bloc/functions_bloc/functions_bloc_state.dart +++ b/lib/pages/routiens/bloc/functions_bloc/functions_bloc_state.dart @@ -1,24 +1,29 @@ part of 'functions_bloc_bloc.dart'; class FunctionBlocState extends Equatable { - final List functions; + final List addedFunctions; final String? selectedFunction; - + final String? selectedOperationName; const FunctionBlocState({ - this.functions = const [], + this.addedFunctions = const [], this.selectedFunction, + this.selectedOperationName, }); FunctionBlocState copyWith({ - List? functions, + List? addedFunctions, String? selectedFunction, + String? selectedOperationName, }) { return FunctionBlocState( - functions: functions ?? this.functions, + addedFunctions: addedFunctions ?? this.addedFunctions, selectedFunction: selectedFunction ?? this.selectedFunction, + selectedOperationName: + selectedOperationName ?? this.selectedOperationName, ); } @override - List get props => [functions, selectedFunction]; + List get props => + [addedFunctions, selectedFunction, selectedOperationName]; } diff --git a/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart b/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart index 20a3c393..2b711f39 100644 --- a/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart +++ b/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart @@ -1,5 +1,6 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; import 'package:syncrow_web/pages/routiens/models/routine_model.dart'; import 'package:syncrow_web/services/routines_api.dart'; @@ -21,32 +22,33 @@ class RoutineBloc extends Bloc { } void _onAddToIfContainer(AddToIfContainer event, Emitter emit) { - if (!_isDuplicate(state.ifItems, event.item)) { - final updatedIfItems = List>.from(state.ifItems) - ..add(event.item); - emit(state.copyWith(ifItems: updatedIfItems)); - } + // if (!_isDuplicate(state.ifItems, event.item)) { + final updatedIfItems = List>.from(state.ifItems) + ..add(event.item); + emit(state.copyWith(ifItems: updatedIfItems)); + // } } void _onAddToThenContainer( AddToThenContainer event, Emitter emit) { - if (!_isDuplicate(state.thenItems, event.item)) { - final updatedThenItems = List>.from(state.thenItems) - ..add(event.item); - emit(state.copyWith(thenItems: updatedThenItems)); - } + // if (!_isDuplicate(state.thenItems, event.item)) { + final updatedThenItems = List>.from(state.thenItems) + ..add(event.item); + emit(state.copyWith(thenItems: updatedThenItems)); + // } } void _onAddFunction(AddFunctionToRoutine event, Emitter emit) { final functions = List.from(state.selectedFunctions); functions.add(event.function); + debugPrint("******" + functions.toString()); emit(state.copyWith(selectedFunctions: functions)); } void _onRemoveFunction(RemoveFunction event, Emitter emit) { final functions = List.from(state.selectedFunctions) ..removeWhere((f) => - f.function == event.function.function && + f.functionCode == event.function.functionCode && f.value == event.function.value); emit(state.copyWith(selectedFunctions: functions)); } @@ -55,12 +57,12 @@ class RoutineBloc extends Bloc { emit(state.copyWith(selectedFunctions: [])); } - bool _isDuplicate( - List> items, Map newItem) { - return items.any((item) => - item['imagePath'] == newItem['imagePath'] && - item['title'] == newItem['title']); - } + // bool _isDuplicate( + // List> items, Map newItem) { + // return items.any((item) => + // item['imagePath'] == newItem['imagePath'] && + // item['title'] == newItem['title']); + // } Future _onLoadScenes( LoadScenes event, Emitter emit) async { diff --git a/lib/pages/routiens/enum/ac_values_enum.dart b/lib/pages/routiens/enum/ac_values_enum.dart deleted file mode 100644 index e7d0f865..00000000 --- a/lib/pages/routiens/enum/ac_values_enum.dart +++ /dev/null @@ -1,9 +0,0 @@ -enum AcValuesEnums { - Cooling, - Heating, - Ventilation, -} - -enum TempModes { hot, cold, wind } - -enum FanSpeeds { auto, low, middle, high } diff --git a/lib/pages/routiens/helper/ac_helper.dart b/lib/pages/routiens/helper/ac_helper.dart index 4a42c43d..9ba02633 100644 --- a/lib/pages/routiens/helper/ac_helper.dart +++ b/lib/pages/routiens/helper/ac_helper.dart @@ -1,7 +1,9 @@ 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/routiens/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/models/ac/ac_function.dart'; +import 'package:syncrow_web/pages/routiens/models/ac/ac_operational_value.dart'; import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart'; import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart'; @@ -13,42 +15,32 @@ import 'package:syncrow_web/pages/routiens/bloc/functions_bloc/functions_bloc_bl class ACHelper { static Future?> showACFunctionsDialog( BuildContext context, - List> functions, + List functions, + AllDevicesModel? device, + List? deviceSelectedFunctions, ) async { List acFunctions = functions.whereType().toList(); - // Initialize the FunctionBloc with existing functions - final initialFunctions = acFunctions - .map((f) => DeviceFunctionData( - entityId: '', - function: f.code, - operationName: f.operationName, - value: null, - )) - .toList(); - return showDialog?>( context: context, builder: (BuildContext context) { return BlocProvider( - create: (_) => - FunctionBloc()..add(InitializeFunctions(initialFunctions)), + create: (_) => FunctionBloc() + ..add(InitializeFunctions(deviceSelectedFunctions ?? [])), child: AlertDialog( contentPadding: EdgeInsets.zero, content: BlocBuilder( builder: (context, state) { final selectedFunction = state.selectedFunction; - final selectedFunctionData = selectedFunction != null - ? state.functions.firstWhere( - (f) => f.function == selectedFunction, + final selectedOperationName = state.selectedOperationName; + final selectedFunctionData = state.addedFunctions + .firstWhere((f) => f.functionCode == selectedFunction, orElse: () => DeviceFunctionData( - entityId: '', - function: selectedFunction, - operationName: '', - value: null, - ), - ) - : null; + entityId: '', + functionCode: selectedFunction ?? '', + operationName: '', + value: null, + )); return Container( width: selectedFunction != null ? 600 : 360, @@ -70,41 +62,27 @@ class ACHelper { SizedBox( width: selectedFunction != null ? 320 : 360, child: _buildFunctionsList( - context, - acFunctions, - (functionCode) => - context.read().add( - AddFunction( - functionData: DeviceFunctionData( - entityId: '', - function: functionCode, - operationName: '', - value: null, - )), - ), + context: context, + acFunctions: acFunctions, + onFunctionSelected: + (functionCode, operationName) => context + .read() + .add(SelectFunction( + functionCode: functionCode, + operationName: operationName, + )), ), ), // Value selector if (selectedFunction != null) Expanded( child: _buildValueSelector( - context, - selectedFunction, - selectedFunctionData, - (value) => context.read().add( - UpdateFunctionValue( - function: selectedFunction, - value: value, - ), - ), - (condition) => - context.read().add( - UpdateFunctionCondition( - function: selectedFunction, - condition: condition, - ), - ), - acFunctions, + context: context, + selectedFunction: selectedFunction, + selectedFunctionData: selectedFunctionData, + acFunctions: acFunctions, + device: device, + operationName: selectedOperationName ?? '', ), ), ], @@ -114,11 +92,10 @@ class ACHelper { onCancel: () { Navigator.pop(context); }, - onConfirm: selectedFunction != null && - selectedFunctionData?.value != null + onConfirm: state.addedFunctions.isNotEmpty ? () { /// add the functions to the routine bloc - for (var function in state.functions) { + for (var function in state.addedFunctions) { context.read().add( AddFunctionToRoutine( function, @@ -126,7 +103,9 @@ class ACHelper { ); } // Return the device data to be added to the container - Navigator.pop(context); + Navigator.pop(context, { + 'deviceId': functions.first.deviceId, + }); } : null, isConfirmEnabled: selectedFunction != null, @@ -145,11 +124,11 @@ class ACHelper { } /// Build functions list for AC functions dialog - static Widget _buildFunctionsList( - BuildContext context, - List acFunctions, - Function(String) onFunctionSelected, - ) { + static Widget _buildFunctionsList({ + required BuildContext context, + required List acFunctions, + required Function(String, String) onFunctionSelected, + }) { return ListView.separated( shrinkWrap: false, physics: const AlwaysScrollableScrollPhysics(), @@ -182,29 +161,34 @@ class ACHelper { size: 16, color: ColorsManager.textGray, ), - onTap: () => onFunctionSelected(function.code), + onTap: () => onFunctionSelected( + function.code, + function.operationName, + ), ); }, ); } /// Build value selector for AC functions dialog - static Widget _buildValueSelector( - BuildContext context, - String selectedFunction, - DeviceFunctionData? selectedFunctionData, - Function(dynamic) onValueChanged, - Function(String) onConditionChanged, - List acFunctions, - ) { + static Widget _buildValueSelector({ + required BuildContext context, + required String selectedFunction, + required DeviceFunctionData? selectedFunctionData, + required List acFunctions, + AllDevicesModel? device, + required String operationName, + }) { if (selectedFunction == 'temp_set' || selectedFunction == 'temp_current') { final initialValue = selectedFunctionData?.value ?? 200; return _buildTemperatureSelector( - context, - initialValue, - selectedFunctionData?.condition, - onValueChanged, - onConditionChanged, + context: context, + initialValue: initialValue, + selectCode: selectedFunction, + currentCondition: selectedFunctionData?.condition, + device: device, + operationName: operationName, + selectedFunctionData: selectedFunctionData, ); } @@ -213,37 +197,43 @@ class ACHelper { final values = selectedFn.getOperationalValues(); return _buildOperationalValuesList( - context, - values, - selectedFunctionData?.value, - onValueChanged, + context: context, + values: values, + selectedValue: selectedFunctionData?.value, + device: device, + operationName: operationName, + selectCode: selectedFunction, + selectedFunctionData: selectedFunctionData, ); } /// Build temperature selector for AC functions dialog - static Widget _buildTemperatureSelector( - BuildContext context, - dynamic initialValue, - String? currentCondition, - Function(dynamic) onValueChanged, - Function(String) onConditionChanged, - ) { + static Widget _buildTemperatureSelector({ + required BuildContext context, + required dynamic initialValue, + required String? currentCondition, + required String selectCode, + AllDevicesModel? device, + required String operationName, + DeviceFunctionData? selectedFunctionData, + }) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildConditionToggle( context, currentCondition, - onConditionChanged, + selectCode, + device, + operationName, + selectedFunctionData, ), const SizedBox(height: 20), - _buildTemperatureDisplay(context, initialValue), + _buildTemperatureDisplay(context, initialValue, device, operationName, + selectedFunctionData, selectCode), const SizedBox(height: 20), - _buildTemperatureSlider( - context, - initialValue, - onValueChanged, - ), + _buildTemperatureSlider(context, initialValue, device, operationName, + selectedFunctionData, selectCode), ], ); } @@ -252,13 +242,28 @@ class ACHelper { static Widget _buildConditionToggle( BuildContext context, String? currentCondition, - Function(String) onConditionChanged, + String selectCode, + AllDevicesModel? device, + String operationName, + DeviceFunctionData? selectedFunctionData, + // Function(String) onConditionChanged, ) { final conditions = ["<", "==", ">"]; return ToggleButtons( onPressed: (int index) { - onConditionChanged(conditions[index]); + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + condition: conditions[index], + value: selectedFunctionData?.value, + valueDescription: selectedFunctionData?.valueDescription, + ), + ), + ); }, borderRadius: const BorderRadius.all(Radius.circular(8)), selectedBorderColor: ColorsManager.primaryColorWithOpacity, @@ -277,7 +282,12 @@ class ACHelper { /// Build temperature display for AC functions dialog static Widget _buildTemperatureDisplay( - BuildContext context, dynamic initialValue) { + BuildContext context, + dynamic initialValue, + AllDevicesModel? device, + String operationName, + DeviceFunctionData? selectedFunctionData, + String selectCode) { return Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), decoration: BoxDecoration( @@ -296,7 +306,10 @@ class ACHelper { static Widget _buildTemperatureSlider( BuildContext context, dynamic initialValue, - Function(dynamic) onValueChanged, + AllDevicesModel? device, + String operationName, + DeviceFunctionData? selectedFunctionData, + String selectCode, ) { return Slider( value: initialValue is int ? initialValue.toDouble() : 200.0, @@ -305,17 +318,32 @@ class ACHelper { divisions: 14, label: '${((initialValue ?? 200) / 10).toInt()}°C', onChanged: (value) { - onValueChanged(value.toInt()); + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + value: value, + condition: selectedFunctionData?.condition, + valueDescription: selectedFunctionData?.valueDescription, + ), + ), + ); }, ); } - static Widget _buildOperationalValuesList( - BuildContext context, - List values, - dynamic selectedValue, - Function(dynamic) onValueChanged, - ) { + static Widget _buildOperationalValuesList({ + required BuildContext context, + required List values, + required dynamic selectedValue, + AllDevicesModel? device, + required String operationName, + required String selectCode, + DeviceFunctionData? selectedFunctionData, + // required Function(dynamic) onValueChanged, + }) { return ListView.builder( shrinkWrap: false, physics: const AlwaysScrollableScrollPhysics(), @@ -349,7 +377,19 @@ class ACHelper { ), onTap: () { if (!isSelected) { - onValueChanged(value.value); + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + value: value.value, + condition: selectedFunctionData?.condition, + valueDescription: + selectedFunctionData?.valueDescription, + ), + ), + ); } }, ); diff --git a/lib/pages/routiens/helper/dialog_helper/device_dialog_helper.dart b/lib/pages/routiens/helper/dialog_helper/device_dialog_helper.dart index 2b21d83d..714feda3 100644 --- a/lib/pages/routiens/helper/dialog_helper/device_dialog_helper.dart +++ b/lib/pages/routiens/helper/dialog_helper/device_dialog_helper.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/helper/ac_helper.dart'; import 'package:syncrow_web/pages/routiens/helper/one_gang_switch_helper.dart'; import 'package:syncrow_web/pages/routiens/helper/three_gang_switch_helper.dart'; @@ -16,6 +18,7 @@ class DeviceDialogHelper { final result = await _getDialogForDeviceType( context, data['productType'], + data, functions, ); @@ -32,21 +35,30 @@ class DeviceDialogHelper { static Future?> _getDialogForDeviceType( BuildContext context, String productType, + Map data, List functions, ) async { + final routineBloc = context.read(); + final deviceSelectedFunctions = routineBloc.state.selectedFunctions + .where((f) => f.entityId == data['deviceId']) + .toList(); + switch (productType) { case 'AC': - return ACHelper.showACFunctionsDialog(context, functions); + return ACHelper.showACFunctionsDialog( + context, functions, data['device'], deviceSelectedFunctions); + case '1G': return OneGangSwitchHelper.showSwitchFunctionsDialog( - context, functions); + context, functions, data['device'], deviceSelectedFunctions); case '2G': return TwoGangSwitchHelper.showSwitchFunctionsDialog( context, functions); case '3G': return ThreeGangSwitchHelper.showSwitchFunctionsDialog( context, functions); - break; + default: + return null; } } } diff --git a/lib/pages/routiens/helper/one_gang_switch_helper.dart b/lib/pages/routiens/helper/one_gang_switch_helper.dart index 014e41f9..6cb28424 100644 --- a/lib/pages/routiens/helper/one_gang_switch_helper.dart +++ b/lib/pages/routiens/helper/one_gang_switch_helper.dart @@ -1,5 +1,11 @@ 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/routiens/bloc/functions_bloc/functions_bloc_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/models/ac/ac_function.dart'; +import 'package:syncrow_web/pages/routiens/models/ac/ac_operational_value.dart'; import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart'; import 'package:syncrow_web/pages/routiens/models/gang_switches/one_gang_switch/one_gang_switch.dart'; @@ -11,59 +17,57 @@ import 'package:syncrow_web/utils/extension/build_context_x.dart'; class OneGangSwitchHelper { static Future?> showSwitchFunctionsDialog( BuildContext context, - List> functions, + List functions, + AllDevicesModel? device, + List? deviceSelectedFunctions, ) async { - List> switchFunctions = functions - .where( - (f) => f is OneGangSwitchFunction || f is OneGangCountdownFunction) - .toList(); - final selectedFunctionNotifier = ValueNotifier(null); - final selectedValueNotifier = ValueNotifier(null); - final selectedConditionNotifier = ValueNotifier("<"); - final selectedConditionsNotifier = - ValueNotifier>([true, false, false]); + List acFunctions = functions.whereType().toList(); return showDialog?>( context: context, builder: (BuildContext context) { - return ValueListenableBuilder( - valueListenable: selectedFunctionNotifier, - builder: (context, selectedFunction, _) { - return AlertDialog( + return BlocProvider( + create: (_) => FunctionBloc() + ..add(InitializeFunctions(deviceSelectedFunctions ?? [])), + child: AlertDialog( contentPadding: EdgeInsets.zero, - content: 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('1 Gang Light Switch Condition'), - Expanded( - child: Row( - children: [ - // Left side: Function list - Expanded( - child: ListView.separated( - itemCount: switchFunctions.length, - separatorBuilder: (_, __) => const Divider( - color: ColorsManager.dividerColor, - ), - itemBuilder: (context, index) { - final function = switchFunctions[index]; - return ValueListenableBuilder( - valueListenable: selectedFunctionNotifier, - builder: (context, selectedFunction, _) { - final isSelected = - selectedFunction == function.code; + content: BlocBuilder( + builder: (context, 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, + )); + 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('1 Gang Light Switch Condition'), + Expanded( + child: Row( + children: [ + // Left side: Function list + Expanded( + child: ListView.separated( + itemCount: acFunctions.length, + separatorBuilder: (_, __) => const Divider( + color: ColorsManager.dividerColor, + ), + itemBuilder: (context, index) { + final function = acFunctions[index]; return ListTile( - tileColor: isSelected - ? Colors.grey.shade100 - : null, leading: SvgPicture.asset( function.icon, width: 24, @@ -79,191 +83,413 @@ class OneGangSwitchHelper { color: ColorsManager.textGray, ), onTap: () { - selectedFunctionNotifier.value = - function.code; - selectedValueNotifier.value = - function is OneGangCountdownFunction - ? 0 - : null; + context + .read() + .add(SelectFunction( + functionCode: function.code, + operationName: + function.operationName, + )); }, ); }, - ); - }, - ), + ), + ), + // Right side: Value selector + if (selectedFunction != null) + Expanded( + child: _buildValueSelector( + context: context, + selectedFunction: selectedFunction, + selectedFunctionData: selectedFunctionData, + acFunctions: acFunctions, + device: device, + operationName: selectedOperationName ?? '', + ), + ), + // ValueListenableBuilder( + // valueListenable: selectedFunctionNotifier, + // builder: (context, selectedFunction, _) { + // final selectedFn = + // switchFunctions.firstWhere( + // (f) => f.code == selectedFunction, + // ); + // return Expanded( + // child: selectedFn + // is OneGangCountdownFunction + // ? _buildCountDownSelector( + // context, + // selectedValueNotifier, + // selectedConditionNotifier, + // selectedConditionsNotifier, + // ) + // : _buildOperationalValuesList( + // context, + // selectedFn as BaseSwitchFunction, + // selectedValueNotifier, + // ), + // ); + // }, + // ), + ], ), - // Right side: Value selector - if (selectedFunction != null) - ValueListenableBuilder( - valueListenable: selectedFunctionNotifier, - builder: (context, selectedFunction, _) { - final selectedFn = switchFunctions.firstWhere( - (f) => f.code == selectedFunction, - ); - return Expanded( - child: selectedFn is OneGangCountdownFunction - ? _buildCountDownSelector( - context, - selectedValueNotifier, - selectedConditionNotifier, - selectedConditionsNotifier, - ) - : _buildOperationalValuesList( - context, - selectedFn as BaseSwitchFunction, - selectedValueNotifier, - ), - ); - }, - ), - ], - ), + ), + Container( + height: 1, + width: double.infinity, + color: ColorsManager.greyColor, + ), + DialogFooter( + onCancel: () { + Navigator.pop(context); + }, + onConfirm: state.addedFunctions.isNotEmpty + ? () { + /// add the functions to the routine bloc + for (var function in state.addedFunctions) { + context.read().add( + AddFunctionToRoutine( + function, + ), + ); + } + // Return the device data to be added to the container + Navigator.pop(context, { + 'deviceId': functions.first.deviceId, + }); + } + : null, + isConfirmEnabled: selectedFunction != null, + ), + ], ), - Container( - height: 1, - width: double.infinity, - color: ColorsManager.greyColor, - ), - DialogFooter( - onCancel: () => Navigator.pop(context), - onConfirm: selectedFunctionNotifier.value != null && - selectedValueNotifier.value != null - ? () { - final selectedFn = switchFunctions.firstWhere( - (f) => f.code == selectedFunctionNotifier.value, - ); - final value = selectedValueNotifier.value; - final functionData = DeviceFunctionData( - entityId: selectedFn.deviceId, - function: selectedFn.code, - operationName: selectedFn.operationName, - value: value, - condition: selectedConditionNotifier.value, - valueDescription: - selectedFn is OneGangCountdownFunction - ? '${value} sec' - : ((selectedFn as BaseSwitchFunction) - .getOperationalValues() - .firstWhere((v) => v.value == value) - .description), - ); - Navigator.pop( - context, {selectedFn.code: functionData}); - } - : null, - isConfirmEnabled: selectedFunctionNotifier.value != null, - ), - ], + ); + }, + ), + )); + }, + ); + } + + static Widget _buildValueSelector({ + required BuildContext context, + required String selectedFunction, + required DeviceFunctionData? selectedFunctionData, + required List acFunctions, + AllDevicesModel? device, + required String operationName, + }) { + if (selectedFunction == 'countdown_1') { + final initialValue = selectedFunctionData?.value ?? 200; + return _buildTemperatureSelector( + context: context, + initialValue: initialValue, + selectCode: selectedFunction, + currentCondition: selectedFunctionData?.condition, + device: device, + operationName: operationName, + selectedFunctionData: selectedFunctionData, + ); + } + + final selectedFn = + acFunctions.firstWhere((f) => f.code == selectedFunction); + final values = selectedFn.getOperationalValues(); + + return _buildOperationalValuesList( + context: context, + values: values, + selectedValue: selectedFunctionData?.value, + device: device, + operationName: operationName, + selectCode: selectedFunction, + selectedFunctionData: selectedFunctionData, + ); + } + + static Widget _buildTemperatureSelector({ + required BuildContext context, + required dynamic initialValue, + required String? currentCondition, + required String selectCode, + AllDevicesModel? device, + required String operationName, + DeviceFunctionData? selectedFunctionData, + }) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildConditionToggle( + context, + currentCondition, + selectCode, + device, + operationName, + selectedFunctionData, + ), + const SizedBox(height: 20), + _buildTemperatureDisplay(context, initialValue, device, operationName, + selectedFunctionData, selectCode), + const SizedBox(height: 20), + _buildTemperatureSlider(context, initialValue, device, operationName, + selectedFunctionData, selectCode), + ], + ); + } + + /// Build condition toggle for AC functions dialog + static Widget _buildConditionToggle( + BuildContext context, + String? currentCondition, + String selectCode, + AllDevicesModel? device, + String operationName, + DeviceFunctionData? selectedFunctionData, + // Function(String) onConditionChanged, + ) { + final conditions = ["<", "==", ">"]; + + return ToggleButtons( + onPressed: (int index) { + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + condition: conditions[index], + value: selectedFunctionData?.value, + valueDescription: selectedFunctionData?.valueDescription, ), ), ); - }, - ); - }, - ).then((value) { - selectedFunctionNotifier.dispose(); - selectedValueNotifier.dispose(); - selectedConditionNotifier.dispose(); - selectedConditionsNotifier.dispose(); - return value; - }); - } - - static Widget _buildCountDownSelector( - BuildContext context, - ValueNotifier valueNotifier, - ValueNotifier conditionNotifier, - ValueNotifier> conditionsNotifier, - ) { - return ValueListenableBuilder( - valueListenable: valueNotifier, - builder: (context, value, _) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ValueListenableBuilder>( - valueListenable: conditionsNotifier, - builder: (context, selectedConditions, _) { - return ToggleButtons( - onPressed: (int index) { - final newConditions = List.filled(3, false); - newConditions[index] = true; - conditionsNotifier.value = newConditions; - conditionNotifier.value = index == 0 - ? "<" - : index == 1 - ? "==" - : ">"; - }, - 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: selectedConditions, - children: const [Text("<"), Text("="), Text(">")], - ); - }, - ), - const SizedBox(height: 20), - Text( - '${value ?? 0} sec', - style: Theme.of(context).textTheme.headlineMedium, - ), - const SizedBox(height: 20), - Slider( - value: (value ?? 0).toDouble(), - min: 0, - max: 300, - divisions: 300, - onChanged: (newValue) { - valueNotifier.value = newValue.toInt(); - }, - ), - ], - ); }, + 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(), ); } - static Widget _buildOperationalValuesList( + /// Build temperature display for AC functions dialog + static Widget _buildTemperatureDisplay( + BuildContext context, + dynamic initialValue, + AllDevicesModel? device, + String operationName, + DeviceFunctionData? selectedFunctionData, + String selectCode) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + decoration: BoxDecoration( + color: ColorsManager.primaryColorWithOpacity.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + '${initialValue ?? 0} sec', + style: context.textTheme.headlineMedium!.copyWith( + color: ColorsManager.primaryColorWithOpacity, + ), + ), + ); + } + + static Widget _buildTemperatureSlider( BuildContext context, - BaseSwitchFunction function, - ValueNotifier valueNotifier, + dynamic initialValue, + AllDevicesModel? device, + String operationName, + DeviceFunctionData? selectedFunctionData, + String selectCode, ) { - final values = function.getOperationalValues(); - return ValueListenableBuilder( - valueListenable: valueNotifier, - builder: (context, selectedValue, _) { - return ListView.builder( - itemCount: values.length, - itemBuilder: (context, index) { - final value = values[index]; - return ListTile( - leading: SvgPicture.asset( - value.icon, - width: 24, - height: 24, - ), - title: Text( - value.description, - style: context.textTheme.bodyMedium, - ), - trailing: Radio( - value: value.value, - groupValue: selectedValue, - onChanged: (newValue) { - valueNotifier.value = newValue; - }, + return Slider( + value: (initialValue ?? 0).toDouble(), + min: 0, + max: 300, + divisions: 300, + onChanged: (value) { + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + value: value, + condition: selectedFunctionData?.condition, + valueDescription: selectedFunctionData?.valueDescription, + ), ), ); + }, + ); + } + + static Widget _buildOperationalValuesList({ + required BuildContext context, + required List values, + required dynamic selectedValue, + AllDevicesModel? device, + required String operationName, + required String selectCode, + DeviceFunctionData? selectedFunctionData, + // required Function(dynamic) onValueChanged, + }) { + return ListView.builder( + shrinkWrap: false, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: values.length, + itemBuilder: (context, index) { + final value = values[index]; + final isSelected = selectedValue == value.value; + return ListTile( + leading: SvgPicture.asset( + value.icon, + width: 24, + height: 24, + placeholderBuilder: (BuildContext context) => Container( + width: 24, + height: 24, + color: Colors.transparent, + ), + ), + title: Text( + value.description, + style: context.textTheme.bodyMedium, + ), + trailing: Icon( + isSelected + ? Icons.radio_button_checked + : Icons.radio_button_unchecked, + size: 24, + color: isSelected + ? ColorsManager.primaryColorWithOpacity + : ColorsManager.textGray, + ), + onTap: () { + if (!isSelected) { + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + value: value.value, + condition: selectedFunctionData?.condition, + valueDescription: + selectedFunctionData?.valueDescription, + ), + ), + ); + } }, ); }, ); } + + // static Widget _buildCountDownSelector( + // BuildContext context, + // ValueNotifier valueNotifier, + // ValueNotifier conditionNotifier, + // ValueNotifier> conditionsNotifier, + // ) { + // return ValueListenableBuilder( + // valueListenable: valueNotifier, + // builder: (context, value, _) { + // return Column( + // mainAxisAlignment: MainAxisAlignment.center, + // children: [ + // ValueListenableBuilder>( + // valueListenable: conditionsNotifier, + // builder: (context, selectedConditions, _) { + // return ToggleButtons( + // onPressed: (int index) { + // final newConditions = List.filled(3, false); + // newConditions[index] = true; + // conditionsNotifier.value = newConditions; + // conditionNotifier.value = index == 0 + // ? "<" + // : index == 1 + // ? "==" + // : ">"; + // }, + // 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: selectedConditions, + // children: const [Text("<"), Text("="), Text(">")], + // ); + // }, + // ), + // const SizedBox(height: 20), + // Text( + // '${value ?? 0} sec', + // style: Theme.of(context).textTheme.headlineMedium, + // ), + // const SizedBox(height: 20), + // Slider( + // value: (value ?? 0).toDouble(), + // min: 0, + // max: 300, + // divisions: 300, + // onChanged: (newValue) { + // valueNotifier.value = newValue.toInt(); + // }, + // ), + // ], + // ); + // }, + // ); + // } + + // static Widget _buildOperationalValuesList( + // BuildContext context, + // BaseSwitchFunction function, + // ValueNotifier valueNotifier, + // ) { + // final values = function.getOperationalValues(); + // return ValueListenableBuilder( + // valueListenable: valueNotifier, + // builder: (context, selectedValue, _) { + // return ListView.builder( + // itemCount: values.length, + // itemBuilder: (context, index) { + // final value = values[index]; + // return ListTile( + // leading: SvgPicture.asset( + // value.icon, + // width: 24, + // height: 24, + // ), + // title: Text( + // value.description, + // style: context.textTheme.bodyMedium, + // ), + // trailing: Radio( + // value: value.value, + // groupValue: selectedValue, + // onChanged: (newValue) { + // valueNotifier.value = newValue; + // }, + // ), + // ); + // }, + // ); + // }, + // ); + // } } diff --git a/lib/pages/routiens/helper/three_gang_switch_helper.dart b/lib/pages/routiens/helper/three_gang_switch_helper.dart index ecbe81f2..502b302e 100644 --- a/lib/pages/routiens/helper/three_gang_switch_helper.dart +++ b/lib/pages/routiens/helper/three_gang_switch_helper.dart @@ -143,7 +143,7 @@ class ThreeGangSwitchHelper { final value = selectedValueNotifier.value; final functionData = DeviceFunctionData( entityId: selectedFn.deviceId, - function: selectedFn.code, + functionCode: selectedFn.code, operationName: selectedFn.operationName, value: value, condition: selectedConditionNotifier.value, diff --git a/lib/pages/routiens/helper/two_gang_switch_helper.dart b/lib/pages/routiens/helper/two_gang_switch_helper.dart index f0e3026a..038ee345 100644 --- a/lib/pages/routiens/helper/two_gang_switch_helper.dart +++ b/lib/pages/routiens/helper/two_gang_switch_helper.dart @@ -138,7 +138,7 @@ class TwoGangSwitchHelper { final value = selectedValueNotifier.value; final functionData = DeviceFunctionData( entityId: selectedFn.deviceId, - function: selectedFn.code, + functionCode: selectedFn.code, operationName: selectedFn.operationName, value: value, condition: selectedConditionNotifier.value, diff --git a/lib/pages/routiens/models/ac/ac_function.dart b/lib/pages/routiens/models/ac/ac_function.dart index 3afe74c6..e5fa78ba 100644 --- a/lib/pages/routiens/models/ac/ac_function.dart +++ b/lib/pages/routiens/models/ac/ac_function.dart @@ -1,25 +1,17 @@ import 'package:syncrow_web/pages/device_managment/ac/model/ac_model.dart'; import 'package:syncrow_web/pages/routiens/models/ac/ac_operational_value.dart'; import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; +import 'package:syncrow_web/utils/constants/app_enum.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; abstract class ACFunction extends DeviceFunction { ACFunction({ - required String deviceId, - required String deviceName, - required String code, - required String operationName, - required String icon, - }) : super( - deviceId: deviceId, - deviceName: deviceName, - code: code, - operationName: operationName, - icon: icon, - ); - - @override - AcStatusModel execute(AcStatusModel currentStatus, dynamic newValue); + required super.deviceId, + required super.deviceName, + required super.code, + required super.operationName, + required super.icon, + }); List getOperationalValues(); } @@ -32,11 +24,6 @@ class SwitchFunction extends ACFunction { icon: Assets.assetsAcPower, ); - @override - AcStatusModel execute(AcStatusModel currentStatus, dynamic newValue) { - return currentStatus.copyWith(acSwitch: newValue as bool); - } - @override List getOperationalValues() => [ ACOperationalValue( @@ -60,27 +47,22 @@ class ModeFunction extends ACFunction { icon: Assets.assetsFreezing, ); - @override - AcStatusModel execute(AcStatusModel currentStatus, dynamic newValue) { - return currentStatus.copyWith(modeString: (newValue as TempModes).toString().split('.').last); - } - @override List getOperationalValues() => [ ACOperationalValue( icon: Assets.assetsAcCooling, description: "Cooling", - value: TempModes.cold, + value: TempModes.cold.name, ), ACOperationalValue( icon: Assets.assetsAcHeating, description: "Heating", - value: TempModes.hot, + value: TempModes.hot.name, ), ACOperationalValue( icon: Assets.assetsFanSpeed, description: "Ventilation", - value: TempModes.wind, + value: TempModes.wind.name, ), ]; } @@ -100,11 +82,6 @@ class TempSetFunction extends ACFunction { icon: Assets.assetsTempreture, ); - @override - AcStatusModel execute(AcStatusModel currentStatus, dynamic newValue) { - return currentStatus.copyWith(tempSet: newValue as int); - } - @override List getOperationalValues() { List values = []; @@ -127,32 +104,27 @@ class LevelFunction extends ACFunction { icon: Assets.assetsFanSpeed, ); - @override - AcStatusModel execute(AcStatusModel currentStatus, dynamic newValue) { - return currentStatus.copyWith(fanSpeedsString: (newValue as FanSpeeds).toString().split('.').last); - } - @override List getOperationalValues() => [ ACOperationalValue( icon: Assets.assetsAcFanLow, description: "LOW", - value: FanSpeeds.low, + value: FanSpeeds.low.name, ), ACOperationalValue( icon: Assets.assetsAcFanMiddle, description: "MIDDLE", - value: FanSpeeds.middle, + value: FanSpeeds.middle.name, ), ACOperationalValue( icon: Assets.assetsAcFanHigh, description: "HIGH", - value: FanSpeeds.high, + value: FanSpeeds.high.name, ), ACOperationalValue( icon: Assets.assetsAcFanAuto, description: "AUTO", - value: FanSpeeds.auto, + value: FanSpeeds.auto.name, ), ]; } @@ -165,11 +137,6 @@ class ChildLockFunction extends ACFunction { icon: Assets.assetsChildLock, ); - @override - AcStatusModel execute(AcStatusModel currentStatus, dynamic newValue) { - return currentStatus.copyWith(childLock: newValue as bool); - } - @override List getOperationalValues() => [ ACOperationalValue( @@ -184,32 +151,3 @@ class ChildLockFunction extends ACFunction { ), ]; } - -/* - - final deviceId = 'AC001'; - final deviceName = 'Living Room AC'; - - // Initial AC status - var acStatus = AcStatusModel( - uuid: deviceId, - acSwitch: false, - modeString: 'cold', - tempSet: 220, - currentTemp: 250, - fanSpeedsString: 'auto', - childLock: false, - ); - - // Get all AC functions - final acFunctions = getACFunctions(deviceId, deviceName); - - // Example: Turn on the AC - final switchFunction = acFunctions.firstWhere((f) => f is SwitchFunction) as SwitchFunction; - acStatus = switchFunction.execute(acStatus, true); - - // Example: Change mode to heat - final modeFunction = acFunctions.firstWhere((f) => f is ModeFunction) as ModeFunction; - acStatus = modeFunction.execute(acStatus, TempModes.hot); - - */ diff --git a/lib/pages/routiens/models/device_functions.dart b/lib/pages/routiens/models/device_functions.dart index 4234c812..a1f5725e 100644 --- a/lib/pages/routiens/models/device_functions.dart +++ b/lib/pages/routiens/models/device_functions.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + abstract class DeviceFunction { final String deviceId; final String deviceName; @@ -12,34 +14,33 @@ abstract class DeviceFunction { required this.operationName, required this.icon, }); - - T execute(T currentStatus, dynamic newValue); } class DeviceFunctionData { final String entityId; final String actionExecutor; - final String function; + final String functionCode; final String operationName; final dynamic value; final String? condition; final String? valueDescription; + final UniqueKey uniqueKey; DeviceFunctionData({ required this.entityId, - this.actionExecutor = 'function', - required this.function, + this.actionExecutor = 'device_issue', + required this.functionCode, required this.operationName, required this.value, this.condition, this.valueDescription, - }); + }) : uniqueKey = UniqueKey(); Map toJson() { return { 'entityId': entityId, 'actionExecutor': actionExecutor, - 'function': function, + 'function': functionCode, 'operationName': operationName, 'value': value, if (condition != null) 'condition': condition, @@ -51,7 +52,7 @@ class DeviceFunctionData { return DeviceFunctionData( entityId: json['entityId'], actionExecutor: json['actionExecutor'] ?? 'function', - function: json['function'], + functionCode: json['function'], operationName: json['operationName'], value: json['value'], condition: json['condition'], @@ -66,7 +67,7 @@ class DeviceFunctionData { return other is DeviceFunctionData && other.entityId == entityId && other.actionExecutor == actionExecutor && - other.function == function && + other.functionCode == functionCode && other.operationName == operationName && other.value == value && other.condition == condition && @@ -77,7 +78,7 @@ class DeviceFunctionData { int get hashCode { return entityId.hashCode ^ actionExecutor.hashCode ^ - function.hashCode ^ + functionCode.hashCode ^ operationName.hashCode ^ value.hashCode ^ condition.hashCode ^ diff --git a/lib/pages/routiens/models/gang_switches/base_switch_function.dart b/lib/pages/routiens/models/gang_switches/base_switch_function.dart index 3c89bebd..f180b203 100644 --- a/lib/pages/routiens/models/gang_switches/base_switch_function.dart +++ b/lib/pages/routiens/models/gang_switches/base_switch_function.dart @@ -10,8 +10,5 @@ abstract class BaseSwitchFunction extends DeviceFunction { required super.icon, }); - @override - bool execute(bool currentStatus, dynamic newValue); - List getOperationalValues(); } diff --git a/lib/pages/routiens/models/gang_switches/one_gang_switch/one_gang_switch.dart b/lib/pages/routiens/models/gang_switches/one_gang_switch/one_gang_switch.dart index 32bc436e..2e20e40e 100644 --- a/lib/pages/routiens/models/gang_switches/one_gang_switch/one_gang_switch.dart +++ b/lib/pages/routiens/models/gang_switches/one_gang_switch/one_gang_switch.dart @@ -10,11 +10,6 @@ class OneGangSwitchFunction extends BaseSwitchFunction { icon: Assets.assetsAcPower, ); - @override - bool execute(bool currentStatus, dynamic newValue) { - return newValue as bool; - } - @override List getOperationalValues() => [ SwitchOperationalValue( @@ -38,11 +33,6 @@ class OneGangCountdownFunction extends BaseSwitchFunction { icon: Assets.assetsLightCountdown, ); - @override - bool execute(bool currentStatus, dynamic newValue) { - return newValue as bool; - } - @override List getOperationalValues() => [ SwitchOperationalValue( diff --git a/lib/pages/routiens/models/gang_switches/three_gang_switch/three_gang_switch.dart b/lib/pages/routiens/models/gang_switches/three_gang_switch/three_gang_switch.dart index 8ff186ef..7f4710f0 100644 --- a/lib/pages/routiens/models/gang_switches/three_gang_switch/three_gang_switch.dart +++ b/lib/pages/routiens/models/gang_switches/three_gang_switch/three_gang_switch.dart @@ -10,11 +10,6 @@ class ThreeGangSwitch1Function extends BaseSwitchFunction { icon: Assets.assetsAcPower, ); - @override - bool execute(bool currentStatus, dynamic newValue) { - return newValue as bool; - } - @override List getOperationalValues() => [ SwitchOperationalValue( @@ -39,11 +34,6 @@ class ThreeGangCountdown1Function extends BaseSwitchFunction { icon: Assets.assetsLightCountdown, ); - @override - bool execute(bool currentStatus, dynamic newValue) { - return newValue as bool; - } - @override List getOperationalValues() => [ SwitchOperationalValue( @@ -65,11 +55,6 @@ class ThreeGangSwitch2Function extends BaseSwitchFunction { icon: Assets.assetsAcPower, ); - @override - bool execute(bool currentStatus, dynamic newValue) { - return newValue as bool; - } - @override List getOperationalValues() => [ SwitchOperationalValue( @@ -94,11 +79,6 @@ class ThreeGangCountdown2Function extends BaseSwitchFunction { icon: Assets.assetsLightCountdown, ); - @override - bool execute(bool currentStatus, dynamic newValue) { - return newValue as bool; - } - @override List getOperationalValues() => [ SwitchOperationalValue( @@ -120,11 +100,6 @@ class ThreeGangSwitch3Function extends BaseSwitchFunction { icon: Assets.assetsAcPower, ); - @override - bool execute(bool currentStatus, dynamic newValue) { - return newValue as bool; - } - @override List getOperationalValues() => [ SwitchOperationalValue( @@ -149,11 +124,6 @@ class ThreeGangCountdown3Function extends BaseSwitchFunction { icon: Assets.assetsLightCountdown, ); - @override - bool execute(bool currentStatus, dynamic newValue) { - return newValue as bool; - } - @override List getOperationalValues() => [ SwitchOperationalValue( diff --git a/lib/pages/routiens/models/gang_switches/two_gang_switch/two_gang_switch.dart b/lib/pages/routiens/models/gang_switches/two_gang_switch/two_gang_switch.dart index 7408d9d0..91bda15c 100644 --- a/lib/pages/routiens/models/gang_switches/two_gang_switch/two_gang_switch.dart +++ b/lib/pages/routiens/models/gang_switches/two_gang_switch/two_gang_switch.dart @@ -10,11 +10,6 @@ class TwoGangSwitch1Function extends BaseSwitchFunction { icon: Assets.assetsAcPower, ); - @override - bool execute(bool currentStatus, dynamic newValue) { - return newValue as bool; - } - @override List getOperationalValues() => [ SwitchOperationalValue( @@ -31,20 +26,13 @@ class TwoGangSwitch1Function extends BaseSwitchFunction { } class TwoGangSwitch2Function extends BaseSwitchFunction { - TwoGangSwitch2Function({required String deviceId, required String deviceName}) + TwoGangSwitch2Function({required super.deviceId, required super.deviceName}) : super( - deviceId: deviceId, - deviceName: deviceName, code: 'switch_2', operationName: 'Light 2 Switch', icon: Assets.assetsAcPower, ); - @override - bool execute(bool currentStatus, dynamic newValue) { - return newValue as bool; - } - @override List getOperationalValues() => [ SwitchOperationalValue( @@ -69,11 +57,6 @@ class TwoGangCountdown1Function extends BaseSwitchFunction { icon: Assets.assetsLightCountdown, ); - @override - bool execute(bool currentStatus, dynamic newValue) { - return newValue as bool; - } - @override List getOperationalValues() => [ SwitchOperationalValue( @@ -96,11 +79,6 @@ class TwoGangCountdown2Function extends BaseSwitchFunction { icon: Assets.assetsLightCountdown, ); - @override - bool execute(bool currentStatus, dynamic newValue) { - return newValue as bool; - } - @override List getOperationalValues() => [ SwitchOperationalValue( diff --git a/lib/pages/routiens/widgets/dragable_card.dart b/lib/pages/routiens/widgets/dragable_card.dart index 016b82c1..197ea0d2 100644 --- a/lib/pages/routiens/widgets/dragable_card.dart +++ b/lib/pages/routiens/widgets/dragable_card.dart @@ -43,95 +43,70 @@ class DraggableCard extends StatelessWidget { BuildContext context, List deviceFunctions) { return Card( color: ColorsManager.whiteColors, - child: SizedBox( + child: Container( + padding: const EdgeInsets.all(8), width: 90, + height: deviceFunctions.isEmpty ? 123 : null, child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - SizedBox( - height: 123, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - height: 50, - width: 50, - decoration: BoxDecoration( - color: ColorsManager.CircleImageBackground, - borderRadius: BorderRadius.circular(90), - border: Border.all( - color: ColorsManager.graysColor, - ), - ), - padding: const EdgeInsets.all(8), - child: imagePath.contains('.svg') - ? SvgPicture.asset( - imagePath, - ) - : Image.network(imagePath), - ), - const SizedBox(height: 8), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 3), - child: Text( - title, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - maxLines: 2, - style: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.blackColor, - fontSize: 12, - ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + height: 50, + width: 50, + decoration: BoxDecoration( + color: ColorsManager.CircleImageBackground, + borderRadius: BorderRadius.circular(90), + border: Border.all( + color: ColorsManager.graysColor, ), ), - ], - ), + padding: const EdgeInsets.all(8), + child: imagePath.contains('.svg') + ? SvgPicture.asset( + imagePath, + ) + : Image.network(imagePath), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 3), + child: Text( + title, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + maxLines: 2, + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.blackColor, + fontSize: 12, + ), + ), + ), + ], ), - if (deviceFunctions.isNotEmpty) ...[ - const Divider(height: 1), - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 4), - itemCount: deviceFunctions.length, - itemBuilder: (context, index) { - final function = deviceFunctions[index]; - return Row( + if (deviceFunctions.isNotEmpty) + // const Divider(height: 1), + ...deviceFunctions.map((function) => Row( mainAxisSize: MainAxisSize.min, children: [ Expanded( child: Text( - '${function.operationName}: ${function.valueDescription}', + '${function.operationName}: ${function.value}', style: context.textTheme.bodySmall?.copyWith( fontSize: 9, color: ColorsManager.textGray, height: 1.2, ), + maxLines: 2, overflow: TextOverflow.ellipsis, ), ), - InkWell( - onTap: () { - context.read().add( - RemoveFunction(function), - ); - }, - child: const Padding( - padding: EdgeInsets.all(2), - child: Icon( - Icons.close, - size: 12, - color: ColorsManager.textGray, - ), - ), - ), ], - ); - }, - ), - ], + )), ], ), ), diff --git a/lib/pages/routiens/widgets/routine_devices.dart b/lib/pages/routiens/widgets/routine_devices.dart index 0dc6e35e..20d47007 100644 --- a/lib/pages/routiens/widgets/routine_devices.dart +++ b/lib/pages/routiens/widgets/routine_devices.dart @@ -9,9 +9,6 @@ class RoutineDevices extends StatelessWidget { @override Widget build(BuildContext context) { - // Get the RoutineBloc instance from the parent - final routineBloc = context.read(); - return BlocProvider( create: (context) => DeviceManagementBloc()..add(FetchDevices()), child: BlocBuilder( @@ -26,32 +23,28 @@ class RoutineDevices extends StatelessWidget { .toList(); // Provide the RoutineBloc to the child widgets - return BlocProvider.value( - value: routineBloc, - child: BlocBuilder( - builder: (context, routineState) { - return Wrap( - spacing: 10, - runSpacing: 10, - children: deviceList.asMap().entries.map((entry) { - final device = entry.value; - return DraggableCard( - imagePath: device.getDefaultIcon(device.productType), - title: device.name ?? '', - deviceData: { - 'key': UniqueKey().toString(), - 'imagePath': - device.getDefaultIcon(device.productType), - 'title': device.name ?? '', - 'deviceId': device.uuid, - 'productType': device.productType, - 'functions': device.functions, - }, - ); - }).toList(), - ); - }, - ), + return BlocBuilder( + builder: (context, routineState) { + return Wrap( + spacing: 10, + runSpacing: 10, + children: deviceList.asMap().entries.map((entry) { + final device = entry.value; + return DraggableCard( + imagePath: device.getDefaultIcon(device.productType), + title: device.name ?? '', + deviceData: { + 'device': device, + 'imagePath': device.getDefaultIcon(device.productType), + 'title': device.name ?? '', + 'deviceId': device.uuid, + 'productType': device.productType, + 'functions': device.functions, + }, + ); + }).toList(), + ); + }, ); } return const Center(child: CircularProgressIndicator()); diff --git a/lib/pages/routiens/widgets/then_container.dart b/lib/pages/routiens/widgets/then_container.dart index 633a1ad1..9c868f09 100644 --- a/lib/pages/routiens/widgets/then_container.dart +++ b/lib/pages/routiens/widgets/then_container.dart @@ -30,7 +30,7 @@ class ThenContainer extends StatelessWidget { runSpacing: 8, children: state.thenItems .map((item) => DraggableCard( - key: Key(item['key']!), + // key: Key(item['key']!), imagePath: item['imagePath']!, title: item['title']!, deviceData: item, @@ -45,6 +45,18 @@ class ThenContainer extends StatelessWidget { onAccept: (data) async { final result = await DeviceDialogHelper.showDeviceDialog(context, data); + // if (result != null) { + // for (var function in routineBloc.state.selectedFunctions) { + // routineBloc.add(AddToThenContainer( + // { + // 'item': function, + // 'imagePath': data['imagePath'], + // 'title': data['name'], + + // } + // )); + // } + // } if (result != null) { context.read().add(AddToThenContainer(data)); } else if (!['AC', '1G', '2G', '3G'] diff --git a/lib/utils/constants/app_enum.dart b/lib/utils/constants/app_enum.dart index 4ca37d9b..e9a21193 100644 --- a/lib/utils/constants/app_enum.dart +++ b/lib/utils/constants/app_enum.dart @@ -97,3 +97,9 @@ extension AccessStatusExtension on AccessStatus { enum TempModes { hot, cold, wind } enum FanSpeeds { auto, low, middle, high } + +enum AcValuesEnums { + Cooling, + Heating, + Ventilation, +} From 5e94de5d78ef2035aa86f0f8a5101a0c69d573ee Mon Sep 17 00:00:00 2001 From: ashrafzarkanisala Date: Sun, 24 Nov 2024 01:10:31 +0300 Subject: [PATCH 15/23] fixes unique id for card drag --- .../all_devices/models/devices_model.dart | 2 + .../bloc/routine_bloc/routine_bloc.dart | 45 +- .../bloc/routine_bloc/routine_event.dart | 7 +- .../bloc/routine_bloc/routine_state.dart | 6 +- lib/pages/routiens/helper/ac_helper.dart | 51 +- .../dialog_helper/device_dialog_helper.dart | 24 +- .../helper/one_gang_switch_helper.dart | 185 ++---- .../helper/three_gang_switch_helper.dart | 571 +++++++++++------- .../helper/two_gang_switch_helper.dart | 564 ++++++++++------- .../routiens/models/device_functions.dart | 5 +- lib/pages/routiens/models/routine_item.dart | 51 +- lib/pages/routiens/models/routine_model.dart | 14 +- lib/pages/routiens/widgets/dragable_card.dart | 7 +- .../routiens/widgets/routine_devices.dart | 1 + .../routiens/widgets/then_container.dart | 51 +- pubspec.lock | 32 + pubspec.yaml | 1 + 17 files changed, 905 insertions(+), 712 deletions(-) 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 367a4fe8..b4f72287 100644 --- a/lib/pages/device_managment/all_devices/models/devices_model.dart +++ b/lib/pages/device_managment/all_devices/models/devices_model.dart @@ -105,6 +105,7 @@ class AllDevicesModel { this.productName, this.spaces, }); + AllDevicesModel.fromJson(Map json) { room = (json['room'] != null && (json['room'] is Map)) ? DevicesModelRoom.fromJson(json['room']) @@ -138,6 +139,7 @@ class AllDevicesModel { updateTime = int.tryParse(json['updateTime']?.toString() ?? ''); uuid = json['uuid']?.toString(); batteryLevel = int.tryParse(json['battery']?.toString() ?? ''); + productName = json['productName']?.toString(); if (json['spaces'] != null && json['spaces'] is List) { spaces = (json['spaces'] as List) diff --git a/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart b/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart index 2b711f39..bbd711a2 100644 --- a/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart +++ b/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart @@ -16,9 +16,9 @@ class RoutineBloc extends Bloc { on(_onAddToThenContainer); on(_onLoadScenes); on(_onLoadAutomation); - on(_onAddFunction); - on(_onRemoveFunction); - on(_onClearFunctions); + on(_onAddFunctionsToRoutine); + // on(_onRemoveFunction); + // on(_onClearFunctions); } void _onAddToIfContainer(AddToIfContainer event, Emitter emit) { @@ -38,24 +38,33 @@ class RoutineBloc extends Bloc { // } } - void _onAddFunction(AddFunctionToRoutine event, Emitter emit) { - final functions = List.from(state.selectedFunctions); - functions.add(event.function); - debugPrint("******" + functions.toString()); - emit(state.copyWith(selectedFunctions: functions)); + void _onAddFunctionsToRoutine( + AddFunctionToRoutine event, Emitter emit) { + debugPrint(event.uniqueCustomId.toString()); + debugPrint(event.functions.toString()); + final currentSelectedFunctions = + Map>.from(state.selectedFunctions); + + if (currentSelectedFunctions.containsKey(event.uniqueCustomId)) { + currentSelectedFunctions[event.uniqueCustomId]!.addAll(event.functions); + } else { + currentSelectedFunctions[event.uniqueCustomId] = event.functions; + } + + emit(state.copyWith(selectedFunctions: currentSelectedFunctions)); } - void _onRemoveFunction(RemoveFunction event, Emitter emit) { - final functions = List.from(state.selectedFunctions) - ..removeWhere((f) => - f.functionCode == event.function.functionCode && - f.value == event.function.value); - emit(state.copyWith(selectedFunctions: functions)); - } + // void _onRemoveFunction(RemoveFunction event, Emitter emit) { + // final functions = List.from(state.selectedFunctions) + // ..removeWhere((f) => + // f.functionCode == event.function.functionCode && + // f.value == event.function.value); + // emit(state.copyWith(selectedFunctions: functions)); + // } - void _onClearFunctions(ClearFunctions event, Emitter emit) { - emit(state.copyWith(selectedFunctions: [])); - } + // void _onClearFunctions(ClearFunctions event, Emitter emit) { + // emit(state.copyWith(selectedFunctions: [])); + // } // bool _isDuplicate( // List> items, Map newItem) { diff --git a/lib/pages/routiens/bloc/routine_bloc/routine_event.dart b/lib/pages/routiens/bloc/routine_bloc/routine_event.dart index 3321af5e..e3071b15 100644 --- a/lib/pages/routiens/bloc/routine_bloc/routine_event.dart +++ b/lib/pages/routiens/bloc/routine_bloc/routine_event.dart @@ -44,10 +44,11 @@ class LoadAutomation extends RoutineEvent { } class AddFunctionToRoutine extends RoutineEvent { - final DeviceFunctionData function; - const AddFunctionToRoutine(this.function); + final List functions; + final String uniqueCustomId; + const AddFunctionToRoutine(this.functions, this.uniqueCustomId); @override - List get props => [function]; + List get props => [functions]; } class RemoveFunction extends RoutineEvent { diff --git a/lib/pages/routiens/bloc/routine_bloc/routine_state.dart b/lib/pages/routiens/bloc/routine_bloc/routine_state.dart index 95aab557..ce6fea15 100644 --- a/lib/pages/routiens/bloc/routine_bloc/routine_state.dart +++ b/lib/pages/routiens/bloc/routine_bloc/routine_state.dart @@ -6,7 +6,7 @@ class RoutineState extends Equatable { final List> availableCards; final List scenes; final List automations; - final List selectedFunctions; + final Map> selectedFunctions; final bool isLoading; final String? errorMessage; @@ -16,7 +16,7 @@ class RoutineState extends Equatable { this.availableCards = const [], this.scenes = const [], this.automations = const [], - this.selectedFunctions = const [], + this.selectedFunctions = const {}, this.isLoading = false, this.errorMessage, }); @@ -26,7 +26,7 @@ class RoutineState extends Equatable { List>? thenItems, List? scenes, List? automations, - List? selectedFunctions, + Map>? selectedFunctions, bool? isLoading, String? errorMessage, }) { diff --git a/lib/pages/routiens/helper/ac_helper.dart b/lib/pages/routiens/helper/ac_helper.dart index 9ba02633..8b555b7a 100644 --- a/lib/pages/routiens/helper/ac_helper.dart +++ b/lib/pages/routiens/helper/ac_helper.dart @@ -18,6 +18,7 @@ class ACHelper { List functions, AllDevicesModel? device, List? deviceSelectedFunctions, + String uniqueCustomId, ) async { List acFunctions = functions.whereType().toList(); @@ -95,13 +96,14 @@ class ACHelper { onConfirm: state.addedFunctions.isNotEmpty ? () { /// add the functions to the routine bloc - for (var function in state.addedFunctions) { - context.read().add( - AddFunctionToRoutine( - function, - ), - ); - } + // for (var function in state.addedFunctions) { + context.read().add( + AddFunctionToRoutine( + state.addedFunctions, + uniqueCustomId, + ), + ); + //} // Return the device data to be added to the container Navigator.pop(context, { 'deviceId': functions.first.deviceId, @@ -229,11 +231,23 @@ class ACHelper { selectedFunctionData, ), const SizedBox(height: 20), - _buildTemperatureDisplay(context, initialValue, device, operationName, - selectedFunctionData, selectCode), + _buildTemperatureDisplay( + context, + initialValue, + device, + operationName, + selectedFunctionData, + selectCode, + ), const SizedBox(height: 20), - _buildTemperatureSlider(context, initialValue, device, operationName, - selectedFunctionData, selectCode), + _buildTemperatureSlider( + context, + initialValue, + device, + operationName, + selectedFunctionData, + selectCode, + ), ], ); } @@ -246,6 +260,7 @@ class ACHelper { AllDevicesModel? device, String operationName, DeviceFunctionData? selectedFunctionData, + // Function(String) onConditionChanged, ) { final conditions = ["<", "==", ">"]; @@ -282,12 +297,13 @@ class ACHelper { /// Build temperature display for AC functions dialog static Widget _buildTemperatureDisplay( - BuildContext context, - dynamic initialValue, - AllDevicesModel? device, - String operationName, - DeviceFunctionData? selectedFunctionData, - String selectCode) { + BuildContext context, + dynamic initialValue, + AllDevicesModel? device, + String operationName, + DeviceFunctionData? selectedFunctionData, + String selectCode, + ) { return Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), decoration: BoxDecoration( @@ -342,6 +358,7 @@ class ACHelper { required String operationName, required String selectCode, DeviceFunctionData? selectedFunctionData, + // required Function(dynamic) onValueChanged, }) { return ListView.builder( diff --git a/lib/pages/routiens/helper/dialog_helper/device_dialog_helper.dart b/lib/pages/routiens/helper/dialog_helper/device_dialog_helper.dart index 714feda3..fcb8e740 100644 --- a/lib/pages/routiens/helper/dialog_helper/device_dialog_helper.dart +++ b/lib/pages/routiens/helper/dialog_helper/device_dialog_helper.dart @@ -39,24 +39,28 @@ class DeviceDialogHelper { List functions, ) async { final routineBloc = context.read(); - final deviceSelectedFunctions = routineBloc.state.selectedFunctions - .where((f) => f.entityId == data['deviceId']) - .toList(); + final deviceSelectedFunctions = + routineBloc.state.selectedFunctions[data['uniqueCustomId']] ?? []; switch (productType) { case 'AC': - return ACHelper.showACFunctionsDialog( - context, functions, data['device'], deviceSelectedFunctions); + return ACHelper.showACFunctionsDialog(context, functions, + data['device'], deviceSelectedFunctions, data['uniqueCustomId']); case '1G': - return OneGangSwitchHelper.showSwitchFunctionsDialog( - context, functions, data['device'], deviceSelectedFunctions); + return OneGangSwitchHelper.showSwitchFunctionsDialog(context, functions, + data['device'], deviceSelectedFunctions, data['uniqueCustomId']); case '2G': - return TwoGangSwitchHelper.showSwitchFunctionsDialog( - context, functions); + return TwoGangSwitchHelper.showSwitchFunctionsDialog(context, functions, + data['device'], deviceSelectedFunctions, data['uniqueCustomId']); case '3G': return ThreeGangSwitchHelper.showSwitchFunctionsDialog( - context, functions); + context, + functions, + data['device'], + deviceSelectedFunctions, + data['uniqueCustomId'], + ); default: return null; } diff --git a/lib/pages/routiens/helper/one_gang_switch_helper.dart b/lib/pages/routiens/helper/one_gang_switch_helper.dart index 6cb28424..c7d5d35a 100644 --- a/lib/pages/routiens/helper/one_gang_switch_helper.dart +++ b/lib/pages/routiens/helper/one_gang_switch_helper.dart @@ -4,11 +4,9 @@ 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/routiens/bloc/functions_bloc/functions_bloc_bloc.dart'; import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; -import 'package:syncrow_web/pages/routiens/models/ac/ac_function.dart'; -import 'package:syncrow_web/pages/routiens/models/ac/ac_operational_value.dart'; import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart'; -import 'package:syncrow_web/pages/routiens/models/gang_switches/one_gang_switch/one_gang_switch.dart'; +import 'package:syncrow_web/pages/routiens/models/gang_switches/switch_operational_value.dart'; import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart'; import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart'; import 'package:syncrow_web/utils/color_manager.dart'; @@ -20,8 +18,10 @@ class OneGangSwitchHelper { List functions, AllDevicesModel? device, List? deviceSelectedFunctions, + String uniqueCustomId, ) async { - List acFunctions = functions.whereType().toList(); + List acFunctions = + functions.whereType().toList(); return showDialog?>( context: context, @@ -107,30 +107,6 @@ class OneGangSwitchHelper { operationName: selectedOperationName ?? '', ), ), - // ValueListenableBuilder( - // valueListenable: selectedFunctionNotifier, - // builder: (context, selectedFunction, _) { - // final selectedFn = - // switchFunctions.firstWhere( - // (f) => f.code == selectedFunction, - // ); - // return Expanded( - // child: selectedFn - // is OneGangCountdownFunction - // ? _buildCountDownSelector( - // context, - // selectedValueNotifier, - // selectedConditionNotifier, - // selectedConditionsNotifier, - // ) - // : _buildOperationalValuesList( - // context, - // selectedFn as BaseSwitchFunction, - // selectedValueNotifier, - // ), - // ); - // }, - // ), ], ), ), @@ -146,13 +122,20 @@ class OneGangSwitchHelper { onConfirm: state.addedFunctions.isNotEmpty ? () { /// add the functions to the routine bloc - for (var function in state.addedFunctions) { - context.read().add( - AddFunctionToRoutine( - function, - ), - ); - } + // for (var function in state.addedFunctions) { + // context.read().add( + // AddFunctionToRoutine( + // function, + // uniqueCustomId, + // ), + // ); + // } + context.read().add( + AddFunctionToRoutine( + state.addedFunctions, + uniqueCustomId, + ), + ); // Return the device data to be added to the container Navigator.pop(context, { 'deviceId': functions.first.deviceId, @@ -175,13 +158,13 @@ class OneGangSwitchHelper { required BuildContext context, required String selectedFunction, required DeviceFunctionData? selectedFunctionData, - required List acFunctions, + required List acFunctions, AllDevicesModel? device, required String operationName, }) { if (selectedFunction == 'countdown_1') { final initialValue = selectedFunctionData?.value ?? 200; - return _buildTemperatureSelector( + return _buildCountDownSelector( context: context, initialValue: initialValue, selectCode: selectedFunction, @@ -207,7 +190,7 @@ class OneGangSwitchHelper { ); } - static Widget _buildTemperatureSelector({ + static Widget _buildCountDownSelector({ required BuildContext context, required dynamic initialValue, required String? currentCondition, @@ -228,10 +211,10 @@ class OneGangSwitchHelper { selectedFunctionData, ), const SizedBox(height: 20), - _buildTemperatureDisplay(context, initialValue, device, operationName, + _buildCountDownDisplay(context, initialValue, device, operationName, selectedFunctionData, selectCode), const SizedBox(height: 20), - _buildTemperatureSlider(context, initialValue, device, operationName, + _buildCountDownSlider(context, initialValue, device, operationName, selectedFunctionData, selectCode), ], ); @@ -280,7 +263,7 @@ class OneGangSwitchHelper { } /// Build temperature display for AC functions dialog - static Widget _buildTemperatureDisplay( + static Widget _buildCountDownDisplay( BuildContext context, dynamic initialValue, AllDevicesModel? device, @@ -302,7 +285,7 @@ class OneGangSwitchHelper { ); } - static Widget _buildTemperatureSlider( + static Widget _buildCountDownSlider( BuildContext context, dynamic initialValue, AllDevicesModel? device, @@ -310,11 +293,22 @@ class OneGangSwitchHelper { DeviceFunctionData? selectedFunctionData, String selectCode, ) { + final operationalValues = SwitchOperationalValue( + icon: '', + description: "sec", + value: 0.0, + minValue: 0, + maxValue: 43200, + stepValue: 1, + ); return Slider( value: (initialValue ?? 0).toDouble(), - min: 0, - max: 300, - divisions: 300, + min: operationalValues.minValue?.toDouble() ?? 0.0, + max: operationalValues.maxValue?.toDouble() ?? 0.0, + divisions: (((operationalValues.maxValue ?? 0) - + (operationalValues.minValue ?? 0)) / + (operationalValues.stepValue ?? 1)) + .round(), onChanged: (value) { context.read().add( AddFunction( @@ -334,13 +328,12 @@ class OneGangSwitchHelper { static Widget _buildOperationalValuesList({ required BuildContext context, - required List values, + required List values, required dynamic selectedValue, AllDevicesModel? device, required String operationName, required String selectCode, DeviceFunctionData? selectedFunctionData, - // required Function(dynamic) onValueChanged, }) { return ListView.builder( shrinkWrap: false, @@ -394,102 +387,4 @@ class OneGangSwitchHelper { }, ); } - - // static Widget _buildCountDownSelector( - // BuildContext context, - // ValueNotifier valueNotifier, - // ValueNotifier conditionNotifier, - // ValueNotifier> conditionsNotifier, - // ) { - // return ValueListenableBuilder( - // valueListenable: valueNotifier, - // builder: (context, value, _) { - // return Column( - // mainAxisAlignment: MainAxisAlignment.center, - // children: [ - // ValueListenableBuilder>( - // valueListenable: conditionsNotifier, - // builder: (context, selectedConditions, _) { - // return ToggleButtons( - // onPressed: (int index) { - // final newConditions = List.filled(3, false); - // newConditions[index] = true; - // conditionsNotifier.value = newConditions; - // conditionNotifier.value = index == 0 - // ? "<" - // : index == 1 - // ? "==" - // : ">"; - // }, - // 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: selectedConditions, - // children: const [Text("<"), Text("="), Text(">")], - // ); - // }, - // ), - // const SizedBox(height: 20), - // Text( - // '${value ?? 0} sec', - // style: Theme.of(context).textTheme.headlineMedium, - // ), - // const SizedBox(height: 20), - // Slider( - // value: (value ?? 0).toDouble(), - // min: 0, - // max: 300, - // divisions: 300, - // onChanged: (newValue) { - // valueNotifier.value = newValue.toInt(); - // }, - // ), - // ], - // ); - // }, - // ); - // } - - // static Widget _buildOperationalValuesList( - // BuildContext context, - // BaseSwitchFunction function, - // ValueNotifier valueNotifier, - // ) { - // final values = function.getOperationalValues(); - // return ValueListenableBuilder( - // valueListenable: valueNotifier, - // builder: (context, selectedValue, _) { - // return ListView.builder( - // itemCount: values.length, - // itemBuilder: (context, index) { - // final value = values[index]; - // return ListTile( - // leading: SvgPicture.asset( - // value.icon, - // width: 24, - // height: 24, - // ), - // title: Text( - // value.description, - // style: context.textTheme.bodyMedium, - // ), - // trailing: Radio( - // value: value.value, - // groupValue: selectedValue, - // onChanged: (newValue) { - // valueNotifier.value = newValue; - // }, - // ), - // ); - // }, - // ); - // }, - // ); - // } } diff --git a/lib/pages/routiens/helper/three_gang_switch_helper.dart b/lib/pages/routiens/helper/three_gang_switch_helper.dart index 502b302e..4a4c5e02 100644 --- a/lib/pages/routiens/helper/three_gang_switch_helper.dart +++ b/lib/pages/routiens/helper/three_gang_switch_helper.dart @@ -1,8 +1,12 @@ 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/routiens/bloc/functions_bloc/functions_bloc_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart'; -import 'package:syncrow_web/pages/routiens/models/gang_switches/three_gang_switch/three_gang_switch.dart'; +import 'package:syncrow_web/pages/routiens/models/gang_switches/switch_operational_value.dart'; import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart'; import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart'; import 'package:syncrow_web/utils/color_manager.dart'; @@ -10,64 +14,60 @@ import 'package:syncrow_web/utils/extension/build_context_x.dart'; class ThreeGangSwitchHelper { static Future?> showSwitchFunctionsDialog( - BuildContext context, List> functions) async { - List> switchFunctions = functions - .where((f) => - f is ThreeGangSwitch1Function || - f is ThreeGangSwitch2Function || - f is ThreeGangSwitch3Function || - f is ThreeGangCountdown1Function || - f is ThreeGangCountdown2Function || - f is ThreeGangCountdown3Function) - .toList(); + BuildContext context, + List functions, + AllDevicesModel? device, + List? deviceSelectedFunctions, + String uniqueCustomId, + ) async { + List switchFunctions = + functions.whereType().toList(); - final selectedFunctionNotifier = ValueNotifier(null); - final selectedValueNotifier = ValueNotifier(null); - final selectedConditionNotifier = ValueNotifier('<'); - final selectedConditionsNotifier = - ValueNotifier>([true, false, false]); - - await showDialog( + return showDialog?>( context: context, builder: (BuildContext context) { - return ValueListenableBuilder( - valueListenable: selectedFunctionNotifier, - builder: (context, selectedFunction, _) { - return AlertDialog( + return BlocProvider( + create: (_) => FunctionBloc() + ..add(InitializeFunctions(deviceSelectedFunctions ?? [])), + child: AlertDialog( contentPadding: EdgeInsets.zero, - content: Container( - width: selectedFunction != null ? 600 : 300, - 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('3 Gangs Light Switch Condition'), - Expanded( - child: Row( - children: [ - // Left side: Function list - Expanded( - child: ListView.separated( - itemCount: switchFunctions.length, - separatorBuilder: (_, __) => const Divider( - color: ColorsManager.dividerColor, - ), - itemBuilder: (context, index) { - final function = switchFunctions[index]; - return ValueListenableBuilder( - valueListenable: selectedFunctionNotifier, - builder: (context, selectedFunction, _) { - final isSelected = - selectedFunction == function.code; + content: BlocBuilder( + builder: (context, 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, + )); + 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('3 Gangs Light Switch Condition'), + Expanded( + child: Row( + children: [ + // Left side: Function list + Expanded( + child: ListView.separated( + itemCount: switchFunctions.length, + separatorBuilder: (_, __) => const Divider( + color: ColorsManager.dividerColor, + ), + itemBuilder: (context, index) { + final function = switchFunctions[index]; return ListTile( - tileColor: isSelected - ? Colors.grey.shade100 - : null, leading: SvgPicture.asset( function.icon, width: 24, @@ -83,196 +83,307 @@ class ThreeGangSwitchHelper { color: ColorsManager.textGray, ), onTap: () { - selectedFunctionNotifier.value = - function.code; - selectedValueNotifier.value = function - is ThreeGangCountdown1Function || - function - is ThreeGangCountdown2Function || - function - is ThreeGangCountdown3Function - ? 0 - : null; + context + .read() + .add(SelectFunction( + functionCode: function.code, + operationName: + function.operationName, + )); }, ); }, - ); - }, - ), - ), - // Right side: Value selector - if (selectedFunction != null) - Expanded( - child: ValueListenableBuilder( - valueListenable: selectedValueNotifier, - builder: (context, selectedValue, _) { - final selectedFn = switchFunctions.firstWhere( - (f) => f.code == selectedFunction, - ); - return selectedFn - is ThreeGangCountdown1Function || - selectedFn - is ThreeGangCountdown2Function || - selectedFn - is ThreeGangCountdown3Function - ? _buildCountDownSelector( - context, - selectedValueNotifier, - selectedConditionNotifier, - selectedConditionsNotifier, - ) - : _buildOperationalValuesList( - context, - selectedFn as BaseSwitchFunction, - selectedValueNotifier, - ); - }, + ), ), - ), - ], - ), + // Right side: Value selector + if (selectedFunction != null) + Expanded( + child: _buildValueSelector( + context: context, + selectedFunction: selectedFunction, + selectedFunctionData: selectedFunctionData, + switchFunctions: switchFunctions, + device: device, + operationName: selectedOperationName ?? '', + ), + ), + ], + ), + ), + Container( + height: 1, + width: double.infinity, + color: ColorsManager.greyColor, + ), + DialogFooter( + onCancel: () { + Navigator.pop(context); + }, + onConfirm: state.addedFunctions.isNotEmpty + ? () { + /// add the functions to the routine bloc + // for (var function in state.addedFunctions) { + // context.read().add( + // AddFunctionToRoutine( + // function, + // uniqueCustomId, + // ), + // ); + // } + context.read().add( + AddFunctionToRoutine( + state.addedFunctions, + uniqueCustomId, + ), + ); + // Return the device data to be added to the container + Navigator.pop(context, { + 'deviceId': functions.first.deviceId, + }); + } + : null, + isConfirmEnabled: selectedFunction != null, + ), + ], ), - DialogFooter( - onCancel: () => Navigator.pop(context), - onConfirm: selectedFunctionNotifier.value != null && - selectedValueNotifier.value != null - ? () { - final selectedFn = switchFunctions.firstWhere( - (f) => f.code == selectedFunctionNotifier.value, - ); - final value = selectedValueNotifier.value; - final functionData = DeviceFunctionData( - entityId: selectedFn.deviceId, - functionCode: selectedFn.code, - operationName: selectedFn.operationName, - value: value, - condition: selectedConditionNotifier.value, - valueDescription: selectedFn - is ThreeGangCountdown1Function || - selectedFn - is ThreeGangCountdown2Function || - selectedFn - is ThreeGangCountdown3Function - ? '${value} sec' - : ((selectedFn as BaseSwitchFunction) - .getOperationalValues() - .firstWhere((v) => v.value == value) - .description), - ); - Navigator.pop( - context, {selectedFn.code: functionData}); - } - : null, - isConfirmEnabled: selectedFunctionNotifier.value != null, - ), - ], - ), + ); + }, ), - ); - }, - ); - }, - ).then((value) { - selectedFunctionNotifier.dispose(); - selectedValueNotifier.dispose(); - selectedConditionNotifier.dispose(); - selectedConditionsNotifier.dispose(); - return value; - }); - } - - static Widget _buildCountDownSelector( - BuildContext context, - ValueNotifier valueNotifier, - ValueNotifier conditionNotifier, - ValueNotifier> conditionsNotifier, - ) { - return ValueListenableBuilder( - valueListenable: valueNotifier, - builder: (context, value, _) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ValueListenableBuilder>( - valueListenable: conditionsNotifier, - builder: (context, selectedConditions, _) { - return ToggleButtons( - onPressed: (int index) { - final newConditions = List.filled(3, false); - newConditions[index] = true; - conditionsNotifier.value = newConditions; - conditionNotifier.value = index == 0 - ? "<" - : index == 1 - ? "==" - : ">"; - }, - 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: selectedConditions, - children: const [Text("<"), Text("="), Text(">")], - ); - }, - ), - const SizedBox(height: 20), - Text( - '${value ?? 0} sec', - style: Theme.of(context).textTheme.headlineMedium, - ), - const SizedBox(height: 20), - Slider( - value: (value ?? 0).toDouble(), - min: 0, - max: 300, - divisions: 300, - onChanged: (newValue) { - valueNotifier.value = newValue.toInt(); - }, - ), - ], - ); + )); }, ); } - static Widget _buildOperationalValuesList( + static Widget _buildValueSelector({ + required BuildContext context, + required String selectedFunction, + required DeviceFunctionData? selectedFunctionData, + required List switchFunctions, + AllDevicesModel? device, + required String operationName, + }) { + if (selectedFunction == 'countdown_1' || + selectedFunction == 'countdown_2' || + selectedFunction == 'countdown_3') { + final initialValue = selectedFunctionData?.value ?? 200; + return _buildTemperatureSelector( + context: context, + initialValue: initialValue, + selectCode: selectedFunction, + currentCondition: selectedFunctionData?.condition, + device: device, + operationName: operationName, + selectedFunctionData: selectedFunctionData, + ); + } + + final selectedFn = + switchFunctions.firstWhere((f) => f.code == selectedFunction); + final values = selectedFn.getOperationalValues(); + + return _buildOperationalValuesList( + context: context, + values: values, + selectedValue: selectedFunctionData?.value, + device: device, + operationName: operationName, + selectCode: selectedFunction, + selectedFunctionData: selectedFunctionData, + ); + } + + static Widget _buildTemperatureSelector({ + required BuildContext context, + required dynamic initialValue, + required String? currentCondition, + required String selectCode, + AllDevicesModel? device, + required String operationName, + DeviceFunctionData? selectedFunctionData, + }) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildConditionToggle( + context, + currentCondition, + selectCode, + device, + operationName, + selectedFunctionData, + ), + const SizedBox(height: 20), + _buildCountDownDisplay(context, initialValue, device, operationName, + selectedFunctionData, selectCode), + const SizedBox(height: 20), + _buildCountDownSlider(context, initialValue, device, operationName, + selectedFunctionData, selectCode), + ], + ); + } + + /// Build condition toggle for AC functions dialog + static Widget _buildConditionToggle( BuildContext context, - BaseSwitchFunction function, - ValueNotifier valueNotifier, + String? currentCondition, + String selectCode, + AllDevicesModel? device, + String operationName, + DeviceFunctionData? selectedFunctionData, + // Function(String) onConditionChanged, ) { - final values = function.getOperationalValues(); - return ValueListenableBuilder( - valueListenable: valueNotifier, - builder: (context, selectedValue, _) { - return ListView.builder( - itemCount: values.length, - itemBuilder: (context, index) { - final value = values[index]; - return ListTile( - leading: SvgPicture.asset( - value.icon, - width: 24, - height: 24, - ), - title: Text( - value.description, - style: context.textTheme.bodyMedium, - ), - trailing: Radio( - value: value.value, - groupValue: selectedValue, - onChanged: (newValue) { - valueNotifier.value = newValue; - }, + final conditions = ["<", "==", ">"]; + + return ToggleButtons( + onPressed: (int index) { + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + condition: conditions[index], + value: selectedFunctionData?.value, + valueDescription: selectedFunctionData?.valueDescription, + ), ), ); + }, + 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(), + ); + } + + /// Build temperature display for AC functions dialog + static Widget _buildCountDownDisplay( + BuildContext context, + dynamic initialValue, + AllDevicesModel? device, + String operationName, + DeviceFunctionData? selectedFunctionData, + String selectCode) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + decoration: BoxDecoration( + color: ColorsManager.primaryColorWithOpacity.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + '${initialValue ?? 0} sec', + style: context.textTheme.headlineMedium!.copyWith( + color: ColorsManager.primaryColorWithOpacity, + ), + ), + ); + } + + static Widget _buildCountDownSlider( + BuildContext context, + dynamic initialValue, + AllDevicesModel? device, + String operationName, + DeviceFunctionData? selectedFunctionData, + String selectCode, + ) { + final operationalValues = SwitchOperationalValue( + icon: '', + description: "sec", + value: 0.0, + minValue: 0, + maxValue: 43200, + stepValue: 1, + ); + return Slider( + value: (initialValue ?? 0).toDouble(), + min: operationalValues.minValue?.toDouble() ?? 0.0, + max: operationalValues.maxValue?.toDouble() ?? 0.0, + divisions: (((operationalValues.maxValue ?? 0) - + (operationalValues.minValue ?? 0)) / + (operationalValues.stepValue ?? 1)) + .round(), + onChanged: (value) { + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + value: value, + condition: selectedFunctionData?.condition, + valueDescription: selectedFunctionData?.valueDescription, + ), + ), + ); + }, + ); + } + + static Widget _buildOperationalValuesList({ + required BuildContext context, + required List values, + required dynamic selectedValue, + AllDevicesModel? device, + required String operationName, + required String selectCode, + DeviceFunctionData? selectedFunctionData, + }) { + return ListView.builder( + shrinkWrap: false, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: values.length, + itemBuilder: (context, index) { + final value = values[index]; + final isSelected = selectedValue == value.value; + return ListTile( + leading: SvgPicture.asset( + value.icon, + width: 24, + height: 24, + placeholderBuilder: (BuildContext context) => Container( + width: 24, + height: 24, + color: Colors.transparent, + ), + ), + title: Text( + value.description, + style: context.textTheme.bodyMedium, + ), + trailing: Icon( + isSelected + ? Icons.radio_button_checked + : Icons.radio_button_unchecked, + size: 24, + color: isSelected + ? ColorsManager.primaryColorWithOpacity + : ColorsManager.textGray, + ), + onTap: () { + if (!isSelected) { + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + value: value.value, + condition: selectedFunctionData?.condition, + valueDescription: + selectedFunctionData?.valueDescription, + ), + ), + ); + } }, ); }, diff --git a/lib/pages/routiens/helper/two_gang_switch_helper.dart b/lib/pages/routiens/helper/two_gang_switch_helper.dart index 038ee345..3805882a 100644 --- a/lib/pages/routiens/helper/two_gang_switch_helper.dart +++ b/lib/pages/routiens/helper/two_gang_switch_helper.dart @@ -1,8 +1,12 @@ 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/routiens/bloc/functions_bloc/functions_bloc_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart'; -import 'package:syncrow_web/pages/routiens/models/gang_switches/two_gang_switch/two_gang_switch.dart'; +import 'package:syncrow_web/pages/routiens/models/gang_switches/switch_operational_value.dart'; import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart'; import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart'; import 'package:syncrow_web/utils/color_manager.dart'; @@ -10,62 +14,60 @@ import 'package:syncrow_web/utils/extension/build_context_x.dart'; class TwoGangSwitchHelper { static Future?> showSwitchFunctionsDialog( - BuildContext context, List> functions) async { - List> switchFunctions = functions - .where((f) => - f is TwoGangSwitch1Function || - f is TwoGangSwitch2Function || - f is TwoGangCountdown1Function || - f is TwoGangCountdown2Function) - .toList(); + BuildContext context, + List functions, + AllDevicesModel? device, + List? deviceSelectedFunctions, + String uniqueCustomId, + ) async { + List switchFunctions = + functions.whereType().toList(); - final selectedFunctionNotifier = ValueNotifier(null); - final selectedValueNotifier = ValueNotifier(null); - final selectedConditionNotifier = ValueNotifier('<'); - final selectedConditionsNotifier = - ValueNotifier>([true, false, false]); - - await showDialog( + return showDialog?>( context: context, builder: (BuildContext context) { - return ValueListenableBuilder( - valueListenable: selectedFunctionNotifier, - builder: (context, selectedFunction, _) { - return AlertDialog( + return BlocProvider( + create: (_) => FunctionBloc() + ..add(InitializeFunctions(deviceSelectedFunctions ?? [])), + child: AlertDialog( contentPadding: EdgeInsets.zero, - content: Container( - width: selectedFunction != null ? 600 : 300, - 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('2 Gangs Light Switch Condition'), - Expanded( - child: Row( - children: [ - // Left side: Function list - Expanded( - child: ListView.separated( - itemCount: switchFunctions.length, - separatorBuilder: (_, __) => const Divider( - color: ColorsManager.dividerColor, - ), - itemBuilder: (context, index) { - final function = switchFunctions[index]; - return ValueListenableBuilder( - valueListenable: selectedFunctionNotifier, - builder: (context, selectedFunction, _) { - final isSelected = - selectedFunction == function.code; + content: BlocBuilder( + builder: (context, 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, + )); + 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('2 Gangs Light Switch Condition'), + Expanded( + child: Row( + children: [ + // Left side: Function list + Expanded( + child: ListView.separated( + itemCount: switchFunctions.length, + separatorBuilder: (_, __) => const Divider( + color: ColorsManager.dividerColor, + ), + itemBuilder: (context, index) { + final function = switchFunctions[index]; return ListTile( - tileColor: isSelected - ? Colors.grey.shade100 - : null, leading: SvgPicture.asset( function.icon, width: 24, @@ -81,191 +83,307 @@ class TwoGangSwitchHelper { color: ColorsManager.textGray, ), onTap: () { - selectedFunctionNotifier.value = - function.code; - selectedValueNotifier.value = function - is TwoGangCountdown1Function || - function - is TwoGangCountdown2Function - ? 0 - : null; + context + .read() + .add(SelectFunction( + functionCode: function.code, + operationName: + function.operationName, + )); }, ); }, - ); - }, - ), - ), - // Right side: Value selector - if (selectedFunction != null) - Expanded( - child: ValueListenableBuilder( - valueListenable: selectedValueNotifier, - builder: (context, selectedValue, _) { - final selectedFn = switchFunctions.firstWhere( - (f) => f.code == selectedFunction, - ); - - if (selectedFn is TwoGangCountdown1Function || - selectedFn is TwoGangCountdown2Function) { - return _buildCountDownSelector( - context, - selectedValueNotifier, - selectedConditionNotifier, - selectedConditionsNotifier, - ); - } - - return _buildOperationalValuesList( - context, - selectedFn as BaseSwitchFunction, - selectedValueNotifier, - ); - }, + ), ), - ), - ], - ), + // Right side: Value selector + if (selectedFunction != null) + Expanded( + child: _buildValueSelector( + context: context, + selectedFunction: selectedFunction, + selectedFunctionData: selectedFunctionData, + switchFunctions: switchFunctions, + device: device, + operationName: selectedOperationName ?? '', + ), + ), + ], + ), + ), + Container( + height: 1, + width: double.infinity, + color: ColorsManager.greyColor, + ), + DialogFooter( + onCancel: () { + Navigator.pop(context); + }, + onConfirm: state.addedFunctions.isNotEmpty + ? () { + /// add the functions to the routine bloc + // for (var function in state.addedFunctions) { + // context.read().add( + // AddFunctionToRoutine( + // function, + // uniqueCustomId + // ), + // ); + // } + context.read().add( + AddFunctionToRoutine( + state.addedFunctions, + uniqueCustomId, + ), + ); + // Return the device data to be added to the container + Navigator.pop(context, { + 'deviceId': functions.first.deviceId, + }); + } + : null, + isConfirmEnabled: selectedFunction != null, + ), + ], ), - DialogFooter( - onCancel: () => Navigator.pop(context), - onConfirm: selectedFunction != null && - selectedValueNotifier.value != null - ? () { - final selectedFn = switchFunctions.firstWhere( - (f) => f.code == selectedFunction, - ); - final value = selectedValueNotifier.value; - final functionData = DeviceFunctionData( - entityId: selectedFn.deviceId, - functionCode: selectedFn.code, - operationName: selectedFn.operationName, - value: value, - condition: selectedConditionNotifier.value, - valueDescription: selectedFn - is TwoGangCountdown1Function || - selectedFn is TwoGangCountdown2Function - ? '${value} sec' - : ((selectedFn as BaseSwitchFunction) - .getOperationalValues() - .firstWhere((v) => v.value == value) - .description), - ); - Navigator.pop( - context, {selectedFn.code: functionData}); - } - : null, - isConfirmEnabled: selectedFunction != null, - ), - ], - ), - ), - ); - }, - ); - }, - ).then((value) { - selectedFunctionNotifier.dispose(); - selectedValueNotifier.dispose(); - selectedConditionNotifier.dispose(); - selectedConditionsNotifier.dispose(); - return value; - }); - } - - static Widget _buildOperationalValuesList( - BuildContext context, - BaseSwitchFunction function, - ValueNotifier valueNotifier, - ) { - final values = function.getOperationalValues(); - return ValueListenableBuilder( - valueListenable: valueNotifier, - builder: (context, selectedValue, _) { - return ListView.builder( - itemCount: values.length, - itemBuilder: (context, index) { - final value = values[index]; - return ListTile( - leading: SvgPicture.asset( - value.icon, - width: 24, - height: 24, - ), - title: Text( - value.description, - style: context.textTheme.bodyMedium, - ), - trailing: Radio( - value: value.value, - groupValue: selectedValue, - onChanged: (newValue) { - valueNotifier.value = newValue; + ); }, ), - ); - }, - ); + )); }, ); } - static Widget _buildCountDownSelector( + static Widget _buildValueSelector({ + required BuildContext context, + required String selectedFunction, + required DeviceFunctionData? selectedFunctionData, + required List switchFunctions, + AllDevicesModel? device, + required String operationName, + }) { + if (selectedFunction == 'countdown_1' || + selectedFunction == 'countdown_2') { + final initialValue = selectedFunctionData?.value ?? 200; + return _buildTemperatureSelector( + context: context, + initialValue: initialValue, + selectCode: selectedFunction, + currentCondition: selectedFunctionData?.condition, + device: device, + operationName: operationName, + selectedFunctionData: selectedFunctionData, + ); + } + + final selectedFn = + switchFunctions.firstWhere((f) => f.code == selectedFunction); + final values = selectedFn.getOperationalValues(); + + return _buildOperationalValuesList( + context: context, + values: values, + selectedValue: selectedFunctionData?.value, + device: device, + operationName: operationName, + selectCode: selectedFunction, + selectedFunctionData: selectedFunctionData, + ); + } + + static Widget _buildTemperatureSelector({ + required BuildContext context, + required dynamic initialValue, + required String? currentCondition, + required String selectCode, + AllDevicesModel? device, + required String operationName, + DeviceFunctionData? selectedFunctionData, + }) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildConditionToggle( + context, + currentCondition, + selectCode, + device, + operationName, + selectedFunctionData, + ), + const SizedBox(height: 20), + _buildCountDownDisplay(context, initialValue, device, operationName, + selectedFunctionData, selectCode), + const SizedBox(height: 20), + _buildCountDownSlider(context, initialValue, device, operationName, + selectedFunctionData, selectCode), + ], + ); + } + + /// Build condition toggle for AC functions dialog + static Widget _buildConditionToggle( BuildContext context, - ValueNotifier valueNotifier, - ValueNotifier conditionNotifier, - ValueNotifier> conditionsNotifier, + String? currentCondition, + String selectCode, + AllDevicesModel? device, + String operationName, + DeviceFunctionData? selectedFunctionData, + // Function(String) onConditionChanged, ) { - return ValueListenableBuilder( - valueListenable: valueNotifier, - builder: (context, value, _) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ValueListenableBuilder>( - valueListenable: conditionsNotifier, - builder: (context, selectedConditions, _) { - return ToggleButtons( - onPressed: (int index) { - final newConditions = List.filled(3, false); - newConditions[index] = true; - conditionsNotifier.value = newConditions; - conditionNotifier.value = index == 0 - ? "<" - : index == 1 - ? "==" - : ">"; - }, - 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: selectedConditions, - children: const [Text("<"), Text("="), Text(">")], - ); - }, + final conditions = ["<", "==", ">"]; + + return ToggleButtons( + onPressed: (int index) { + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + condition: conditions[index], + value: selectedFunctionData?.value, + valueDescription: selectedFunctionData?.valueDescription, + ), + ), + ); + }, + 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(), + ); + } + + /// Build temperature display for AC functions dialog + static Widget _buildCountDownDisplay( + BuildContext context, + dynamic initialValue, + AllDevicesModel? device, + String operationName, + DeviceFunctionData? selectedFunctionData, + String selectCode) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + decoration: BoxDecoration( + color: ColorsManager.primaryColorWithOpacity.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + '${initialValue ?? 0} sec', + style: context.textTheme.headlineMedium!.copyWith( + color: ColorsManager.primaryColorWithOpacity, + ), + ), + ); + } + + static Widget _buildCountDownSlider( + BuildContext context, + dynamic initialValue, + AllDevicesModel? device, + String operationName, + DeviceFunctionData? selectedFunctionData, + String selectCode, + ) { + final operationalValues = SwitchOperationalValue( + icon: '', + description: "sec", + value: 0.0, + minValue: 0, + maxValue: 43200, + stepValue: 1, + ); + return Slider( + value: (initialValue ?? 0).toDouble(), + min: operationalValues.minValue?.toDouble() ?? 0.0, + max: operationalValues.maxValue?.toDouble() ?? 0.0, + divisions: (((operationalValues.maxValue ?? 0) - + (operationalValues.minValue ?? 0)) / + (operationalValues.stepValue ?? 1)) + .round(), + onChanged: (value) { + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + value: value, + condition: selectedFunctionData?.condition, + valueDescription: selectedFunctionData?.valueDescription, + ), + ), + ); + }, + ); + } + + static Widget _buildOperationalValuesList({ + required BuildContext context, + required List values, + required dynamic selectedValue, + AllDevicesModel? device, + required String operationName, + required String selectCode, + DeviceFunctionData? selectedFunctionData, + }) { + return ListView.builder( + shrinkWrap: false, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: values.length, + itemBuilder: (context, index) { + final value = values[index]; + final isSelected = selectedValue == value.value; + return ListTile( + leading: SvgPicture.asset( + value.icon, + width: 24, + height: 24, + placeholderBuilder: (BuildContext context) => Container( + width: 24, + height: 24, + color: Colors.transparent, ), - const SizedBox(height: 20), - Text( - '${value ?? 0} sec', - style: Theme.of(context).textTheme.headlineMedium, - ), - const SizedBox(height: 20), - Slider( - value: (value ?? 0).toDouble(), - min: 0, - max: 300, - divisions: 300, - onChanged: (newValue) { - valueNotifier.value = newValue.toInt(); - }, - ), - ], + ), + title: Text( + value.description, + style: context.textTheme.bodyMedium, + ), + trailing: Icon( + isSelected + ? Icons.radio_button_checked + : Icons.radio_button_unchecked, + size: 24, + color: isSelected + ? ColorsManager.primaryColorWithOpacity + : ColorsManager.textGray, + ), + onTap: () { + if (!isSelected) { + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + value: value.value, + condition: selectedFunctionData?.condition, + valueDescription: + selectedFunctionData?.valueDescription, + ), + ), + ); + } + }, ); }, ); diff --git a/lib/pages/routiens/models/device_functions.dart b/lib/pages/routiens/models/device_functions.dart index a1f5725e..59b63a4f 100644 --- a/lib/pages/routiens/models/device_functions.dart +++ b/lib/pages/routiens/models/device_functions.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - abstract class DeviceFunction { final String deviceId; final String deviceName; @@ -24,7 +22,6 @@ class DeviceFunctionData { final dynamic value; final String? condition; final String? valueDescription; - final UniqueKey uniqueKey; DeviceFunctionData({ required this.entityId, @@ -34,7 +31,7 @@ class DeviceFunctionData { required this.value, this.condition, this.valueDescription, - }) : uniqueKey = UniqueKey(); + }); Map toJson() { return { diff --git a/lib/pages/routiens/models/routine_item.dart b/lib/pages/routiens/models/routine_item.dart index 33920634..08263d53 100644 --- a/lib/pages/routiens/models/routine_item.dart +++ b/lib/pages/routiens/models/routine_item.dart @@ -1,29 +1,30 @@ -import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +// import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; -class RoutineItem { - final AllDevicesModel device; - final String? function; - final dynamic value; +// class RoutineItem { +// final AllDevicesModel device; +// final String? function; +// final dynamic value; - RoutineItem({ - required this.device, - this.function, - this.value, - }); +// RoutineItem({ +// required this.device, +// this.function, +// this.value, +// }); - Map toMap() { - return { - 'device': device, - 'function': function, - 'value': value, - }; - } +// Map toMap() { +// return { +// 'device': device, +// 'function': function, +// 'value': value, +// }; +// } - factory RoutineItem.fromMap(Map map) { - return RoutineItem( - device: map['device'] as AllDevicesModel, - function: map['function'], - value: map['value'], - ); - } -} +// factory RoutineItem.fromMap(Map map) { +// return RoutineItem( +// device: map['device'] as AllDevicesModel, +// function: map['function'], +// value: map['value'], +// ); +// } +// } +// : uniqueCustomId = uniqueCustomId ?? const Uuid().v4() diff --git a/lib/pages/routiens/models/routine_model.dart b/lib/pages/routiens/models/routine_model.dart index 2db2247f..8035b044 100644 --- a/lib/pages/routiens/models/routine_model.dart +++ b/lib/pages/routiens/models/routine_model.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - class ScenesModel { final String id; final String name; @@ -7,11 +5,13 @@ class ScenesModel { final String type; final String? icon; - ScenesModel({required this.id, required this.name, required this.status, required this.type, this.icon}); - - factory ScenesModel.fromRawJson(String str) => ScenesModel.fromJson(json.decode(str)); - - String toRawJson() => json.encode(toJson()); + ScenesModel({ + required this.id, + required this.name, + required this.status, + required this.type, + this.icon, + }); factory ScenesModel.fromJson(Map json) => ScenesModel( id: json["id"], diff --git a/lib/pages/routiens/widgets/dragable_card.dart b/lib/pages/routiens/widgets/dragable_card.dart index 197ea0d2..2b25b9c4 100644 --- a/lib/pages/routiens/widgets/dragable_card.dart +++ b/lib/pages/routiens/widgets/dragable_card.dart @@ -22,9 +22,8 @@ class DraggableCard extends StatelessWidget { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - final deviceFunctions = state.selectedFunctions - .where((f) => f.entityId == deviceData['deviceId']) - .toList(); + final deviceFunctions = + state.selectedFunctions[deviceData['uniqueCustomId']] ?? []; return Draggable>( data: deviceData, @@ -45,7 +44,7 @@ class DraggableCard extends StatelessWidget { color: ColorsManager.whiteColors, child: Container( padding: const EdgeInsets.all(8), - width: 90, + width: 110, height: deviceFunctions.isEmpty ? 123 : null, child: Column( mainAxisSize: MainAxisSize.min, diff --git a/lib/pages/routiens/widgets/routine_devices.dart b/lib/pages/routiens/widgets/routine_devices.dart index 20d47007..67d1dc85 100644 --- a/lib/pages/routiens/widgets/routine_devices.dart +++ b/lib/pages/routiens/widgets/routine_devices.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart'; import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; +import 'package:uuid/uuid.dart'; class RoutineDevices extends StatelessWidget { const RoutineDevices({super.key}); diff --git a/lib/pages/routiens/widgets/then_container.dart b/lib/pages/routiens/widgets/then_container.dart index 9c868f09..82a5feea 100644 --- a/lib/pages/routiens/widgets/then_container.dart +++ b/lib/pages/routiens/widgets/then_container.dart @@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/helper/dialog_helper/device_dialog_helper.dart'; import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; +import 'package:uuid/uuid.dart'; class ThenContainer extends StatelessWidget { const ThenContainer({super.key}); @@ -15,34 +16,38 @@ class ThenContainer extends StatelessWidget { builder: (context, state) { return DragTarget>( builder: (context, candidateData, rejectedData) { - return Container( - padding: const EdgeInsets.all(16), - width: double.infinity, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('THEN', - style: - TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), - const SizedBox(height: 16), - Wrap( - spacing: 8, - runSpacing: 8, - children: state.thenItems - .map((item) => DraggableCard( - // key: Key(item['key']!), - imagePath: item['imagePath']!, - title: item['title']!, - deviceData: item, - )) - .toList(), - ), - ], + return SingleChildScrollView( + child: Container( + padding: const EdgeInsets.all(16), + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('THEN', + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.bold)), + const SizedBox(height: 16), + Wrap( + spacing: 8, + runSpacing: 8, + children: state.thenItems + .map((item) => DraggableCard( + // key: Key(item['key']!), + imagePath: item['imagePath']!, + title: item['title']!, + deviceData: item, + )) + .toList(), + ), + ], + ), ), ); }, onWillAccept: (data) => data != null, onAccept: (data) async { + final uniqueCustomId = const Uuid().v4(); + data['uniqueCustomId'] = uniqueCustomId; final result = await DeviceDialogHelper.showDeviceDialog(context, data); // if (result != null) { diff --git a/pubspec.lock b/pubspec.lock index 3e12d6c5..cfd8b310 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" cupertino_icons: dependency: "direct main" description: @@ -137,6 +145,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" fl_chart: dependency: "direct main" description: @@ -533,6 +549,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: @@ -589,6 +613,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" vector_graphics: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c8f42ea5..3aa81860 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -51,6 +51,7 @@ dependencies: flutter_dotenv: ^5.1.0 fl_chart: ^0.69.0 time_picker_spinner: ^1.0.0 + uuid: ^4.4.0 dev_dependencies: flutter_test: From 1f49b9bc496d9ab68f99c4533506330bd3d890cb Mon Sep 17 00:00:00 2001 From: Abdullah Alassaf Date: Sun, 24 Nov 2024 01:12:28 +0300 Subject: [PATCH 16/23] Added tab to run to if container --- .../bloc/routine_bloc/routine_bloc.dart | 24 ++++---- .../bloc/routine_bloc/routine_event.dart | 5 +- lib/pages/routiens/widgets/dragable_card.dart | 8 +-- lib/pages/routiens/widgets/if_container.dart | 59 ++++++++++++------- 4 files changed, 54 insertions(+), 42 deletions(-) diff --git a/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart b/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart index 2b711f39..babd820a 100644 --- a/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart +++ b/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart @@ -11,6 +11,9 @@ part 'routine_state.dart'; const spaceId = '25c96044-fadf-44bb-93c7-3c079e527ce6'; class RoutineBloc extends Bloc { + bool isAutomation = false; + bool isTabToRun = false; + RoutineBloc() : super(const RoutineState()) { on(_onAddToIfContainer); on(_onAddToThenContainer); @@ -23,17 +26,15 @@ class RoutineBloc extends Bloc { void _onAddToIfContainer(AddToIfContainer event, Emitter emit) { // if (!_isDuplicate(state.ifItems, event.item)) { - final updatedIfItems = List>.from(state.ifItems) - ..add(event.item); + final updatedIfItems = List>.from(state.ifItems)..add(event.item); + isTabToRun = event.isTabToRun; emit(state.copyWith(ifItems: updatedIfItems)); // } } - void _onAddToThenContainer( - AddToThenContainer event, Emitter emit) { + void _onAddToThenContainer(AddToThenContainer event, Emitter emit) { // if (!_isDuplicate(state.thenItems, event.item)) { - final updatedThenItems = List>.from(state.thenItems) - ..add(event.item); + final updatedThenItems = List>.from(state.thenItems)..add(event.item); emit(state.copyWith(thenItems: updatedThenItems)); // } } @@ -47,9 +48,8 @@ class RoutineBloc extends Bloc { void _onRemoveFunction(RemoveFunction event, Emitter emit) { final functions = List.from(state.selectedFunctions) - ..removeWhere((f) => - f.functionCode == event.function.functionCode && - f.value == event.function.value); + ..removeWhere( + (f) => f.functionCode == event.function.functionCode && f.value == event.function.value); emit(state.copyWith(selectedFunctions: functions)); } @@ -64,8 +64,7 @@ class RoutineBloc extends Bloc { // item['title'] == newItem['title']); // } - Future _onLoadScenes( - LoadScenes event, Emitter emit) async { + Future _onLoadScenes(LoadScenes event, Emitter emit) async { emit(state.copyWith(isLoading: true, errorMessage: null)); try { @@ -82,8 +81,7 @@ class RoutineBloc extends Bloc { } } - Future _onLoadAutomation( - LoadAutomation event, Emitter emit) async { + Future _onLoadAutomation(LoadAutomation event, Emitter emit) async { emit(state.copyWith(isLoading: true, errorMessage: null)); try { diff --git a/lib/pages/routiens/bloc/routine_bloc/routine_event.dart b/lib/pages/routiens/bloc/routine_bloc/routine_event.dart index 3321af5e..8ac1efd7 100644 --- a/lib/pages/routiens/bloc/routine_bloc/routine_event.dart +++ b/lib/pages/routiens/bloc/routine_bloc/routine_event.dart @@ -9,11 +9,12 @@ abstract class RoutineEvent extends Equatable { class AddToIfContainer extends RoutineEvent { final Map item; + final bool isTabToRun; - const AddToIfContainer(this.item); + const AddToIfContainer(this.item, this.isTabToRun); @override - List get props => [item]; + List get props => [item, isTabToRun]; } class AddToThenContainer extends RoutineEvent { diff --git a/lib/pages/routiens/widgets/dragable_card.dart b/lib/pages/routiens/widgets/dragable_card.dart index 197ea0d2..fa4f7f7c 100644 --- a/lib/pages/routiens/widgets/dragable_card.dart +++ b/lib/pages/routiens/widgets/dragable_card.dart @@ -22,9 +22,8 @@ class DraggableCard extends StatelessWidget { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - final deviceFunctions = state.selectedFunctions - .where((f) => f.entityId == deviceData['deviceId']) - .toList(); + final deviceFunctions = + state.selectedFunctions.where((f) => f.entityId == deviceData['deviceId']).toList(); return Draggable>( data: deviceData, @@ -39,8 +38,7 @@ class DraggableCard extends StatelessWidget { ); } - Widget _buildCardContent( - BuildContext context, List deviceFunctions) { + Widget _buildCardContent(BuildContext context, List deviceFunctions) { return Card( color: ColorsManager.whiteColors, child: Container( diff --git a/lib/pages/routiens/widgets/if_container.dart b/lib/pages/routiens/widgets/if_container.dart index 779453f1..a7afa245 100644 --- a/lib/pages/routiens/widgets/if_container.dart +++ b/lib/pages/routiens/widgets/if_container.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/helper/dialog_helper/device_dialog_helper.dart'; import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; class IfContainer extends StatelessWidget { const IfContainer({super.key}); @@ -19,35 +20,49 @@ class IfContainer extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('IF', - style: - TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const Text('IF', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 16), - Wrap( - spacing: 8, - runSpacing: 8, - children: state.ifItems - .map((item) => DraggableCard( - key: Key(item['key']!), - imagePath: item['imagePath']!, - title: item['title']!, - deviceData: item, - )) - .toList(), - ), + if (context.read().isTabToRun) + const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + DraggableCard( + imagePath: Assets.tabToRun, + title: 'Tab to run', + deviceData: {}, + ), + ], + ), + if (!context.read().isTabToRun) + Wrap( + spacing: 8, + runSpacing: 8, + children: state.ifItems + .map((item) => DraggableCard( + // key: Key(item['key']!), + imagePath: item['imagePath'] ?? '', + title: item['title'] ?? 'Tab to run', + deviceData: item, + )) + .toList(), + ), ], ), ); }, onWillAccept: (data) => data != null, onAccept: (data) async { - final result = - await DeviceDialogHelper.showDeviceDialog(context, data); - if (result != null) { - context.read().add(AddToIfContainer(result)); - } else if (!['AC', '1G', '2G', '3G'] - .contains(data['productType'])) { - context.read().add(AddToIfContainer(data)); + if (!context.read().isTabToRun) { + if (data['deviceId'] == 'tab_to_run') { + context.read().add(AddToIfContainer(data, true)); + } else { + final result = await DeviceDialogHelper.showDeviceDialog(context, data); + if (result != null) { + context.read().add(AddToIfContainer(result, false)); + } else if (!['AC', '1G', '2G', '3G'].contains(data['productType'])) { + context.read().add(AddToIfContainer(data, false)); + } + } } }, ); From 5a9729fe1046d1f069602ca5017e811f777d51fe Mon Sep 17 00:00:00 2001 From: ashrafzarkanisala Date: Sun, 24 Nov 2024 09:07:01 +0300 Subject: [PATCH 17/23] push add delay device --- .../all_devices/models/devices_model.dart | 1 + .../bloc/routine_bloc/routine_bloc.dart | 34 ++++----- .../bloc/routine_bloc/routine_event.dart | 2 +- lib/pages/routiens/helper/delay_helper.dart | 76 +++++++++++++++++++ .../models/delay/delay_fucntions.dart | 28 +++++++ .../conditions_routines_devices_view.dart | 1 + lib/pages/routiens/widgets/dialog_header.dart | 4 + lib/pages/routiens/widgets/dragable_card.dart | 16 ++-- .../routiens/widgets/routine_devices.dart | 2 +- .../routiens/widgets/then_container.dart | 44 ++++++----- 10 files changed, 165 insertions(+), 43 deletions(-) create mode 100644 lib/pages/routiens/helper/delay_helper.dart create mode 100644 lib/pages/routiens/models/delay/delay_fucntions.dart 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 b4f72287..b7e4f010 100644 --- a/lib/pages/device_managment/all_devices/models/devices_model.dart +++ b/lib/pages/device_managment/all_devices/models/devices_model.dart @@ -277,6 +277,7 @@ SOS ThreeGangCountdown3Function( deviceId: uuid ?? '', deviceName: name ?? ''), ]; + default: return []; } diff --git a/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart b/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart index 44e2d84b..197da85f 100644 --- a/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart +++ b/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart @@ -25,23 +25,23 @@ class RoutineBloc extends Bloc { } void _onAddToIfContainer(AddToIfContainer event, Emitter emit) { - // if (!_isDuplicate(state.ifItems, event.item)) { - final updatedIfItems = List>.from(state.ifItems)..add(event.item); + final updatedIfItems = List>.from(state.ifItems) + ..add(event.item); isTabToRun = event.isTabToRun; emit(state.copyWith(ifItems: updatedIfItems)); - // } } - void _onAddToThenContainer(AddToThenContainer event, Emitter emit) { - // if (!_isDuplicate(state.thenItems, event.item)) { - final updatedThenItems = List>.from(state.thenItems)..add(event.item); + void _onAddToThenContainer( + AddToThenContainer event, Emitter emit) { + final updatedThenItems = List>.from(state.thenItems) + ..add(event.item); emit(state.copyWith(thenItems: updatedThenItems)); - // } } - void _onAddFunctionsToRoutine(AddFunctionToRoutine event, Emitter emit) { - debugPrint(event.uniqueCustomId.toString()); - debugPrint(event.functions.toString()); + void _onAddFunctionsToRoutine( + AddFunctionToRoutine event, Emitter emit) { + debugPrint('Adding functions to routine: ${event.functions}'); + debugPrint('Unique Custom ID: ${event.uniqueCustomId}'); final currentSelectedFunctions = Map>.from(state.selectedFunctions); @@ -52,6 +52,7 @@ class RoutineBloc extends Bloc { } emit(state.copyWith(selectedFunctions: currentSelectedFunctions)); + debugPrint('Updated selected functions: $currentSelectedFunctions'); } // void _onRemoveFunction(RemoveFunction event, Emitter emit) { @@ -66,14 +67,8 @@ class RoutineBloc extends Bloc { // emit(state.copyWith(selectedFunctions: [])); // } - // bool _isDuplicate( - // List> items, Map newItem) { - // return items.any((item) => - // item['imagePath'] == newItem['imagePath'] && - // item['title'] == newItem['title']); - // } - - Future _onLoadScenes(LoadScenes event, Emitter emit) async { + Future _onLoadScenes( + LoadScenes event, Emitter emit) async { emit(state.copyWith(isLoading: true, errorMessage: null)); try { @@ -90,7 +85,8 @@ class RoutineBloc extends Bloc { } } - Future _onLoadAutomation(LoadAutomation event, Emitter emit) async { + Future _onLoadAutomation( + LoadAutomation event, Emitter emit) async { emit(state.copyWith(isLoading: true, errorMessage: null)); try { diff --git a/lib/pages/routiens/bloc/routine_bloc/routine_event.dart b/lib/pages/routiens/bloc/routine_bloc/routine_event.dart index 37c1e564..9e7c6f16 100644 --- a/lib/pages/routiens/bloc/routine_bloc/routine_event.dart +++ b/lib/pages/routiens/bloc/routine_bloc/routine_event.dart @@ -49,7 +49,7 @@ class AddFunctionToRoutine extends RoutineEvent { final String uniqueCustomId; const AddFunctionToRoutine(this.functions, this.uniqueCustomId); @override - List get props => [functions]; + List get props => [functions, uniqueCustomId]; } class RemoveFunction extends RoutineEvent { diff --git a/lib/pages/routiens/helper/delay_helper.dart b/lib/pages/routiens/helper/delay_helper.dart new file mode 100644 index 00000000..8ecff897 --- /dev/null +++ b/lib/pages/routiens/helper/delay_helper.dart @@ -0,0 +1,76 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/functions_bloc/functions_bloc_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class DelayHelper { + static Future?> showDelayPickerDialog( + BuildContext context, String uniqueCustomId) async { + int hours = 0; + int minutes = 0; + + return showDialog?>( + context: context, + builder: (BuildContext context) { + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: Container( + width: 600, + height: 300, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const DialogHeader('Select Delay Duration'), + Expanded( + child: CupertinoTimerPicker( + mode: CupertinoTimerPickerMode.hm, + initialTimerDuration: + Duration(hours: hours, minutes: minutes), + onTimerDurationChanged: (Duration newDuration) { + hours = newDuration.inHours; + minutes = newDuration.inMinutes % 60; + }, + ), + ), + DialogFooter( + onCancel: () { + Navigator.of(context).pop(); + }, + onConfirm: () { + int totalSeconds = (hours * 3600) + (minutes * 60); + context.read().add(AddFunctionToRoutine( + [ + DeviceFunctionData( + entityId: 'delay', + functionCode: 'delay', + operationName: 'Delay', + value: totalSeconds, + ) + ], + uniqueCustomId, + )); + + Navigator.pop(context, { + 'deviceId': 'delay', + 'value': totalSeconds, + }); + }, + isConfirmEnabled: true, + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/pages/routiens/models/delay/delay_fucntions.dart b/lib/pages/routiens/models/delay/delay_fucntions.dart new file mode 100644 index 00000000..ff04251a --- /dev/null +++ b/lib/pages/routiens/models/delay/delay_fucntions.dart @@ -0,0 +1,28 @@ +import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart'; +import 'package:syncrow_web/pages/routiens/models/gang_switches/switch_operational_value.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class DelayFunction extends BaseSwitchFunction { + DelayFunction({required super.deviceId, required super.deviceName}) + : super( + code: 'delay', + operationName: 'Delay', + icon: Assets.delay, + ); + + @override + List getOperationalValues() => [ + SwitchOperationalValue( + icon: '', + description: "Duration in seconds", + value: 0.0, + minValue: 0, + maxValue: 43200, + stepValue: 1, + ), + ]; + + int convertToSeconds(int hours, int minutes) { + return (hours * 3600) + (minutes * 60); + } +} diff --git a/lib/pages/routiens/widgets/conditions_routines_devices_view.dart b/lib/pages/routiens/widgets/conditions_routines_devices_view.dart index 7bda1d51..89b1f3f0 100644 --- a/lib/pages/routiens/widgets/conditions_routines_devices_view.dart +++ b/lib/pages/routiens/widgets/conditions_routines_devices_view.dart @@ -91,6 +91,7 @@ class ConditionsRoutinesDevicesView extends StatelessWidget { 'deviceId': 'delay', 'type': 'action', 'name': 'Delay the action', + 'uniqueCustomId': '', }, ), ], diff --git a/lib/pages/routiens/widgets/dialog_header.dart b/lib/pages/routiens/widgets/dialog_header.dart index 6701b5b0..4fe1f0b1 100644 --- a/lib/pages/routiens/widgets/dialog_header.dart +++ b/lib/pages/routiens/widgets/dialog_header.dart @@ -9,7 +9,11 @@ class DialogHeader extends StatelessWidget { @override Widget build(BuildContext context) { return Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ + const SizedBox( + height: 10, + ), Text( title, style: Theme.of(context).textTheme.bodyMedium!.copyWith( diff --git a/lib/pages/routiens/widgets/dragable_card.dart b/lib/pages/routiens/widgets/dragable_card.dart index f3d8b355..02c802bc 100644 --- a/lib/pages/routiens/widgets/dragable_card.dart +++ b/lib/pages/routiens/widgets/dragable_card.dart @@ -10,38 +10,44 @@ class DraggableCard extends StatelessWidget { final String imagePath; final String title; final Map deviceData; + final EdgeInsetsGeometry? padding; const DraggableCard({ super.key, required this.imagePath, required this.title, required this.deviceData, + this.padding, }); @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - final deviceFunctions = state.selectedFunctions[deviceData['uniqueCustomId']] ?? []; + final deviceFunctions = + state.selectedFunctions[deviceData['uniqueCustomId']] ?? []; return Draggable>( data: deviceData, feedback: Transform.rotate( angle: -0.1, - child: _buildCardContent(context, deviceFunctions), + child: + _buildCardContent(context, deviceFunctions, padding: padding), ), childWhenDragging: _buildGreyContainer(), - child: _buildCardContent(context, deviceFunctions), + child: _buildCardContent(context, deviceFunctions, padding: padding), ); }, ); } - Widget _buildCardContent(BuildContext context, List deviceFunctions) { + Widget _buildCardContent( + BuildContext context, List deviceFunctions, + {EdgeInsetsGeometry? padding}) { return Card( color: ColorsManager.whiteColors, child: Container( - padding: const EdgeInsets.all(8), + padding: padding ?? const EdgeInsets.all(16), width: 110, height: deviceFunctions.isEmpty ? 123 : null, child: Column( diff --git a/lib/pages/routiens/widgets/routine_devices.dart b/lib/pages/routiens/widgets/routine_devices.dart index 67d1dc85..02c1b4fa 100644 --- a/lib/pages/routiens/widgets/routine_devices.dart +++ b/lib/pages/routiens/widgets/routine_devices.dart @@ -3,7 +3,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart'; import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; -import 'package:uuid/uuid.dart'; class RoutineDevices extends StatelessWidget { const RoutineDevices({super.key}); @@ -41,6 +40,7 @@ class RoutineDevices extends StatelessWidget { 'deviceId': device.uuid, 'productType': device.productType, 'functions': device.functions, + 'uniqueCustomId': '', }, ); }).toList(), diff --git a/lib/pages/routiens/widgets/then_container.dart b/lib/pages/routiens/widgets/then_container.dart index 82a5feea..5d71e405 100644 --- a/lib/pages/routiens/widgets/then_container.dart +++ b/lib/pages/routiens/widgets/then_container.dart @@ -3,8 +3,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/helper/delay_helper.dart'; import 'package:syncrow_web/pages/routiens/helper/dialog_helper/device_dialog_helper.dart'; import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:uuid/uuid.dart'; class ThenContainer extends StatelessWidget { @@ -36,6 +38,8 @@ class ThenContainer extends StatelessWidget { imagePath: item['imagePath']!, title: item['title']!, deviceData: item, + padding: EdgeInsets.symmetric( + horizontal: 4, vertical: 8), )) .toList(), ), @@ -47,26 +51,32 @@ class ThenContainer extends StatelessWidget { onWillAccept: (data) => data != null, onAccept: (data) async { final uniqueCustomId = const Uuid().v4(); - data['uniqueCustomId'] = uniqueCustomId; - final result = - await DeviceDialogHelper.showDeviceDialog(context, data); - // if (result != null) { - // for (var function in routineBloc.state.selectedFunctions) { - // routineBloc.add(AddToThenContainer( - // { - // 'item': function, - // 'imagePath': data['imagePath'], - // 'title': data['name'], - // } - // )); - // } - // } + final mutableData = Map.from(data); + mutableData['uniqueCustomId'] = uniqueCustomId; + + if (mutableData['deviceId'] == 'delay') { + final result = await DelayHelper.showDelayPickerDialog( + context, mutableData['uniqueCustomId']); + + if (result != null) { + context.read().add(AddToThenContainer({ + ...mutableData, + 'imagePath': Assets.delay, + 'title': 'Delay', + })); + } + return; + } + + final result = + await DeviceDialogHelper.showDeviceDialog(context, mutableData); + if (result != null) { - context.read().add(AddToThenContainer(data)); + context.read().add(AddToThenContainer(mutableData)); } else if (!['AC', '1G', '2G', '3G'] - .contains(data['productType'])) { - context.read().add(AddToThenContainer(data)); + .contains(mutableData['productType'])) { + context.read().add(AddToThenContainer(mutableData)); } }, ); From 87c47a74ce465150478bc8d0d28c1ab17c1737da Mon Sep 17 00:00:00 2001 From: ashrafzarkanisala Date: Sun, 24 Nov 2024 11:17:54 +0300 Subject: [PATCH 18/23] push calling create scene --- .../common/text_field/custom_text_field.dart | 18 +- .../view/device_managment_page.dart | 89 ++++--- .../widgets/device_search_filters.dart | 10 +- .../bloc/routine_bloc/routine_bloc.dart | 103 +++++++- .../bloc/routine_bloc/routine_event.dart | 20 ++ .../bloc/routine_bloc/routine_state.dart | 10 + .../routiens/helper/save_routine_helper.dart | 132 ++++++++++ lib/pages/routiens/helper/setting_helper.dart | 191 ++++++++++----- .../create_scene/create_scene_model.dart | 230 ++++++++++++++++++ .../widgets/routine_search_and_buttons.dart | 30 ++- .../widgets/search_bar_condition_title.dart | 5 +- lib/services/routines_api.dart | 33 +-- lib/utils/constants/api_const.dart | 19 +- 13 files changed, 759 insertions(+), 131 deletions(-) create mode 100644 lib/pages/routiens/helper/save_routine_helper.dart create mode 100644 lib/pages/routiens/models/create_scene/create_scene_model.dart diff --git a/lib/pages/common/text_field/custom_text_field.dart b/lib/pages/common/text_field/custom_text_field.dart index 23f033b6..250ff110 100644 --- a/lib/pages/common/text_field/custom_text_field.dart +++ b/lib/pages/common/text_field/custom_text_field.dart @@ -18,6 +18,7 @@ class StatefulTextField extends StatefulWidget { this.padding, this.icon, this.hintColor, + required this.onChanged, }); final String title; @@ -32,6 +33,7 @@ class StatefulTextField extends StatefulWidget { final double? padding; final IconData? icon; final Color? hintColor; + final Function(String)? onChanged; @override State createState() => _StatefulTextFieldState(); @@ -59,6 +61,7 @@ class _StatefulTextFieldState extends State { padding: widget.padding, icon: widget.icon, hintColor: widget.hintColor, + onChanged: widget.onChanged, ); } } @@ -78,6 +81,7 @@ class CustomTextField extends StatelessWidget { this.padding, this.icon, this.hintColor, + required this.onChanged, }); final String title; @@ -92,6 +96,7 @@ class CustomTextField extends StatelessWidget { final double? padding; final IconData? icon; final Color? hintColor; + final Function(String)? onChanged; @override Widget build(BuildContext context) { @@ -120,14 +125,21 @@ class CustomTextField extends StatelessWidget { style: const TextStyle(color: Colors.black), decoration: InputDecoration( hintText: hintText, - hintStyle: TextStyle(fontSize: 12, color: hintColor ?? ColorsManager.blackColor), - contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: padding ?? 10), + hintStyle: TextStyle( + fontSize: 12, color: hintColor ?? ColorsManager.blackColor), + contentPadding: EdgeInsets.symmetric( + horizontal: 12, vertical: padding ?? 10), border: InputBorder.none, - suffixIcon: icon != null ? Icon(icon, color: ColorsManager.greyColor) : null, + suffixIcon: icon != null + ? Icon(icon, color: ColorsManager.greyColor) + : null, ), onFieldSubmitted: (_) { onSubmittedFun!(); }, + onChanged: (value) { + onChanged!(value); + }, ), ), ), diff --git a/lib/pages/device_managment/all_devices/view/device_managment_page.dart b/lib/pages/device_managment/all_devices/view/device_managment_page.dart index e0661b50..ffc57131 100644 --- a/lib/pages/device_managment/all_devices/view/device_managment_page.dart +++ b/lib/pages/device_managment/all_devices/view/device_managment_page.dart @@ -2,8 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/bloc/switch_tabs/switch_tabs_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/widgets/device_managment_body.dart'; import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart'; import 'package:syncrow_web/pages/routiens/view/create_new_routine_view.dart'; +import 'package:syncrow_web/pages/routiens/view/routines_view.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -20,7 +22,8 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout { create: (context) => DeviceManagementBloc()..add(FetchDevices()), ), BlocProvider( - create: (context) => SwitchTabsBloc()..add(const TriggerSwitchTabsEvent(false)), + create: (context) => + SwitchTabsBloc()..add(const TriggerSwitchTabsEvent(false)), ), ], child: WebScaffold( @@ -30,7 +33,8 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout { style: Theme.of(context).textTheme.headlineLarge, ), ), - centerBody: BlocBuilder(builder: (context, state) { + centerBody: BlocBuilder( + builder: (context, state) { return Row( mainAxisSize: MainAxisSize.min, children: [ @@ -39,16 +43,21 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout { backgroundColor: null, ), onPressed: () { - context.read().add(const TriggerSwitchTabsEvent(false)); + context + .read() + .add(const TriggerSwitchTabsEvent(false)); }, child: Text( 'Devices', style: context.textTheme.titleMedium?.copyWith( - color: state is SelectedTabState && state.selectedTab == false - ? ColorsManager.whiteColors - : ColorsManager.grayColor, - fontWeight: - (state is SelectedTabState) && state.selectedTab == false ? FontWeight.w700 : FontWeight.w400, + color: + state is SelectedTabState && state.selectedTab == false + ? ColorsManager.whiteColors + : ColorsManager.grayColor, + fontWeight: (state is SelectedTabState) && + state.selectedTab == false + ? FontWeight.w700 + : FontWeight.w400, ), ), ), @@ -57,16 +66,21 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout { backgroundColor: null, ), onPressed: () { - context.read().add(const TriggerSwitchTabsEvent(true)); + context + .read() + .add(const TriggerSwitchTabsEvent(true)); }, child: Text( 'Routines', style: context.textTheme.titleMedium?.copyWith( - color: (state is SelectedTabState) && state.selectedTab == true - ? ColorsManager.whiteColors - : ColorsManager.grayColor, + color: + (state is SelectedTabState) && state.selectedTab == true + ? ColorsManager.whiteColors + : ColorsManager.grayColor, fontWeight: - (state is SelectedTabState) && state.selectedTab == true ? FontWeight.w700 : FontWeight.w400, + (state is SelectedTabState) && state.selectedTab == true + ? FontWeight.w700 + : FontWeight.w400, ), ), ), @@ -74,30 +88,31 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout { ); }), rightBody: const NavigateHomeGridView(), - scaffoldBody: CreateNewRoutineView(), - // BlocBuilder(builder: (context, state) { - // if (state is SelectedTabState && state.selectedTab) { - // return const RoutinesView(); - // } - // if (state is ShowCreateRoutineState && state.showCreateRoutine) { - // return const CreateNewRoutineView(); - // } - // - // return BlocBuilder( - // builder: (context, deviceState) { - // if (deviceState is DeviceManagementLoading) { - // return const Center(child: CircularProgressIndicator()); - // } else if (deviceState is DeviceManagementLoaded || deviceState is DeviceManagementFiltered) { - // final devices = - // (deviceState as dynamic).devices ?? (deviceState as DeviceManagementFiltered).filteredDevices; - // - // return DeviceManagementBody(devices: devices); - // } else { - // return const Center(child: Text('Error fetching Devices')); - // } - // }, - // ); - // }), + scaffoldBody: BlocBuilder( + builder: (context, state) { + if (state is SelectedTabState && state.selectedTab) { + return const RoutinesView(); + } + if (state is ShowCreateRoutineState && state.showCreateRoutine) { + return const CreateNewRoutineView(); + } + + return BlocBuilder( + builder: (context, deviceState) { + if (deviceState is DeviceManagementLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (deviceState is DeviceManagementLoaded || + deviceState is DeviceManagementFiltered) { + final devices = (deviceState as dynamic).devices ?? + (deviceState as DeviceManagementFiltered).filteredDevices; + + return DeviceManagementBody(devices: devices); + } else { + return const Center(child: Text('Error fetching Devices')); + } + }, + ); + }), ), ); } diff --git a/lib/pages/device_managment/all_devices/widgets/device_search_filters.dart b/lib/pages/device_managment/all_devices/widgets/device_search_filters.dart index d9e47aa6..e3bec220 100644 --- a/lib/pages/device_managment/all_devices/widgets/device_search_filters.dart +++ b/lib/pages/device_managment/all_devices/widgets/device_search_filters.dart @@ -12,7 +12,8 @@ class DeviceSearchFilters extends StatefulWidget { State createState() => _DeviceSearchFiltersState(); } -class _DeviceSearchFiltersState extends State with HelperResponsiveLayout { +class _DeviceSearchFiltersState extends State + with HelperResponsiveLayout { final TextEditingController communityController = TextEditingController(); final TextEditingController unitNameController = TextEditingController(); final TextEditingController productNameController = TextEditingController(); @@ -34,7 +35,8 @@ class _DeviceSearchFiltersState extends State with HelperRe const SizedBox(width: 20), _buildSearchField("Space Name", unitNameController, 200), const SizedBox(width: 20), - _buildSearchField("Device Name / Product Name", productNameController, 300), + _buildSearchField( + "Device Name / Product Name", productNameController, 300), const SizedBox(width: 20), _buildSearchResetButtons(), ], @@ -59,7 +61,8 @@ class _DeviceSearchFiltersState extends State with HelperRe ); } - Widget _buildSearchField(String title, TextEditingController controller, double width) { + Widget _buildSearchField( + String title, TextEditingController controller, double width) { return Container( child: StatefulTextField( title: title, @@ -73,6 +76,7 @@ class _DeviceSearchFiltersState extends State with HelperRe community: communityController.text, searchField: true)); }, + onChanged: (p0) {}, ), ); } diff --git a/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart b/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart index 197da85f..1490b12d 100644 --- a/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart +++ b/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart @@ -1,6 +1,9 @@ +import 'dart:async'; + import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/routiens/models/create_scene/create_scene_model.dart'; import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; import 'package:syncrow_web/pages/routiens/models/routine_model.dart'; import 'package:syncrow_web/services/routines_api.dart'; @@ -20,6 +23,9 @@ class RoutineBloc extends Bloc { on(_onLoadScenes); on(_onLoadAutomation); on(_onAddFunctionsToRoutine); + on(_onSearchRoutines); + on(_onAddSelectedIcon); + on(_onCreateScene); // on(_onRemoveFunction); // on(_onClearFunctions); } @@ -40,8 +46,6 @@ class RoutineBloc extends Bloc { void _onAddFunctionsToRoutine( AddFunctionToRoutine event, Emitter emit) { - debugPrint('Adding functions to routine: ${event.functions}'); - debugPrint('Unique Custom ID: ${event.uniqueCustomId}'); final currentSelectedFunctions = Map>.from(state.selectedFunctions); @@ -52,7 +56,6 @@ class RoutineBloc extends Bloc { } emit(state.copyWith(selectedFunctions: currentSelectedFunctions)); - debugPrint('Updated selected functions: $currentSelectedFunctions'); } // void _onRemoveFunction(RemoveFunction event, Emitter emit) { @@ -80,7 +83,7 @@ class RoutineBloc extends Bloc { } catch (e) { emit(state.copyWith( isLoading: false, - errorMessage: 'Something went wrong', + errorMessage: 'Failed to load scenes', )); } } @@ -98,7 +101,97 @@ class RoutineBloc extends Bloc { } catch (e) { emit(state.copyWith( isLoading: false, - errorMessage: 'Something went wrong', + errorMessage: 'Failed to load automations', + )); + } + } + + FutureOr _onSearchRoutines( + SearchRoutines event, Emitter emit) { + emit(state.copyWith(routineName: event.query)); + } + + FutureOr _onAddSelectedIcon( + AddSelectedIcon event, Emitter emit) { + emit(state.copyWith(selectedIcon: event.icon)); + } + + bool _isFirstActionDelay(List> actions) { + if (actions.isEmpty) return false; + return actions.first['deviceId'] == 'delay'; + } + + Future _onCreateScene( + CreateSceneEvent event, Emitter emit) async { + try { + // Check if first action is delay + if (_isFirstActionDelay(state.thenItems)) { + emit(state.copyWith( + errorMessage: 'Cannot have delay as the first action', + isLoading: false, + )); + return; + } + + emit(state.copyWith(isLoading: true)); + + final actions = state.thenItems + .map((item) { + final functions = + state.selectedFunctions[item['uniqueCustomId']] ?? []; + if (functions.isEmpty) return null; + + final function = functions.first; + if (item['deviceId'] == 'delay') { + return CreateSceneAction( + entityId: function.entityId, + actionExecutor: 'delay', + executorProperty: CreateSceneExecutorProperty( + functionCode: '', + functionValue: '', + delaySeconds: function.value, + ), + ); + } + + return CreateSceneAction( + entityId: function.entityId, + actionExecutor: 'device_issue', + executorProperty: CreateSceneExecutorProperty( + functionCode: function.functionCode.toString(), + functionValue: function.value, + delaySeconds: 0, + ), + ); + }) + .whereType() + .toList(); + + final createSceneModel = CreateSceneModel( + spaceUuid: spaceId, + iconId: state.selectedIcon ?? '', + showInDevice: true, + sceneName: state.routineName ?? '', + decisionExpr: 'and', + actions: actions, + ); + + final result = await SceneApi.createScene(createSceneModel); + if (result['success']) { + emit(state.copyWith( + isLoading: false, + errorMessage: null, + )); + } else { + emit(state.copyWith( + isLoading: false, + errorMessage: result['message'], + )); + } + } catch (e) { + emit(state.copyWith( + isLoading: false, + errorMessage: e.toString(), )); } } diff --git a/lib/pages/routiens/bloc/routine_bloc/routine_event.dart b/lib/pages/routiens/bloc/routine_bloc/routine_event.dart index 9e7c6f16..6168748d 100644 --- a/lib/pages/routiens/bloc/routine_bloc/routine_event.dart +++ b/lib/pages/routiens/bloc/routine_bloc/routine_event.dart @@ -59,4 +59,24 @@ class RemoveFunction extends RoutineEvent { List get props => [function]; } +class SearchRoutines extends RoutineEvent { + final String query; + const SearchRoutines(this.query); + @override + List get props => [query]; +} + +class AddSelectedIcon extends RoutineEvent { + final String icon; + const AddSelectedIcon(this.icon); + @override + List get props => [icon]; +} + +class CreateSceneEvent extends RoutineEvent { + const CreateSceneEvent(); + @override + List get props => []; +} + class ClearFunctions extends RoutineEvent {} diff --git a/lib/pages/routiens/bloc/routine_bloc/routine_state.dart b/lib/pages/routiens/bloc/routine_bloc/routine_state.dart index ce6fea15..911992dc 100644 --- a/lib/pages/routiens/bloc/routine_bloc/routine_state.dart +++ b/lib/pages/routiens/bloc/routine_bloc/routine_state.dart @@ -9,6 +9,8 @@ class RoutineState extends Equatable { final Map> selectedFunctions; final bool isLoading; final String? errorMessage; + final String? routineName; + final String? selectedIcon; const RoutineState({ this.ifItems = const [], @@ -19,6 +21,8 @@ class RoutineState extends Equatable { this.selectedFunctions = const {}, this.isLoading = false, this.errorMessage, + this.routineName, + this.selectedIcon, }); RoutineState copyWith({ @@ -29,6 +33,8 @@ class RoutineState extends Equatable { Map>? selectedFunctions, bool? isLoading, String? errorMessage, + String? routineName, + String? selectedIcon, }) { return RoutineState( ifItems: ifItems ?? this.ifItems, @@ -38,6 +44,8 @@ class RoutineState extends Equatable { selectedFunctions: selectedFunctions ?? this.selectedFunctions, isLoading: isLoading ?? this.isLoading, errorMessage: errorMessage ?? this.errorMessage, + routineName: routineName ?? this.routineName, + selectedIcon: selectedIcon ?? this.selectedIcon, ); } @@ -50,5 +58,7 @@ class RoutineState extends Equatable { selectedFunctions, isLoading, errorMessage, + routineName, + selectedIcon, ]; } diff --git a/lib/pages/routiens/helper/save_routine_helper.dart b/lib/pages/routiens/helper/save_routine_helper.dart new file mode 100644 index 00000000..69329992 --- /dev/null +++ b/lib/pages/routiens/helper/save_routine_helper.dart @@ -0,0 +1,132 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class SaveRoutineHelper { + static Future showSaveRoutineDialog(BuildContext context) async { + return showDialog( + context: context, + builder: (BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: Container( + width: 600, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + DialogHeader('Create a scene: ${state.routineName ?? ""}'), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Left side - IF + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'IF:', + style: TextStyle( + fontSize: 16, + ), + ), + const SizedBox(height: 8), + if (context.read().isTabToRun) + ListTile( + leading: SvgPicture.asset( + Assets.tabToRun, + width: 24, + height: 24, + ), + title: const Text('Tab to run'), + ), + ], + ), + ), + const SizedBox(width: 16), + // Right side - THEN items + + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'THEN:', + style: TextStyle( + fontSize: 16, + ), + ), + const SizedBox(height: 8), + ...state.thenItems.map((item) { + final functions = state.selectedFunctions[ + item['uniqueCustomId']] ?? + []; + return ListTile( + leading: SvgPicture.asset( + item['imagePath'], + width: 22, + height: 22, + ), + title: Text(item['title'], + style: const TextStyle(fontSize: 14)), + subtitle: Wrap( + children: functions + .map((f) => Text( + '${f.operationName}: ${f.value}, ', + style: const TextStyle( + color: + ColorsManager.grayColor, + fontSize: 8), + overflow: TextOverflow.ellipsis, + maxLines: 3, + )) + .toList(), + ), + ); + }), + ], + ), + ), + ], + ), + ), + if (state.errorMessage != null) + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + state.errorMessage!, + style: const TextStyle(color: Colors.red), + ), + ), + DialogFooter( + onCancel: () => Navigator.pop(context), + onConfirm: () { + context + .read() + .add(const CreateSceneEvent()); + Navigator.pop(context); + }, + isConfirmEnabled: true, + ), + ], + ), + ), + ); + }, + ); + }, + ); + } +} diff --git a/lib/pages/routiens/helper/setting_helper.dart b/lib/pages/routiens/helper/setting_helper.dart index ae8a1bc8..2c0cee77 100644 --- a/lib/pages/routiens/helper/setting_helper.dart +++ b/lib/pages/routiens/helper/setting_helper.dart @@ -10,13 +10,16 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:flutter/cupertino.dart'; class SettingHelper { - static Future?> showSettingDialog( - {required BuildContext context, String? iconId, required bool isAutomation}) async { - return showDialog?>( + static Future showSettingDialog( + {required BuildContext context, + String? iconId, + required bool isAutomation}) async { + return showDialog( context: context, builder: (BuildContext context) { return BlocProvider( - create: (_) => SettingBloc()..add(InitialEvent(selectedIcon: iconId ?? '')), + create: (_) => + SettingBloc()..add(InitialEvent(selectedIcon: iconId ?? '')), child: AlertDialog( contentPadding: EdgeInsets.zero, content: BlocBuilder( @@ -29,7 +32,9 @@ class SettingHelper { } return Container( width: context.read().isExpanded ? 800 : 400, - height: context.read().isExpanded && isAutomation ? 500 : 300, + height: context.read().isExpanded && isAutomation + ? 500 + : 300, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), @@ -52,13 +57,18 @@ class SettingHelper { children: [ Container( padding: const EdgeInsets.only( - top: 10, left: 10, right: 10, bottom: 10), + top: 10, + left: 10, + right: 10, + bottom: 10), child: Column( children: [ InkWell( onTap: () {}, child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, children: [ Text( 'Validity', @@ -66,13 +76,18 @@ class SettingHelper { .textTheme .bodyMedium! .copyWith( - color: ColorsManager.textPrimaryColor, - fontWeight: FontWeight.w400, + color: ColorsManager + .textPrimaryColor, + fontWeight: + FontWeight + .w400, fontSize: 14), ), const Icon( - Icons.arrow_forward_ios_outlined, - color: ColorsManager.textGray, + Icons + .arrow_forward_ios_outlined, + color: ColorsManager + .textGray, size: 15, ) ], @@ -89,14 +104,18 @@ class SettingHelper { ), InkWell( onTap: () { - BlocProvider.of(context).add( - FetchIcons( + BlocProvider.of( + context) + .add(FetchIcons( expanded: !context - .read() + .read< + SettingBloc>() .isExpanded)); }, child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, children: [ Text( 'Effective Period', @@ -104,13 +123,18 @@ class SettingHelper { .textTheme .bodyMedium! .copyWith( - color: ColorsManager.textPrimaryColor, - fontWeight: FontWeight.w400, + color: ColorsManager + .textPrimaryColor, + fontWeight: + FontWeight + .w400, fontSize: 14), ), const Icon( - Icons.arrow_forward_ios_outlined, - color: ColorsManager.textGray, + Icons + .arrow_forward_ios_outlined, + color: ColorsManager + .textGray, size: 15, ) ], @@ -126,7 +150,9 @@ class SettingHelper { height: 5, ), Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, children: [ Text( 'Executed by', @@ -134,8 +160,10 @@ class SettingHelper { .textTheme .bodyMedium! .copyWith( - color: ColorsManager.textPrimaryColor, - fontWeight: FontWeight.w400, + color: ColorsManager + .textPrimaryColor, + fontWeight: + FontWeight.w400, fontSize: 14), ), Text('Cloud', @@ -143,8 +171,12 @@ class SettingHelper { .textTheme .bodyMedium! .copyWith( - color: ColorsManager.textGray, - fontWeight: FontWeight.w400, + color: + ColorsManager + .textGray, + fontWeight: + FontWeight + .w400, fontSize: 14)), ], ), @@ -157,19 +189,26 @@ class SettingHelper { children: [ Container( padding: const EdgeInsets.only( - top: 10, left: 10, right: 10, bottom: 10), + top: 10, + left: 10, + right: 10, + bottom: 10), child: Column( children: [ InkWell( onTap: () { - BlocProvider.of(context).add( - FetchIcons( + BlocProvider.of( + context) + .add(FetchIcons( expanded: !context - .read() + .read< + SettingBloc>() .isExpanded)); }, child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, children: [ Text( 'Icons', @@ -177,13 +216,18 @@ class SettingHelper { .textTheme .bodyMedium! .copyWith( - color: ColorsManager.textPrimaryColor, - fontWeight: FontWeight.w400, + color: ColorsManager + .textPrimaryColor, + fontWeight: + FontWeight + .w400, fontSize: 14), ), const Icon( - Icons.arrow_forward_ios_outlined, - color: ColorsManager.textGray, + Icons + .arrow_forward_ios_outlined, + color: ColorsManager + .textGray, size: 15, ) ], @@ -199,7 +243,9 @@ class SettingHelper { height: 5, ), Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, children: [ Text( 'Show on devices page', @@ -207,17 +253,21 @@ class SettingHelper { .textTheme .bodyMedium! .copyWith( - color: ColorsManager.textPrimaryColor, - fontWeight: FontWeight.w400, + color: ColorsManager + .textPrimaryColor, + fontWeight: + FontWeight.w400, fontSize: 14), ), Row( - mainAxisAlignment: MainAxisAlignment.end, + mainAxisAlignment: + MainAxisAlignment.end, children: [ Container( height: 30, width: 1, - color: ColorsManager.graysColor, + color: ColorsManager + .graysColor, ), Transform.scale( scale: .8, @@ -241,7 +291,9 @@ class SettingHelper { height: 5, ), Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, children: [ Text( 'Executed by', @@ -249,8 +301,10 @@ class SettingHelper { .textTheme .bodyMedium! .copyWith( - color: ColorsManager.textPrimaryColor, - fontWeight: FontWeight.w400, + color: ColorsManager + .textPrimaryColor, + fontWeight: + FontWeight.w400, fontSize: 14), ), Text('Cloud', @@ -258,8 +312,12 @@ class SettingHelper { .textTheme .bodyMedium! .copyWith( - color: ColorsManager.textGray, - fontWeight: FontWeight.w400, + color: + ColorsManager + .textGray, + fontWeight: + FontWeight + .w400, fontSize: 14)), ], ), @@ -268,12 +326,14 @@ class SettingHelper { ], ), ), - if (context.read().isExpanded && !isAutomation) + if (context.read().isExpanded && + !isAutomation) SizedBox( width: 400, height: 150, child: state is LoadingState - ? const Center(child: CircularProgressIndicator()) + ? const Center( + child: CircularProgressIndicator()) : GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( @@ -290,7 +350,8 @@ class SettingHelper { height: 35, child: InkWell( onTap: () { - BlocProvider.of(context) + BlocProvider.of( + context) .add(SelectIcon( iconId: iconModel.uuid, )); @@ -299,12 +360,16 @@ class SettingHelper { child: SizedBox( child: ClipOval( child: Container( - padding: const EdgeInsets.all(1), + padding: + const EdgeInsets.all(1), decoration: BoxDecoration( border: Border.all( - color: selectedIcon == iconModel.uuid - ? ColorsManager.primaryColorWithOpacity - : Colors.transparent, + color: selectedIcon == + iconModel.uuid + ? ColorsManager + .primaryColorWithOpacity + : Colors + .transparent, width: 2, ), shape: BoxShape.circle, @@ -319,8 +384,12 @@ class SettingHelper { ); }, )), - if (context.read().isExpanded && isAutomation) - const SizedBox(height: 350, width: 400, child: EffectivePeriodView()) + if (context.read().isExpanded && + isAutomation) + const SizedBox( + height: 350, + width: 400, + child: EffectivePeriodView()) ], ), Container( @@ -344,14 +413,20 @@ class SettingHelper { alignment: AlignmentDirectional.center, child: Text( 'Cancel', - style: Theme.of(context).textTheme.bodyMedium!.copyWith( + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( color: ColorsManager.textGray, ), ), ), ), ), - Container(width: 1, height: 50, color: ColorsManager.greyColor), + Container( + width: 1, + height: 50, + color: ColorsManager.greyColor), Expanded( child: InkWell( onTap: () { @@ -361,8 +436,12 @@ class SettingHelper { alignment: AlignmentDirectional.center, child: Text( 'Confirm', - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: ColorsManager.primaryColorWithOpacity, + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: ColorsManager + .primaryColorWithOpacity, ), ), ), diff --git a/lib/pages/routiens/models/create_scene/create_scene_model.dart b/lib/pages/routiens/models/create_scene/create_scene_model.dart new file mode 100644 index 00000000..c669aa9a --- /dev/null +++ b/lib/pages/routiens/models/create_scene/create_scene_model.dart @@ -0,0 +1,230 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; + +class CreateSceneModel { + String spaceUuid; + String iconId; + bool showInDevice; + String sceneName; + String decisionExpr; + List actions; + + CreateSceneModel({ + required this.spaceUuid, + required this.iconId, + required this.showInDevice, + required this.sceneName, + required this.decisionExpr, + required this.actions, + }); + + CreateSceneModel copyWith({ + String? spaceUuid, + String? iconId, + bool? showInDevice, + String? sceneName, + String? decisionExpr, + List? actions, + bool? showInHomePage, + }) { + return CreateSceneModel( + spaceUuid: spaceUuid ?? this.spaceUuid, + iconId: iconId ?? this.iconId, + showInDevice: showInDevice ?? this.showInDevice, + sceneName: sceneName ?? this.sceneName, + decisionExpr: decisionExpr ?? this.decisionExpr, + actions: actions ?? this.actions, + ); + } + + Map toMap([String? sceneId]) { + return { + if (sceneId == null) 'spaceUuid': spaceUuid, + if (iconId.isNotEmpty) 'iconUuid': iconId, + 'showInHomePage': showInDevice, + 'sceneName': sceneName, + 'decisionExpr': decisionExpr, + 'actions': actions.map((x) => x.toMap()).toList(), + }; + } + + factory CreateSceneModel.fromMap(Map map) { + return CreateSceneModel( + spaceUuid: map['spaceUuid'] ?? '', + showInDevice: map['showInHomePage'] ?? false, + iconId: map['iconUuid'] ?? '', + sceneName: map['sceneName'] ?? '', + decisionExpr: map['decisionExpr'] ?? '', + actions: List.from( + map['actions']?.map((x) => CreateSceneAction.fromMap(x))), + ); + } + + String toJson([String? sceneId]) => json.encode(toMap(sceneId)); + + factory CreateSceneModel.fromJson(String source) => + CreateSceneModel.fromMap(json.decode(source)); + + @override + String toString() { + return 'CreateSceneModel(unitUuid: $spaceUuid, sceneName: $sceneName, decisionExpr: $decisionExpr, actions: $actions)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is CreateSceneModel && + other.spaceUuid == spaceUuid && + other.iconId == iconId && + other.showInDevice == showInDevice && + other.sceneName == sceneName && + other.decisionExpr == decisionExpr && + listEquals(other.actions, actions); + } + + @override + int get hashCode { + return spaceUuid.hashCode ^ + sceneName.hashCode ^ + decisionExpr.hashCode ^ + actions.hashCode; + } +} + +class CreateSceneAction { + String entityId; + String actionExecutor; + CreateSceneExecutorProperty? executorProperty; + + CreateSceneAction({ + required this.entityId, + required this.actionExecutor, + required this.executorProperty, + }); + + CreateSceneAction copyWith({ + String? entityId, + String? actionExecutor, + CreateSceneExecutorProperty? executorProperty, + }) { + return CreateSceneAction( + entityId: entityId ?? this.entityId, + actionExecutor: actionExecutor ?? this.actionExecutor, + executorProperty: executorProperty ?? this.executorProperty, + ); + } + + Map toMap() { + if (executorProperty != null) { + return { + 'entityId': entityId, + 'actionExecutor': actionExecutor, + 'executorProperty': executorProperty?.toMap(actionExecutor), + }; + } else { + return { + 'entityId': entityId, + 'actionExecutor': actionExecutor, + }; + } + } + + factory CreateSceneAction.fromMap(Map map) { + return CreateSceneAction( + entityId: map['entityId'] ?? '', + actionExecutor: map['actionExecutor'] ?? '', + executorProperty: + CreateSceneExecutorProperty.fromMap(map['executorProperty']), + ); + } + + String toJson() => json.encode(toMap()); + + factory CreateSceneAction.fromJson(String source) => + CreateSceneAction.fromMap(json.decode(source)); + + @override + String toString() => + 'CreateSceneAction(entityId: $entityId, actionExecutor: $actionExecutor, executorProperty: $executorProperty)'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is CreateSceneAction && + other.entityId == entityId && + other.actionExecutor == actionExecutor && + other.executorProperty == executorProperty; + } + + @override + int get hashCode => + entityId.hashCode ^ actionExecutor.hashCode ^ executorProperty.hashCode; +} + +class CreateSceneExecutorProperty { + String functionCode; + dynamic functionValue; + int delaySeconds; + + CreateSceneExecutorProperty({ + required this.functionCode, + required this.functionValue, + required this.delaySeconds, + }); + + CreateSceneExecutorProperty copyWith({ + String? functionCode, + dynamic functionValue, + int? delaySeconds, + }) { + return CreateSceneExecutorProperty( + functionCode: functionCode ?? this.functionCode, + functionValue: functionValue ?? this.functionValue, + delaySeconds: delaySeconds ?? this.delaySeconds, + ); + } + + Map toMap(String actionExecutor) { + final map = {}; + if (functionCode.isNotEmpty) map['functionCode'] = functionCode; + if (functionValue != null) map['functionValue'] = functionValue; + if (actionExecutor == 'delay' && delaySeconds > 0) { + map['delaySeconds'] = delaySeconds; + } + return map; + } + + factory CreateSceneExecutorProperty.fromMap(Map map) { + return CreateSceneExecutorProperty( + functionCode: map['functionCode'] ?? '', + functionValue: map['functionValue'] ?? '', + delaySeconds: map['delaySeconds']?.toInt() ?? 0, + ); + } + + String toJson(String actionExecutor) => json.encode(toMap(actionExecutor)); + + factory CreateSceneExecutorProperty.fromJson(String source) => + CreateSceneExecutorProperty.fromMap(json.decode(source)); + + @override + String toString() => + 'CreateSceneExecutorProperty(functionCode: $functionCode, functionValue: $functionValue, delaySeconds: $delaySeconds)'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is CreateSceneExecutorProperty && + other.functionCode == functionCode && + other.functionValue == functionValue && + other.delaySeconds == delaySeconds; + } + + @override + int get hashCode => + functionCode.hashCode ^ functionValue.hashCode ^ delaySeconds.hashCode; +} diff --git a/lib/pages/routiens/widgets/routine_search_and_buttons.dart b/lib/pages/routiens/widgets/routine_search_and_buttons.dart index 598905e1..66f2a3a0 100644 --- a/lib/pages/routiens/widgets/routine_search_and_buttons.dart +++ b/lib/pages/routiens/widgets/routine_search_and_buttons.dart @@ -1,6 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/main.dart'; import 'package:syncrow_web/pages/common/buttons/default_button.dart'; import 'package:syncrow_web/pages/common/text_field/custom_text_field.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/helper/save_routine_helper.dart'; import 'package:syncrow_web/pages/routiens/helper/setting_helper.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/style.dart'; @@ -28,7 +32,9 @@ class RoutineSearchAndButtons extends StatelessWidget { children: [ ConstrainedBox( constraints: BoxConstraints( - maxWidth: constraints.maxWidth > 700 ? 450 : constraints.maxWidth - 32), + maxWidth: constraints.maxWidth > 700 + ? 450 + : constraints.maxWidth - 32), child: StatefulTextField( title: 'Routine Name', height: 40, @@ -38,6 +44,11 @@ class RoutineSearchAndButtons extends StatelessWidget { elevation: 0, borderRadius: 15, width: 450, + onChanged: (value) { + context + .read() + .add(SearchRoutines(value)); + }, ), ), (constraints.maxWidth <= 1000) @@ -48,8 +59,17 @@ class RoutineSearchAndButtons extends StatelessWidget { child: Center( child: DefaultButton( onPressed: () async { - final result = await SettingHelper.showSettingDialog( - context: context, isAutomation: true); + final result = + await SettingHelper.showSettingDialog( + context: context, + isAutomation: context + .read() + .isAutomation); + if (result != null) { + context + .read() + .add(AddSelectedIcon(result)); + } }, borderRadius: 15, elevation: 0, @@ -100,7 +120,9 @@ class RoutineSearchAndButtons extends StatelessWidget { width: 200, child: Center( child: DefaultButton( - onPressed: () {}, + onPressed: () { + SaveRoutineHelper.showSaveRoutineDialog(context); + }, borderRadius: 15, elevation: 0, backgroundColor: ColorsManager.primaryColor, diff --git a/lib/pages/routiens/widgets/search_bar_condition_title.dart b/lib/pages/routiens/widgets/search_bar_condition_title.dart index 40d6b575..e36ba873 100644 --- a/lib/pages/routiens/widgets/search_bar_condition_title.dart +++ b/lib/pages/routiens/widgets/search_bar_condition_title.dart @@ -4,7 +4,8 @@ import 'package:syncrow_web/pages/routiens/widgets/routines_title_widget.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; -class ConditionTitleAndSearchBar extends StatelessWidget with HelperResponsiveLayout { +class ConditionTitleAndSearchBar extends StatelessWidget + with HelperResponsiveLayout { const ConditionTitleAndSearchBar({ super.key, }); @@ -33,6 +34,7 @@ class ConditionTitleAndSearchBar extends StatelessWidget with HelperResponsiveLa borderRadius: BorderRadius.circular(15), ), controller: TextEditingController(), + onChanged: (value) {}, ), ], ) @@ -55,6 +57,7 @@ class ConditionTitleAndSearchBar extends StatelessWidget with HelperResponsiveLa borderRadius: BorderRadius.circular(15), ), controller: TextEditingController(), + onChanged: (value) {}, ), ], ); diff --git a/lib/services/routines_api.dart b/lib/services/routines_api.dart index 7b001751..b137982d 100644 --- a/lib/services/routines_api.dart +++ b/lib/services/routines_api.dart @@ -1,3 +1,4 @@ +import 'package:syncrow_web/pages/routiens/models/create_scene/create_scene_model.dart'; import 'package:syncrow_web/pages/routiens/models/icon_model.dart'; import 'package:syncrow_web/pages/routiens/models/routine_model.dart'; import 'package:syncrow_web/services/api/http_service.dart'; @@ -7,22 +8,22 @@ class SceneApi { static final HTTPService _httpService = HTTPService(); // //create scene -// static Future> createScene( -// CreateSceneModel createSceneModel) async { -// try { -// final response = await _httpService.post( -// path: ApiEndpoints.createScene, -// body: createSceneModel.toMap(), -// showServerMessage: false, -// expectedResponseModel: (json) { -// return json; -// }, -// ); -// return response; -// } catch (e) { -// rethrow; -// } -// } + static Future> createScene( + CreateSceneModel createSceneModel) async { + try { + final response = await _httpService.post( + path: ApiEndpoints.createScene, + body: createSceneModel.toMap(), + showServerMessage: false, + expectedResponseModel: (json) { + return json; + }, + ); + return response; + } catch (e) { + rethrow; + } + } // // // create automation // static Future> createAutomation( diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index c7f7bccb..a5decc3b 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -11,12 +11,14 @@ abstract class ApiEndpoints { static const String visitorPassword = '/visitor-password'; static const String getDevices = '/visitor-password/devices'; - static const String sendOnlineOneTime = '/visitor-password/temporary-password/online/one-time'; + static const String sendOnlineOneTime = + '/visitor-password/temporary-password/online/one-time'; static const String sendOnlineMultipleTime = '/visitor-password/temporary-password/online/multiple-time'; //offline Password - static const String sendOffLineOneTime = '/visitor-password/temporary-password/offline/one-time'; + static const String sendOffLineOneTime = + '/visitor-password/temporary-password/offline/one-time'; static const String sendOffLineMultipleTime = '/visitor-password/temporary-password/offline/multiple-time'; @@ -38,13 +40,18 @@ abstract class ApiEndpoints { '/device/report-logs/{uuid}?code={code}&startTime={startTime}&endTime={endTime}'; static const String scheduleByDeviceId = '/schedule/{deviceUuid}'; - static const String getScheduleByDeviceId = '/schedule/{deviceUuid}?category={category}'; - static const String deleteScheduleByDeviceId = '/schedule/{deviceUuid}/{scheduleUuid}'; - static const String updateScheduleByDeviceId = '/schedule/enable/{deviceUuid}'; + static const String getScheduleByDeviceId = + '/schedule/{deviceUuid}?category={category}'; + static const String deleteScheduleByDeviceId = + '/schedule/{deviceUuid}/{scheduleUuid}'; + static const String updateScheduleByDeviceId = + '/schedule/enable/{deviceUuid}'; static const String factoryReset = '/device/factory/reset/{deviceUuid}'; - static const String powerClamp = '/device/{powerClampUuid}/power-clamp/status'; + static const String powerClamp = + '/device/{powerClampUuid}/power-clamp/status'; static const String getSpaceScenes = '/scene/tap-to-run/{unitUuid}'; static const String getSpaceAutomation = '/automation/{unitUuid}'; static const String getIconScene = '/scene/icon'; + static const String createScene = '/scene/tap-to-run'; } From 0c555cda83574eceac44b779a09095bd3d7c8ee9 Mon Sep 17 00:00:00 2001 From: ashrafzarkanisala Date: Sun, 24 Nov 2024 23:07:03 +0300 Subject: [PATCH 19/23] push tab to run and handling automation --- .../common/text_field/custom_text_field.dart | 16 ++ .../view/device_managment_page.dart | 47 +++--- .../bloc/routine_bloc/routine_bloc.dart | 73 +++++--- .../bloc/routine_bloc/routine_event.dart | 8 + .../bloc/routine_bloc/routine_state.dart | 12 ++ lib/pages/routiens/helper/ac_helper.dart | 3 +- lib/pages/routiens/helper/delay_helper.dart | 2 - lib/pages/routiens/helper/setting_helper.dart | 10 +- lib/pages/routiens/widgets/dragable_card.dart | 156 +++++++++++------- lib/pages/routiens/widgets/if_container.dart | 79 +++++++-- .../widgets/routine_search_and_buttons.dart | 7 +- .../routiens/widgets/then_container.dart | 32 ++-- lib/services/routines_api.dart | 4 + 13 files changed, 295 insertions(+), 154 deletions(-) diff --git a/lib/pages/common/text_field/custom_text_field.dart b/lib/pages/common/text_field/custom_text_field.dart index 250ff110..3ba71cd1 100644 --- a/lib/pages/common/text_field/custom_text_field.dart +++ b/lib/pages/common/text_field/custom_text_field.dart @@ -19,6 +19,7 @@ class StatefulTextField extends StatefulWidget { this.icon, this.hintColor, required this.onChanged, + this.isRequired, }); final String title; @@ -34,6 +35,7 @@ class StatefulTextField extends StatefulWidget { final IconData? icon; final Color? hintColor; final Function(String)? onChanged; + final bool? isRequired; @override State createState() => _StatefulTextFieldState(); @@ -62,6 +64,7 @@ class _StatefulTextFieldState extends State { icon: widget.icon, hintColor: widget.hintColor, onChanged: widget.onChanged, + isRequired: widget.isRequired, ); } } @@ -82,6 +85,7 @@ class CustomTextField extends StatelessWidget { this.icon, this.hintColor, required this.onChanged, + this.isRequired, }); final String title; @@ -97,6 +101,7 @@ class CustomTextField extends StatelessWidget { final IconData? icon; final Color? hintColor; final Function(String)? onChanged; + final bool? isRequired; @override Widget build(BuildContext context) { @@ -123,6 +128,7 @@ class CustomTextField extends StatelessWidget { child: TextFormField( controller: controller, style: const TextStyle(color: Colors.black), + decoration: InputDecoration( hintText: hintText, hintStyle: TextStyle( @@ -140,6 +146,16 @@ class CustomTextField extends StatelessWidget { onChanged: (value) { onChanged!(value); }, + + /// required validator + validator: (value) { + if (isRequired == true) { + if (value == null || value.isEmpty) { + return 'This field is required'; + } + } + return null; + }, ), ), ), diff --git a/lib/pages/device_managment/all_devices/view/device_managment_page.dart b/lib/pages/device_managment/all_devices/view/device_managment_page.dart index ffc57131..4044c056 100644 --- a/lib/pages/device_managment/all_devices/view/device_managment_page.dart +++ b/lib/pages/device_managment/all_devices/view/device_managment_page.dart @@ -88,31 +88,32 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout { ); }), rightBody: const NavigateHomeGridView(), - scaffoldBody: BlocBuilder( - builder: (context, state) { - if (state is SelectedTabState && state.selectedTab) { - return const RoutinesView(); - } - if (state is ShowCreateRoutineState && state.showCreateRoutine) { - return const CreateNewRoutineView(); - } + scaffoldBody: CreateNewRoutineView(), + // BlocBuilder( + // builder: (context, state) { + // if (state is SelectedTabState && state.selectedTab) { + // return const RoutinesView(); + // } + // if (state is ShowCreateRoutineState && state.showCreateRoutine) { + // return const CreateNewRoutineView(); + // } - return BlocBuilder( - builder: (context, deviceState) { - if (deviceState is DeviceManagementLoading) { - return const Center(child: CircularProgressIndicator()); - } else if (deviceState is DeviceManagementLoaded || - deviceState is DeviceManagementFiltered) { - final devices = (deviceState as dynamic).devices ?? - (deviceState as DeviceManagementFiltered).filteredDevices; + // return BlocBuilder( + // builder: (context, deviceState) { + // if (deviceState is DeviceManagementLoading) { + // return const Center(child: CircularProgressIndicator()); + // } else if (deviceState is DeviceManagementLoaded || + // deviceState is DeviceManagementFiltered) { + // final devices = (deviceState as dynamic).devices ?? + // (deviceState as DeviceManagementFiltered).filteredDevices; - return DeviceManagementBody(devices: devices); - } else { - return const Center(child: Text('Error fetching Devices')); - } - }, - ); - }), + // return DeviceManagementBody(devices: devices); + // } else { + // return const Center(child: Text('Error fetching Devices')); + // } + // }, + // ); + // }), ), ); } diff --git a/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart b/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart index 1490b12d..35a522b6 100644 --- a/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart +++ b/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart @@ -26,6 +26,7 @@ class RoutineBloc extends Bloc { on(_onSearchRoutines); on(_onAddSelectedIcon); on(_onCreateScene); + on(_onRemoveDragCard); // on(_onRemoveFunction); // on(_onClearFunctions); } @@ -33,7 +34,13 @@ class RoutineBloc extends Bloc { void _onAddToIfContainer(AddToIfContainer event, Emitter emit) { final updatedIfItems = List>.from(state.ifItems) ..add(event.item); - isTabToRun = event.isTabToRun; + if (event.isTabToRun) { + isTabToRun = true; + isAutomation = false; + } else { + isTabToRun = false; + isAutomation = true; + } emit(state.copyWith(ifItems: updatedIfItems)); } @@ -46,30 +53,27 @@ class RoutineBloc extends Bloc { void _onAddFunctionsToRoutine( AddFunctionToRoutine event, Emitter emit) { - final currentSelectedFunctions = - Map>.from(state.selectedFunctions); + try { + if (event.functions.isEmpty) return; - if (currentSelectedFunctions.containsKey(event.uniqueCustomId)) { - currentSelectedFunctions[event.uniqueCustomId]!.addAll(event.functions); - } else { - currentSelectedFunctions[event.uniqueCustomId] = event.functions; + final currentSelectedFunctions = + Map>.from(state.selectedFunctions); + + if (currentSelectedFunctions.containsKey(event.uniqueCustomId)) { + currentSelectedFunctions[event.uniqueCustomId] = + List.from(currentSelectedFunctions[event.uniqueCustomId]!) + ..addAll(event.functions); + } else { + currentSelectedFunctions[event.uniqueCustomId] = + List.from(event.functions); + } + + emit(state.copyWith(selectedFunctions: currentSelectedFunctions)); + } catch (e) { + debugPrint('Error adding functions: $e'); } - - emit(state.copyWith(selectedFunctions: currentSelectedFunctions)); } - // void _onRemoveFunction(RemoveFunction event, Emitter emit) { - // final functions = List.from(state.selectedFunctions) - // ..removeWhere((f) => - // f.functionCode == event.function.functionCode && - // f.value == event.function.value); - // emit(state.copyWith(selectedFunctions: functions)); - // } - - // void _onClearFunctions(ClearFunctions event, Emitter emit) { - // emit(state.copyWith(selectedFunctions: [])); - // } - Future _onLoadScenes( LoadScenes event, Emitter emit) async { emit(state.copyWith(isLoading: true, errorMessage: null)); @@ -83,7 +87,9 @@ class RoutineBloc extends Bloc { } catch (e) { emit(state.copyWith( isLoading: false, - errorMessage: 'Failed to load scenes', + loadScenesErrorMessage: 'Failed to load scenes', + errorMessage: '', + loadAutomationErrorMessage: '', )); } } @@ -101,7 +107,9 @@ class RoutineBloc extends Bloc { } catch (e) { emit(state.copyWith( isLoading: false, - errorMessage: 'Failed to load automations', + loadAutomationErrorMessage: 'Failed to load automations', + errorMessage: '', + loadScenesErrorMessage: '', )); } } @@ -178,10 +186,7 @@ class RoutineBloc extends Bloc { final result = await SceneApi.createScene(createSceneModel); if (result['success']) { - emit(state.copyWith( - isLoading: false, - errorMessage: null, - )); + emit(const RoutineState()); } else { emit(state.copyWith( isLoading: false, @@ -195,4 +200,18 @@ class RoutineBloc extends Bloc { )); } } + + FutureOr _onRemoveDragCard( + RemoveDragCard event, Emitter emit) { + if (event.isFromThen) { + /// remove element from thenItems at specific index + final thenItems = List>.from(state.thenItems); + thenItems.removeAt(event.index); + emit(state.copyWith(thenItems: thenItems)); + } else { + final ifItems = List>.from(state.ifItems); + ifItems.removeAt(event.index); + emit(state.copyWith(ifItems: ifItems)); + } + } } diff --git a/lib/pages/routiens/bloc/routine_bloc/routine_event.dart b/lib/pages/routiens/bloc/routine_bloc/routine_event.dart index 6168748d..9e154cf4 100644 --- a/lib/pages/routiens/bloc/routine_bloc/routine_event.dart +++ b/lib/pages/routiens/bloc/routine_bloc/routine_event.dart @@ -79,4 +79,12 @@ class CreateSceneEvent extends RoutineEvent { List get props => []; } +class RemoveDragCard extends RoutineEvent { + final int index; + final bool isFromThen; + const RemoveDragCard({required this.index, required this.isFromThen}); + @override + List get props => [index]; +} + class ClearFunctions extends RoutineEvent {} diff --git a/lib/pages/routiens/bloc/routine_bloc/routine_state.dart b/lib/pages/routiens/bloc/routine_bloc/routine_state.dart index 911992dc..784cd148 100644 --- a/lib/pages/routiens/bloc/routine_bloc/routine_state.dart +++ b/lib/pages/routiens/bloc/routine_bloc/routine_state.dart @@ -9,6 +9,8 @@ class RoutineState extends Equatable { final Map> selectedFunctions; final bool isLoading; final String? errorMessage; + final String? loadScenesErrorMessage; + final String? loadAutomationErrorMessage; final String? routineName; final String? selectedIcon; @@ -23,6 +25,8 @@ class RoutineState extends Equatable { this.errorMessage, this.routineName, this.selectedIcon, + this.loadScenesErrorMessage, + this.loadAutomationErrorMessage, }); RoutineState copyWith({ @@ -35,6 +39,8 @@ class RoutineState extends Equatable { String? errorMessage, String? routineName, String? selectedIcon, + String? loadAutomationErrorMessage, + String? loadScenesErrorMessage, }) { return RoutineState( ifItems: ifItems ?? this.ifItems, @@ -46,6 +52,10 @@ class RoutineState extends Equatable { errorMessage: errorMessage ?? this.errorMessage, routineName: routineName ?? this.routineName, selectedIcon: selectedIcon ?? this.selectedIcon, + loadScenesErrorMessage: + loadScenesErrorMessage ?? this.loadScenesErrorMessage, + loadAutomationErrorMessage: + loadAutomationErrorMessage ?? this.loadAutomationErrorMessage, ); } @@ -60,5 +70,7 @@ class RoutineState extends Equatable { errorMessage, routineName, selectedIcon, + loadScenesErrorMessage, + loadAutomationErrorMessage, ]; } diff --git a/lib/pages/routiens/helper/ac_helper.dart b/lib/pages/routiens/helper/ac_helper.dart index 8b555b7a..10606b69 100644 --- a/lib/pages/routiens/helper/ac_helper.dart +++ b/lib/pages/routiens/helper/ac_helper.dart @@ -96,14 +96,13 @@ class ACHelper { onConfirm: state.addedFunctions.isNotEmpty ? () { /// add the functions to the routine bloc - // for (var function in state.addedFunctions) { context.read().add( AddFunctionToRoutine( state.addedFunctions, uniqueCustomId, ), ); - //} + // Return the device data to be added to the container Navigator.pop(context, { 'deviceId': functions.first.deviceId, diff --git a/lib/pages/routiens/helper/delay_helper.dart b/lib/pages/routiens/helper/delay_helper.dart index 8ecff897..1191ac58 100644 --- a/lib/pages/routiens/helper/delay_helper.dart +++ b/lib/pages/routiens/helper/delay_helper.dart @@ -1,12 +1,10 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/routiens/bloc/functions_bloc/functions_bloc_bloc.dart'; import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart'; import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; class DelayHelper { static Future?> showDelayPickerDialog( diff --git a/lib/pages/routiens/helper/setting_helper.dart b/lib/pages/routiens/helper/setting_helper.dart index 2c0cee77..621a5219 100644 --- a/lib/pages/routiens/helper/setting_helper.dart +++ b/lib/pages/routiens/helper/setting_helper.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/bloc/setting_bloc/setting_bloc.dart'; import 'package:syncrow_web/pages/routiens/bloc/setting_bloc/setting_event.dart'; import 'package:syncrow_web/pages/routiens/bloc/setting_bloc/setting_state.dart'; @@ -10,13 +11,14 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:flutter/cupertino.dart'; class SettingHelper { - static Future showSettingDialog( - {required BuildContext context, - String? iconId, - required bool isAutomation}) async { + static Future showSettingDialog({ + required BuildContext context, + String? iconId, + }) async { return showDialog( context: context, builder: (BuildContext context) { + final isAutomation = context.read().isAutomation; return BlocProvider( create: (_) => SettingBloc()..add(InitialEvent(selectedIcon: iconId ?? '')), diff --git a/lib/pages/routiens/widgets/dragable_card.dart b/lib/pages/routiens/widgets/dragable_card.dart index 02c802bc..ae66ce02 100644 --- a/lib/pages/routiens/widgets/dragable_card.dart +++ b/lib/pages/routiens/widgets/dragable_card.dart @@ -11,6 +11,9 @@ class DraggableCard extends StatelessWidget { final String title; final Map deviceData; final EdgeInsetsGeometry? padding; + final void Function()? onRemove; + final bool? isFromThen; + final bool? isFromIf; const DraggableCard({ super.key, @@ -18,6 +21,9 @@ class DraggableCard extends StatelessWidget { required this.title, required this.deviceData, this.padding, + this.onRemove, + this.isFromThen, + this.isFromIf, }); @override @@ -44,75 +50,97 @@ class DraggableCard extends StatelessWidget { Widget _buildCardContent( BuildContext context, List deviceFunctions, {EdgeInsetsGeometry? padding}) { - return Card( - color: ColorsManager.whiteColors, - child: Container( - padding: padding ?? const EdgeInsets.all(16), - width: 110, - height: deviceFunctions.isEmpty ? 123 : null, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Column( + return Stack( + children: [ + Card( + color: ColorsManager.whiteColors, + child: Container( + padding: padding ?? const EdgeInsets.all(16), + width: 110, + height: deviceFunctions.isEmpty ? 123 : null, + child: Column( + mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Container( - height: 50, - width: 50, - decoration: BoxDecoration( - color: ColorsManager.CircleImageBackground, - borderRadius: BorderRadius.circular(90), - border: Border.all( - color: ColorsManager.graysColor, - ), - ), - padding: const EdgeInsets.all(8), - child: imagePath.contains('.svg') - ? SvgPicture.asset( - imagePath, - ) - : Image.network(imagePath), - ), - const SizedBox(height: 8), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 3), - child: Text( - title, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - maxLines: 2, - style: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.blackColor, - fontSize: 12, - ), - ), - ), - ], - ), - if (deviceFunctions.isNotEmpty) - // const Divider(height: 1), - ...deviceFunctions.map((function) => Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: Text( - '${function.operationName}: ${function.value}', - style: context.textTheme.bodySmall?.copyWith( - fontSize: 9, - color: ColorsManager.textGray, - height: 1.2, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + height: 50, + width: 50, + decoration: BoxDecoration( + color: ColorsManager.CircleImageBackground, + borderRadius: BorderRadius.circular(90), + border: Border.all( + color: ColorsManager.graysColor, ), ), - ], - )), - ], + padding: const EdgeInsets.all(8), + child: imagePath.contains('.svg') + ? SvgPicture.asset( + imagePath, + ) + : Image.network(imagePath), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 3), + child: Text( + title, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + maxLines: 2, + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.blackColor, + fontSize: 12, + ), + ), + ), + ], + ), + if (deviceFunctions.isNotEmpty) + // const Divider(height: 1), + ...deviceFunctions.map((function) => Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: Text( + '${function.operationName}: ${function.value}', + style: context.textTheme.bodySmall?.copyWith( + fontSize: 9, + color: ColorsManager.textGray, + height: 1.2, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + )), + ], + ), + ), ), - ), + Positioned( + top: -4, + right: -6, + child: Visibility( + visible: (isFromIf ?? false) || (isFromThen ?? false), + child: IconButton( + onPressed: onRemove == null + ? null + : () { + onRemove!(); + }, + icon: const Icon( + Icons.close, + color: ColorsManager.boxColor, + ), + ), + ), + ), + ], ); } diff --git a/lib/pages/routiens/widgets/if_container.dart b/lib/pages/routiens/widgets/if_container.dart index a7afa245..cfb1b27c 100644 --- a/lib/pages/routiens/widgets/if_container.dart +++ b/lib/pages/routiens/widgets/if_container.dart @@ -4,6 +4,7 @@ import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/helper/dialog_helper/device_dialog_helper.dart'; import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:uuid/uuid.dart'; class IfContainer extends StatelessWidget { const IfContainer({super.key}); @@ -20,7 +21,9 @@ class IfContainer extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('IF', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const Text('IF', + style: + TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 16), if (context.read().isTabToRun) const Row( @@ -37,14 +40,23 @@ class IfContainer extends StatelessWidget { Wrap( spacing: 8, runSpacing: 8, - children: state.ifItems - .map((item) => DraggableCard( - // key: Key(item['key']!), - imagePath: item['imagePath'] ?? '', - title: item['title'] ?? 'Tab to run', - deviceData: item, - )) - .toList(), + children: List.generate( + state.ifItems.length, + (index) => DraggableCard( + imagePath: + state.ifItems[index]['imagePath'] ?? '', + title: state.ifItems[index]['title'] ?? '', + deviceData: state.ifItems[index], + padding: const EdgeInsets.symmetric( + horizontal: 4, vertical: 8), + isFromThen: false, + isFromIf: true, + onRemove: () { + context.read().add( + RemoveDragCard( + index: index, isFromThen: false)); + }, + )), ), ], ), @@ -52,15 +64,52 @@ class IfContainer extends StatelessWidget { }, onWillAccept: (data) => data != null, onAccept: (data) async { + // final uniqueCustomId = const Uuid().v4(); + + // final mutableData = Map.from(data); + // mutableData['uniqueCustomId'] = uniqueCustomId; + + // if (!context.read().isTabToRun) { + // if (data['deviceId'] == 'tab_to_run') { + // context.read().add(AddToIfContainer(data, true)); + // } else { + // final result = + // await DeviceDialogHelper.showDeviceDialog(context, mutableData); + // if (result != null) { + // context + // .read() + // .add(AddToIfContainer(mutableData, false)); + // } else if (!['AC', '1G', '2G', '3G'] + // .contains(data['productType'])) { + // context + // .read() + // .add(AddToIfContainer(mutableData, false)); + // } + // } + //} + final uniqueCustomId = const Uuid().v4(); + + final mutableData = Map.from(data); + mutableData['uniqueCustomId'] = uniqueCustomId; + if (!context.read().isTabToRun) { - if (data['deviceId'] == 'tab_to_run') { - context.read().add(AddToIfContainer(data, true)); + if (mutableData['deviceId'] == 'tab_to_run') { + context + .read() + .add(AddToIfContainer(mutableData, true)); } else { - final result = await DeviceDialogHelper.showDeviceDialog(context, data); + final result = await DeviceDialogHelper.showDeviceDialog( + context, mutableData); + if (result != null) { - context.read().add(AddToIfContainer(result, false)); - } else if (!['AC', '1G', '2G', '3G'].contains(data['productType'])) { - context.read().add(AddToIfContainer(data, false)); + context + .read() + .add(AddToIfContainer(mutableData, false)); + } else if (!['AC', '1G', '2G', '3G'] + .contains(mutableData['productType'])) { + context + .read() + .add(AddToIfContainer(mutableData, false)); } } } diff --git a/lib/pages/routiens/widgets/routine_search_and_buttons.dart b/lib/pages/routiens/widgets/routine_search_and_buttons.dart index 66f2a3a0..fa0e2128 100644 --- a/lib/pages/routiens/widgets/routine_search_and_buttons.dart +++ b/lib/pages/routiens/widgets/routine_search_and_buttons.dart @@ -43,6 +43,7 @@ class RoutineSearchAndButtons extends StatelessWidget { boxDecoration: containerWhiteDecoration, elevation: 0, borderRadius: 15, + isRequired: true, width: 450, onChanged: (value) { context @@ -61,10 +62,8 @@ class RoutineSearchAndButtons extends StatelessWidget { onPressed: () async { final result = await SettingHelper.showSettingDialog( - context: context, - isAutomation: context - .read() - .isAutomation); + context: context, + ); if (result != null) { context .read() diff --git a/lib/pages/routiens/widgets/then_container.dart b/lib/pages/routiens/widgets/then_container.dart index 5d71e405..d6c9500f 100644 --- a/lib/pages/routiens/widgets/then_container.dart +++ b/lib/pages/routiens/widgets/then_container.dart @@ -30,19 +30,25 @@ class ThenContainer extends StatelessWidget { fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 16), Wrap( - spacing: 8, - runSpacing: 8, - children: state.thenItems - .map((item) => DraggableCard( - // key: Key(item['key']!), - imagePath: item['imagePath']!, - title: item['title']!, - deviceData: item, - padding: EdgeInsets.symmetric( - horizontal: 4, vertical: 8), - )) - .toList(), - ), + spacing: 8, + runSpacing: 8, + children: List.generate( + state.thenItems.length, + (index) => DraggableCard( + imagePath: + state.thenItems[index]['imagePath'] ?? '', + title: state.thenItems[index]['title'] ?? '', + deviceData: state.thenItems[index], + padding: const EdgeInsets.symmetric( + horizontal: 4, vertical: 8), + isFromThen: true, + isFromIf: false, + onRemove: () { + context.read().add( + RemoveDragCard( + index: index, isFromThen: true)); + }, + ))), ], ), ), diff --git a/lib/services/routines_api.dart b/lib/services/routines_api.dart index b137982d..abf5099e 100644 --- a/lib/services/routines_api.dart +++ b/lib/services/routines_api.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/routiens/models/create_scene/create_scene_model.dart'; import 'package:syncrow_web/pages/routiens/models/icon_model.dart'; import 'package:syncrow_web/pages/routiens/models/routine_model.dart'; @@ -11,6 +12,7 @@ class SceneApi { static Future> createScene( CreateSceneModel createSceneModel) async { try { + debugPrint('create scene model: ${createSceneModel.toMap()}'); final response = await _httpService.post( path: ApiEndpoints.createScene, body: createSceneModel.toMap(), @@ -19,8 +21,10 @@ class SceneApi { return json; }, ); + debugPrint('create scene response: $response'); return response; } catch (e) { + debugPrint(e.toString()); rethrow; } } From 30db9cfc2a5eac77af526b90c7cbbd745a829807 Mon Sep 17 00:00:00 2001 From: Abdullah Alassaf Date: Mon, 25 Nov 2024 01:47:17 +0300 Subject: [PATCH 20/23] Added routines search field functionlity --- .../bloc/routine_bloc/routine_bloc.dart | 43 ++++------ .../bloc/routine_bloc/routine_state.dart | 82 +++++++++---------- .../routiens/widgets/routine_devices.dart | 49 +++++++---- .../widgets/routine_search_and_buttons.dart | 16 +--- .../widgets/scenes_and_automations.dart | 38 ++++++--- .../widgets/search_bar_condition_title.dart | 19 ++++- 6 files changed, 138 insertions(+), 109 deletions(-) diff --git a/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart b/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart index 35a522b6..88414196 100644 --- a/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart +++ b/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart @@ -32,8 +32,7 @@ class RoutineBloc extends Bloc { } void _onAddToIfContainer(AddToIfContainer event, Emitter emit) { - final updatedIfItems = List>.from(state.ifItems) - ..add(event.item); + final updatedIfItems = List>.from(state.ifItems)..add(event.item); if (event.isTabToRun) { isTabToRun = true; isAutomation = false; @@ -44,15 +43,12 @@ class RoutineBloc extends Bloc { emit(state.copyWith(ifItems: updatedIfItems)); } - void _onAddToThenContainer( - AddToThenContainer event, Emitter emit) { - final updatedThenItems = List>.from(state.thenItems) - ..add(event.item); + void _onAddToThenContainer(AddToThenContainer event, Emitter emit) { + final updatedThenItems = List>.from(state.thenItems)..add(event.item); emit(state.copyWith(thenItems: updatedThenItems)); } - void _onAddFunctionsToRoutine( - AddFunctionToRoutine event, Emitter emit) { + void _onAddFunctionsToRoutine(AddFunctionToRoutine event, Emitter emit) { try { if (event.functions.isEmpty) return; @@ -61,11 +57,9 @@ class RoutineBloc extends Bloc { if (currentSelectedFunctions.containsKey(event.uniqueCustomId)) { currentSelectedFunctions[event.uniqueCustomId] = - List.from(currentSelectedFunctions[event.uniqueCustomId]!) - ..addAll(event.functions); + List.from(currentSelectedFunctions[event.uniqueCustomId]!)..addAll(event.functions); } else { - currentSelectedFunctions[event.uniqueCustomId] = - List.from(event.functions); + currentSelectedFunctions[event.uniqueCustomId] = List.from(event.functions); } emit(state.copyWith(selectedFunctions: currentSelectedFunctions)); @@ -74,8 +68,7 @@ class RoutineBloc extends Bloc { } } - Future _onLoadScenes( - LoadScenes event, Emitter emit) async { + Future _onLoadScenes(LoadScenes event, Emitter emit) async { emit(state.copyWith(isLoading: true, errorMessage: null)); try { @@ -94,8 +87,7 @@ class RoutineBloc extends Bloc { } } - Future _onLoadAutomation( - LoadAutomation event, Emitter emit) async { + Future _onLoadAutomation(LoadAutomation event, Emitter emit) async { emit(state.copyWith(isLoading: true, errorMessage: null)); try { @@ -114,13 +106,13 @@ class RoutineBloc extends Bloc { } } - FutureOr _onSearchRoutines( - SearchRoutines event, Emitter emit) { - emit(state.copyWith(routineName: event.query)); + FutureOr _onSearchRoutines(SearchRoutines event, Emitter emit) async { + emit(state.copyWith(isLoading: true, errorMessage: null)); + await Future.delayed(const Duration(seconds: 1)); + emit(state.copyWith(searchText: event.query)); } - FutureOr _onAddSelectedIcon( - AddSelectedIcon event, Emitter emit) { + FutureOr _onAddSelectedIcon(AddSelectedIcon event, Emitter emit) { emit(state.copyWith(selectedIcon: event.icon)); } @@ -129,8 +121,7 @@ class RoutineBloc extends Bloc { return actions.first['deviceId'] == 'delay'; } - Future _onCreateScene( - CreateSceneEvent event, Emitter emit) async { + Future _onCreateScene(CreateSceneEvent event, Emitter emit) async { try { // Check if first action is delay if (_isFirstActionDelay(state.thenItems)) { @@ -145,8 +136,7 @@ class RoutineBloc extends Bloc { final actions = state.thenItems .map((item) { - final functions = - state.selectedFunctions[item['uniqueCustomId']] ?? []; + final functions = state.selectedFunctions[item['uniqueCustomId']] ?? []; if (functions.isEmpty) return null; final function = functions.first; @@ -201,8 +191,7 @@ class RoutineBloc extends Bloc { } } - FutureOr _onRemoveDragCard( - RemoveDragCard event, Emitter emit) { + FutureOr _onRemoveDragCard(RemoveDragCard event, Emitter emit) { if (event.isFromThen) { /// remove element from thenItems at specific index final thenItems = List>.from(state.thenItems); diff --git a/lib/pages/routiens/bloc/routine_bloc/routine_state.dart b/lib/pages/routiens/bloc/routine_bloc/routine_state.dart index 784cd148..f057c2c0 100644 --- a/lib/pages/routiens/bloc/routine_bloc/routine_state.dart +++ b/lib/pages/routiens/bloc/routine_bloc/routine_state.dart @@ -13,50 +13,49 @@ class RoutineState extends Equatable { final String? loadAutomationErrorMessage; final String? routineName; final String? selectedIcon; + final String? searchText; - const RoutineState({ - this.ifItems = const [], - this.thenItems = const [], - this.availableCards = const [], - this.scenes = const [], - this.automations = const [], - this.selectedFunctions = const {}, - this.isLoading = false, - this.errorMessage, - this.routineName, - this.selectedIcon, - this.loadScenesErrorMessage, - this.loadAutomationErrorMessage, - }); + const RoutineState( + {this.ifItems = const [], + this.thenItems = const [], + this.availableCards = const [], + this.scenes = const [], + this.automations = const [], + this.selectedFunctions = const {}, + this.isLoading = false, + this.errorMessage, + this.routineName, + this.selectedIcon, + this.loadScenesErrorMessage, + this.loadAutomationErrorMessage, + this.searchText}); - RoutineState copyWith({ - List>? ifItems, - List>? thenItems, - List? scenes, - List? automations, - Map>? selectedFunctions, - bool? isLoading, - String? errorMessage, - String? routineName, - String? selectedIcon, - String? loadAutomationErrorMessage, - String? loadScenesErrorMessage, - }) { + RoutineState copyWith( + {List>? ifItems, + List>? thenItems, + List? scenes, + List? automations, + Map>? selectedFunctions, + bool? isLoading, + String? errorMessage, + String? routineName, + String? selectedIcon, + String? loadAutomationErrorMessage, + String? loadScenesErrorMessage, + String? searchText}) { return RoutineState( - ifItems: ifItems ?? this.ifItems, - thenItems: thenItems ?? this.thenItems, - scenes: scenes ?? this.scenes, - automations: automations ?? this.automations, - selectedFunctions: selectedFunctions ?? this.selectedFunctions, - isLoading: isLoading ?? this.isLoading, - errorMessage: errorMessage ?? this.errorMessage, - routineName: routineName ?? this.routineName, - selectedIcon: selectedIcon ?? this.selectedIcon, - loadScenesErrorMessage: - loadScenesErrorMessage ?? this.loadScenesErrorMessage, - loadAutomationErrorMessage: - loadAutomationErrorMessage ?? this.loadAutomationErrorMessage, - ); + ifItems: ifItems ?? this.ifItems, + thenItems: thenItems ?? this.thenItems, + scenes: scenes ?? this.scenes, + automations: automations ?? this.automations, + selectedFunctions: selectedFunctions ?? this.selectedFunctions, + isLoading: isLoading ?? this.isLoading, + errorMessage: errorMessage ?? this.errorMessage, + routineName: routineName ?? this.routineName, + selectedIcon: selectedIcon ?? this.selectedIcon, + loadScenesErrorMessage: loadScenesErrorMessage ?? this.loadScenesErrorMessage, + loadAutomationErrorMessage: loadAutomationErrorMessage ?? this.loadAutomationErrorMessage, + searchText: searchText ?? this.searchText); } @override @@ -72,5 +71,6 @@ class RoutineState extends Equatable { selectedIcon, loadScenesErrorMessage, loadAutomationErrorMessage, + searchText ]; } diff --git a/lib/pages/routiens/widgets/routine_devices.dart b/lib/pages/routiens/widgets/routine_devices.dart index 02c1b4fa..dbdf71f4 100644 --- a/lib/pages/routiens/widgets/routine_devices.dart +++ b/lib/pages/routiens/widgets/routine_devices.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; @@ -14,7 +15,7 @@ class RoutineDevices extends StatelessWidget { child: BlocBuilder( builder: (context, state) { if (state is DeviceManagementLoaded) { - final deviceList = state.devices + List deviceList = state.devices .where((device) => device.productType == 'AC' || device.productType == '1G' || @@ -30,19 +31,39 @@ class RoutineDevices extends StatelessWidget { runSpacing: 10, children: deviceList.asMap().entries.map((entry) { final device = entry.value; - return DraggableCard( - imagePath: device.getDefaultIcon(device.productType), - title: device.name ?? '', - deviceData: { - 'device': device, - 'imagePath': device.getDefaultIcon(device.productType), - 'title': device.name ?? '', - 'deviceId': device.uuid, - 'productType': device.productType, - 'functions': device.functions, - 'uniqueCustomId': '', - }, - ); + if (routineState.searchText != null && routineState.searchText!.isNotEmpty) { + return device.name! + .toLowerCase() + .contains(routineState.searchText!.toLowerCase()) + ? DraggableCard( + imagePath: device.getDefaultIcon(device.productType), + title: device.name ?? '', + deviceData: { + 'device': device, + 'imagePath': device.getDefaultIcon(device.productType), + 'title': device.name ?? '', + 'deviceId': device.uuid, + 'productType': device.productType, + 'functions': device.functions, + 'uniqueCustomId': '', + }, + ) + : Container(); + } else { + return DraggableCard( + imagePath: device.getDefaultIcon(device.productType), + title: device.name ?? '', + deviceData: { + 'device': device, + 'imagePath': device.getDefaultIcon(device.productType), + 'title': device.name ?? '', + 'deviceId': device.uuid, + 'productType': device.productType, + 'functions': device.functions, + 'uniqueCustomId': '', + }, + ); + } }).toList(), ); }, diff --git a/lib/pages/routiens/widgets/routine_search_and_buttons.dart b/lib/pages/routiens/widgets/routine_search_and_buttons.dart index fa0e2128..41a47c17 100644 --- a/lib/pages/routiens/widgets/routine_search_and_buttons.dart +++ b/lib/pages/routiens/widgets/routine_search_and_buttons.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/main.dart'; import 'package:syncrow_web/pages/common/buttons/default_button.dart'; import 'package:syncrow_web/pages/common/text_field/custom_text_field.dart'; import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; @@ -32,9 +31,7 @@ class RoutineSearchAndButtons extends StatelessWidget { children: [ ConstrainedBox( constraints: BoxConstraints( - maxWidth: constraints.maxWidth > 700 - ? 450 - : constraints.maxWidth - 32), + maxWidth: constraints.maxWidth > 700 ? 450 : constraints.maxWidth - 32), child: StatefulTextField( title: 'Routine Name', height: 40, @@ -46,9 +43,7 @@ class RoutineSearchAndButtons extends StatelessWidget { isRequired: true, width: 450, onChanged: (value) { - context - .read() - .add(SearchRoutines(value)); + // context.read().add(SearchRoutines(value)); }, ), ), @@ -60,14 +55,11 @@ class RoutineSearchAndButtons extends StatelessWidget { child: Center( child: DefaultButton( onPressed: () async { - final result = - await SettingHelper.showSettingDialog( + final result = await SettingHelper.showSettingDialog( context: context, ); if (result != null) { - context - .read() - .add(AddSelectedIcon(result)); + context.read().add(AddSelectedIcon(result)); } }, borderRadius: 15, diff --git a/lib/pages/routiens/widgets/scenes_and_automations.dart b/lib/pages/routiens/widgets/scenes_and_automations.dart index a74634ba..633f9a82 100644 --- a/lib/pages/routiens/widgets/scenes_and_automations.dart +++ b/lib/pages/routiens/widgets/scenes_and_automations.dart @@ -33,17 +33,33 @@ class _ScenesAndAutomationsState extends State { runSpacing: 10, children: scenes.asMap().entries.map((entry) { final scene = entry.value; - return DraggableCard( - imagePath: Assets.logo, - title: scene.name, - deviceData: { - 'deviceId': scene.id, - 'name': scene.name, - 'status': scene.status, - 'type': scene.type, - 'icon': scene.icon, - }, - ); + if (state.searchText != null && state.searchText!.isNotEmpty) { + return scene.name.toLowerCase().contains(state.searchText!.toLowerCase()) + ? DraggableCard( + imagePath: Assets.logo, + title: scene.name, + deviceData: { + 'deviceId': scene.id, + 'name': scene.name, + 'status': scene.status, + 'type': scene.type, + 'icon': scene.icon, + }, + ) + : Container(); + } else { + return DraggableCard( + imagePath: Assets.logo, + title: scene.name, + deviceData: { + 'deviceId': scene.id, + 'name': scene.name, + 'status': scene.status, + 'type': scene.type, + 'icon': scene.icon, + }, + ); + } }).toList(), ); } diff --git a/lib/pages/routiens/widgets/search_bar_condition_title.dart b/lib/pages/routiens/widgets/search_bar_condition_title.dart index e36ba873..c009f040 100644 --- a/lib/pages/routiens/widgets/search_bar_condition_title.dart +++ b/lib/pages/routiens/widgets/search_bar_condition_title.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/common/text_field/custom_text_field.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/widgets/routines_title_widget.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; -class ConditionTitleAndSearchBar extends StatelessWidget - with HelperResponsiveLayout { +class ConditionTitleAndSearchBar extends StatelessWidget with HelperResponsiveLayout { const ConditionTitleAndSearchBar({ super.key, }); @@ -34,7 +35,12 @@ class ConditionTitleAndSearchBar extends StatelessWidget borderRadius: BorderRadius.circular(15), ), controller: TextEditingController(), - onChanged: (value) {}, + // onSubmitted: (value) { + // context.read().add(SearchRoutines(value)); + // }, + onChanged: (value) { + context.read().add(SearchRoutines(value)); + }, ), ], ) @@ -57,7 +63,12 @@ class ConditionTitleAndSearchBar extends StatelessWidget borderRadius: BorderRadius.circular(15), ), controller: TextEditingController(), - onChanged: (value) {}, + // onSubmitted: (value) { + // context.read().add(SearchRoutines(value)); + // }, + onChanged: (value) { + context.read().add(SearchRoutines(value)); + }, ), ], ); From 19694bec986111f5359006c4ef59bf8fbf81b9e7 Mon Sep 17 00:00:00 2001 From: ashrafzarkanisala Date: Mon, 25 Nov 2024 10:08:50 +0300 Subject: [PATCH 21/23] push design and missing view and folder structure --- assets/icons/routine/automation.svg | 23 ++ .../common/text_field/custom_text_field.dart | 26 ++- .../dialog_helper/device_dialog_helper.dart | 8 +- .../helper/duration_format_helper.dart | 15 ++ lib/pages/routiens/models/ac/ac_function.dart | 4 +- lib/pages/routiens/models/routine_model.dart | 10 +- .../routiens/view/effective_period_view.dart | 2 +- .../conditions_routines_devices_view.dart | 26 +-- lib/pages/routiens/widgets/dragable_card.dart | 23 +- lib/pages/routiens/widgets/period_option.dart | 18 +- .../routine_dialogs/ac_dialog.dart} | 4 +- .../routine_dialogs/delay_dialog.dart} | 0 .../effictive_period_dialog.dart} | 0 .../one_gang_switch_dialog.dart} | 3 +- .../routine_dialogs/setting_dialog.dart} | 0 .../three_gang_switch_dialog.dart} | 3 +- .../two_gang_switch_dialog.dart} | 3 +- .../widgets/routine_search_and_buttons.dart | 13 +- .../widgets/scenes_and_automations.dart | 6 +- .../routiens/widgets/then_container.dart | 2 +- lib/services/routines_api.dart | 2 +- lib/utils/constants/assets.dart | 203 ++++++++++++------ pubspec.lock | 32 +++ 23 files changed, 310 insertions(+), 116 deletions(-) create mode 100644 assets/icons/routine/automation.svg create mode 100644 lib/pages/routiens/helper/duration_format_helper.dart rename lib/pages/routiens/{helper/ac_helper.dart => widgets/routine_dialogs/ac_dialog.dart} (99%) rename lib/pages/routiens/{helper/delay_helper.dart => widgets/routine_dialogs/delay_dialog.dart} (100%) rename lib/pages/routiens/{helper/effictive_period_helper.dart => widgets/routine_dialogs/effictive_period_dialog.dart} (100%) rename lib/pages/routiens/{helper/one_gang_switch_helper.dart => widgets/routine_dialogs/one_gang_switch_dialog.dart} (99%) rename lib/pages/routiens/{helper/setting_helper.dart => widgets/routine_dialogs/setting_dialog.dart} (100%) rename lib/pages/routiens/{helper/three_gang_switch_helper.dart => widgets/routine_dialogs/three_gang_switch_dialog.dart} (99%) rename lib/pages/routiens/{helper/two_gang_switch_helper.dart => widgets/routine_dialogs/two_gang_switch_dialog.dart} (99%) diff --git a/assets/icons/routine/automation.svg b/assets/icons/routine/automation.svg new file mode 100644 index 00000000..a67aadaf --- /dev/null +++ b/assets/icons/routine/automation.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/pages/common/text_field/custom_text_field.dart b/lib/pages/common/text_field/custom_text_field.dart index 3ba71cd1..bb574e74 100644 --- a/lib/pages/common/text_field/custom_text_field.dart +++ b/lib/pages/common/text_field/custom_text_field.dart @@ -109,13 +109,25 @@ class CustomTextField extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, children: [ - Text( - title, - style: context.textTheme.bodyMedium!.copyWith( - fontSize: 13, - fontWeight: FontWeight.w600, - color: const Color(0xff000000), - ), + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Visibility( + visible: isRequired == true, + child: Text('* ', + style: context.textTheme.bodyMedium! + .copyWith(color: Colors.red, fontSize: 13)), + ), + Text( + title, + style: context.textTheme.bodyMedium!.copyWith( + fontSize: 13, + fontWeight: FontWeight.w600, + color: const Color(0xff000000), + ), + ), + ], ), const SizedBox(height: 8), Material( diff --git a/lib/pages/routiens/helper/dialog_helper/device_dialog_helper.dart b/lib/pages/routiens/helper/dialog_helper/device_dialog_helper.dart index fcb8e740..33cfffee 100644 --- a/lib/pages/routiens/helper/dialog_helper/device_dialog_helper.dart +++ b/lib/pages/routiens/helper/dialog_helper/device_dialog_helper.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; -import 'package:syncrow_web/pages/routiens/helper/ac_helper.dart'; -import 'package:syncrow_web/pages/routiens/helper/one_gang_switch_helper.dart'; -import 'package:syncrow_web/pages/routiens/helper/three_gang_switch_helper.dart'; -import 'package:syncrow_web/pages/routiens/helper/two_gang_switch_helper.dart'; +import 'package:syncrow_web/pages/routiens/widgets/routine_dialogs/ac_dialog.dart'; +import 'package:syncrow_web/pages/routiens/widgets/routine_dialogs/one_gang_switch_dialog.dart'; +import 'package:syncrow_web/pages/routiens/widgets/routine_dialogs/three_gang_switch_dialog.dart'; +import 'package:syncrow_web/pages/routiens/widgets/routine_dialogs/two_gang_switch_dialog.dart'; import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; class DeviceDialogHelper { diff --git a/lib/pages/routiens/helper/duration_format_helper.dart b/lib/pages/routiens/helper/duration_format_helper.dart new file mode 100644 index 00000000..1f22c9b2 --- /dev/null +++ b/lib/pages/routiens/helper/duration_format_helper.dart @@ -0,0 +1,15 @@ +class DurationFormatMixin { + static String formatDuration(int seconds) { + if (seconds >= 3600) { + final hours = (seconds / 3600).floor(); + final remainingMinutes = ((seconds % 3600) / 60).floor(); + final remainingSeconds = seconds % 60; + return '$hours h ${remainingMinutes}m ${remainingSeconds}s'; + } else if (seconds >= 60) { + final minutes = (seconds / 60).floor(); + final remainingSeconds = seconds % 60; + return '$minutes m ${remainingSeconds}s'; + } + return '${seconds}s'; + } +} diff --git a/lib/pages/routiens/models/ac/ac_function.dart b/lib/pages/routiens/models/ac/ac_function.dart index e5fa78ba..8feccda7 100644 --- a/lib/pages/routiens/models/ac/ac_function.dart +++ b/lib/pages/routiens/models/ac/ac_function.dart @@ -73,9 +73,9 @@ class TempSetFunction extends ACFunction { final int step; TempSetFunction({required super.deviceId, required super.deviceName}) - : min = 200, + : min = 160, max = 300, - step = 5, + step = 1, super( code: 'temp_set', operationName: 'Set Temperature', diff --git a/lib/pages/routiens/models/routine_model.dart b/lib/pages/routiens/models/routine_model.dart index 8035b044..e2075579 100644 --- a/lib/pages/routiens/models/routine_model.dart +++ b/lib/pages/routiens/models/routine_model.dart @@ -1,3 +1,5 @@ +import 'package:syncrow_web/utils/constants/assets.dart'; + class ScenesModel { final String id; final String name; @@ -13,12 +15,16 @@ class ScenesModel { this.icon, }); - factory ScenesModel.fromJson(Map json) => ScenesModel( + factory ScenesModel.fromJson(Map json, + {bool? isAutomation}) => + ScenesModel( id: json["id"], name: json["name"] ?? '', status: json["status"] ?? '', type: json["type"] ?? '', - icon: json["icon"] as String?, + icon: (isAutomation ?? false) + ? Assets.automation + : json["icon"] as String?, ); Map toJson() => { diff --git a/lib/pages/routiens/view/effective_period_view.dart b/lib/pages/routiens/view/effective_period_view.dart index 5bcd9ff0..d9d9bc2f 100644 --- a/lib/pages/routiens/view/effective_period_view.dart +++ b/lib/pages/routiens/view/effective_period_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_bloc.dart'; -import 'package:syncrow_web/pages/routiens/helper/effictive_period_helper.dart'; +import 'package:syncrow_web/pages/routiens/widgets/routine_dialogs/effictive_period_dialog.dart'; import 'package:syncrow_web/pages/routiens/widgets/period_option.dart'; import 'package:syncrow_web/pages/routiens/widgets/repeat_days.dart'; import 'package:syncrow_web/utils/color_manager.dart'; diff --git a/lib/pages/routiens/widgets/conditions_routines_devices_view.dart b/lib/pages/routiens/widgets/conditions_routines_devices_view.dart index 89b1f3f0..5cc31bf3 100644 --- a/lib/pages/routiens/widgets/conditions_routines_devices_view.dart +++ b/lib/pages/routiens/widgets/conditions_routines_devices_view.dart @@ -16,7 +16,7 @@ class ConditionsRoutinesDevicesView extends StatelessWidget { return BlocBuilder( builder: (context, state) { return const Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), + padding: EdgeInsets.symmetric(horizontal: 8.0), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -65,13 +65,13 @@ class ConditionsRoutinesDevicesView extends StatelessWidget { ), ], ), - const SizedBox(height: 10), - const TitleRoutine( + SizedBox(height: 10), + TitleRoutine( title: 'Conditions', subtitle: '(THEN)', ), - const SizedBox(height: 10), - const Wrap( + SizedBox(height: 10), + Wrap( spacing: 10, runSpacing: 10, children: [ @@ -96,20 +96,20 @@ class ConditionsRoutinesDevicesView extends StatelessWidget { ), ], ), - const SizedBox(height: 10), - const TitleRoutine( + SizedBox(height: 10), + TitleRoutine( title: 'Routines', subtitle: '(THEN)', ), - const SizedBox(height: 10), - const ScenesAndAutomations(), - const SizedBox(height: 10), - const TitleRoutine( + SizedBox(height: 10), + ScenesAndAutomations(), + SizedBox(height: 10), + TitleRoutine( title: 'Devices', subtitle: '', ), - const SizedBox(height: 10), - const RoutineDevices(), + SizedBox(height: 10), + RoutineDevices(), ], ), ), diff --git a/lib/pages/routiens/widgets/dragable_card.dart b/lib/pages/routiens/widgets/dragable_card.dart index ae66ce02..db8ad1c9 100644 --- a/lib/pages/routiens/widgets/dragable_card.dart +++ b/lib/pages/routiens/widgets/dragable_card.dart @@ -106,7 +106,7 @@ class DraggableCard extends StatelessWidget { children: [ Expanded( child: Text( - '${function.operationName}: ${function.value}', + '${function.operationName}: ${_formatFunctionValue(function)}', style: context.textTheme.bodySmall?.copyWith( fontSize: 9, color: ColorsManager.textGray, @@ -144,6 +144,27 @@ class DraggableCard extends StatelessWidget { ); } + String _formatFunctionValue(DeviceFunctionData function) { + if (function.functionCode == 'temp_set' || + function.functionCode == 'temp_current') { + return '${(function.value / 10).toStringAsFixed(0)}°C'; + } else if (function.functionCode.contains('countdown')) { + final seconds = function.value.toInt(); + if (seconds >= 3600) { + final hours = (seconds / 3600).floor(); + final remainingMinutes = ((seconds % 3600) / 60).floor(); + final remainingSeconds = seconds % 60; + return '$hours h ${remainingMinutes}m ${remainingSeconds}s'; + } else if (seconds >= 60) { + final minutes = (seconds / 60).floor(); + final remainingSeconds = seconds % 60; + return '$minutes m ${remainingSeconds}s'; + } + return '${seconds}s'; + } + return function.value.toString(); + } + Widget _buildGreyContainer() { return Container( height: 123, diff --git a/lib/pages/routiens/widgets/period_option.dart b/lib/pages/routiens/widgets/period_option.dart index acb69231..8a89e2f9 100644 --- a/lib/pages/routiens/widgets/period_option.dart +++ b/lib/pages/routiens/widgets/period_option.dart @@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_bloc.dart'; import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_event.dart'; import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_state.dart'; -import 'package:syncrow_web/pages/routiens/helper/effictive_period_helper.dart'; +import 'package:syncrow_web/pages/routiens/widgets/routine_dialogs/effictive_period_dialog.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/app_enum.dart'; @@ -21,9 +21,12 @@ class PeriodOptions extends StatelessWidget { builder: (context, state) { return Column( children: [ - _buildRadioOption(context, EnumEffectivePeriodOptions.allDay, '24 Hours'), - _buildRadioOption(context, EnumEffectivePeriodOptions.daytime, 'Sunrise to Sunset'), - _buildRadioOption(context, EnumEffectivePeriodOptions.night, 'Sunset to Sunrise'), + _buildRadioOption( + context, EnumEffectivePeriodOptions.allDay, '24 Hours'), + _buildRadioOption(context, EnumEffectivePeriodOptions.daytime, + 'Sunrise to Sunset'), + _buildRadioOption( + context, EnumEffectivePeriodOptions.night, 'Sunset to Sunrise'), ListTile( contentPadding: EdgeInsets.zero, onTap: () => showCustomTimePicker(context), @@ -34,7 +37,8 @@ class PeriodOptions extends StatelessWidget { fontWeight: FontWeight.w400, fontSize: 14), ), - subtitle: state.customStartTime != null && state.customEndTime != null + subtitle: state.customStartTime != null && + state.customEndTime != null ? Text( '${"${state.customStartTime}"} - ${"${state.customEndTime}"}', style: Theme.of(context).textTheme.bodyMedium!.copyWith( @@ -79,7 +83,9 @@ class PeriodOptions extends StatelessWidget { subtitle: Text( subtitle, style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: ColorsManager.textPrimaryColor, fontWeight: FontWeight.w400, fontSize: 10), + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 10), ), trailing: Radio( value: value, diff --git a/lib/pages/routiens/helper/ac_helper.dart b/lib/pages/routiens/widgets/routine_dialogs/ac_dialog.dart similarity index 99% rename from lib/pages/routiens/helper/ac_helper.dart rename to lib/pages/routiens/widgets/routine_dialogs/ac_dialog.dart index 10606b69..e8b87a16 100644 --- a/lib/pages/routiens/helper/ac_helper.dart +++ b/lib/pages/routiens/widgets/routine_dialogs/ac_dialog.dart @@ -327,11 +327,11 @@ class ACHelper { String selectCode, ) { return Slider( - value: initialValue is int ? initialValue.toDouble() : 200.0, + value: initialValue is int ? initialValue.toDouble() : 160.0, min: 160, max: 300, divisions: 14, - label: '${((initialValue ?? 200) / 10).toInt()}°C', + label: '${((initialValue ?? 160) / 10).toInt()}°C', onChanged: (value) { context.read().add( AddFunction( diff --git a/lib/pages/routiens/helper/delay_helper.dart b/lib/pages/routiens/widgets/routine_dialogs/delay_dialog.dart similarity index 100% rename from lib/pages/routiens/helper/delay_helper.dart rename to lib/pages/routiens/widgets/routine_dialogs/delay_dialog.dart diff --git a/lib/pages/routiens/helper/effictive_period_helper.dart b/lib/pages/routiens/widgets/routine_dialogs/effictive_period_dialog.dart similarity index 100% rename from lib/pages/routiens/helper/effictive_period_helper.dart rename to lib/pages/routiens/widgets/routine_dialogs/effictive_period_dialog.dart diff --git a/lib/pages/routiens/helper/one_gang_switch_helper.dart b/lib/pages/routiens/widgets/routine_dialogs/one_gang_switch_dialog.dart similarity index 99% rename from lib/pages/routiens/helper/one_gang_switch_helper.dart rename to lib/pages/routiens/widgets/routine_dialogs/one_gang_switch_dialog.dart index c7d5d35a..ad25c657 100644 --- a/lib/pages/routiens/helper/one_gang_switch_helper.dart +++ b/lib/pages/routiens/widgets/routine_dialogs/one_gang_switch_dialog.dart @@ -4,6 +4,7 @@ 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/routiens/bloc/functions_bloc/functions_bloc_bloc.dart'; import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/helper/duration_format_helper.dart'; import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart'; import 'package:syncrow_web/pages/routiens/models/gang_switches/switch_operational_value.dart'; @@ -277,7 +278,7 @@ class OneGangSwitchHelper { borderRadius: BorderRadius.circular(10), ), child: Text( - '${initialValue ?? 0} sec', + DurationFormatMixin.formatDuration(initialValue?.toInt() ?? 0), style: context.textTheme.headlineMedium!.copyWith( color: ColorsManager.primaryColorWithOpacity, ), diff --git a/lib/pages/routiens/helper/setting_helper.dart b/lib/pages/routiens/widgets/routine_dialogs/setting_dialog.dart similarity index 100% rename from lib/pages/routiens/helper/setting_helper.dart rename to lib/pages/routiens/widgets/routine_dialogs/setting_dialog.dart diff --git a/lib/pages/routiens/helper/three_gang_switch_helper.dart b/lib/pages/routiens/widgets/routine_dialogs/three_gang_switch_dialog.dart similarity index 99% rename from lib/pages/routiens/helper/three_gang_switch_helper.dart rename to lib/pages/routiens/widgets/routine_dialogs/three_gang_switch_dialog.dart index 4a4c5e02..4f0d85a7 100644 --- a/lib/pages/routiens/helper/three_gang_switch_helper.dart +++ b/lib/pages/routiens/widgets/routine_dialogs/three_gang_switch_dialog.dart @@ -4,6 +4,7 @@ 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/routiens/bloc/functions_bloc/functions_bloc_bloc.dart'; import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/helper/duration_format_helper.dart'; import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart'; import 'package:syncrow_web/pages/routiens/models/gang_switches/switch_operational_value.dart'; @@ -279,7 +280,7 @@ class ThreeGangSwitchHelper { borderRadius: BorderRadius.circular(10), ), child: Text( - '${initialValue ?? 0} sec', + DurationFormatMixin.formatDuration(initialValue?.toInt() ?? 0), style: context.textTheme.headlineMedium!.copyWith( color: ColorsManager.primaryColorWithOpacity, ), diff --git a/lib/pages/routiens/helper/two_gang_switch_helper.dart b/lib/pages/routiens/widgets/routine_dialogs/two_gang_switch_dialog.dart similarity index 99% rename from lib/pages/routiens/helper/two_gang_switch_helper.dart rename to lib/pages/routiens/widgets/routine_dialogs/two_gang_switch_dialog.dart index 3805882a..8282bc7b 100644 --- a/lib/pages/routiens/helper/two_gang_switch_helper.dart +++ b/lib/pages/routiens/widgets/routine_dialogs/two_gang_switch_dialog.dart @@ -4,6 +4,7 @@ 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/routiens/bloc/functions_bloc/functions_bloc_bloc.dart'; import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/helper/duration_format_helper.dart'; import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; import 'package:syncrow_web/pages/routiens/models/gang_switches/base_switch_function.dart'; import 'package:syncrow_web/pages/routiens/models/gang_switches/switch_operational_value.dart'; @@ -278,7 +279,7 @@ class TwoGangSwitchHelper { borderRadius: BorderRadius.circular(10), ), child: Text( - '${initialValue ?? 0} sec', + DurationFormatMixin.formatDuration(initialValue?.toInt() ?? 0), style: context.textTheme.headlineMedium!.copyWith( color: ColorsManager.primaryColorWithOpacity, ), diff --git a/lib/pages/routiens/widgets/routine_search_and_buttons.dart b/lib/pages/routiens/widgets/routine_search_and_buttons.dart index 41a47c17..01352bf8 100644 --- a/lib/pages/routiens/widgets/routine_search_and_buttons.dart +++ b/lib/pages/routiens/widgets/routine_search_and_buttons.dart @@ -4,7 +4,7 @@ import 'package:syncrow_web/pages/common/buttons/default_button.dart'; import 'package:syncrow_web/pages/common/text_field/custom_text_field.dart'; import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/helper/save_routine_helper.dart'; -import 'package:syncrow_web/pages/routiens/helper/setting_helper.dart'; +import 'package:syncrow_web/pages/routiens/widgets/routine_dialogs/setting_dialog.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/style.dart'; @@ -31,7 +31,9 @@ class RoutineSearchAndButtons extends StatelessWidget { children: [ ConstrainedBox( constraints: BoxConstraints( - maxWidth: constraints.maxWidth > 700 ? 450 : constraints.maxWidth - 32), + maxWidth: constraints.maxWidth > 700 + ? 450 + : constraints.maxWidth - 32), child: StatefulTextField( title: 'Routine Name', height: 40, @@ -55,11 +57,14 @@ class RoutineSearchAndButtons extends StatelessWidget { child: Center( child: DefaultButton( onPressed: () async { - final result = await SettingHelper.showSettingDialog( + final result = + await SettingHelper.showSettingDialog( context: context, ); if (result != null) { - context.read().add(AddSelectedIcon(result)); + context + .read() + .add(AddSelectedIcon(result)); } }, borderRadius: 15, diff --git a/lib/pages/routiens/widgets/scenes_and_automations.dart b/lib/pages/routiens/widgets/scenes_and_automations.dart index 633f9a82..7a26979c 100644 --- a/lib/pages/routiens/widgets/scenes_and_automations.dart +++ b/lib/pages/routiens/widgets/scenes_and_automations.dart @@ -34,7 +34,9 @@ class _ScenesAndAutomationsState extends State { children: scenes.asMap().entries.map((entry) { final scene = entry.value; if (state.searchText != null && state.searchText!.isNotEmpty) { - return scene.name.toLowerCase().contains(state.searchText!.toLowerCase()) + return scene.name + .toLowerCase() + .contains(state.searchText!.toLowerCase()) ? DraggableCard( imagePath: Assets.logo, title: scene.name, @@ -49,7 +51,7 @@ class _ScenesAndAutomationsState extends State { : Container(); } else { return DraggableCard( - imagePath: Assets.logo, + imagePath: scene.icon ?? Assets.loginLogo, title: scene.name, deviceData: { 'deviceId': scene.id, diff --git a/lib/pages/routiens/widgets/then_container.dart b/lib/pages/routiens/widgets/then_container.dart index d6c9500f..2bc65b0e 100644 --- a/lib/pages/routiens/widgets/then_container.dart +++ b/lib/pages/routiens/widgets/then_container.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; -import 'package:syncrow_web/pages/routiens/helper/delay_helper.dart'; +import 'package:syncrow_web/pages/routiens/widgets/routine_dialogs/delay_dialog.dart'; import 'package:syncrow_web/pages/routiens/helper/dialog_helper/device_dialog_helper.dart'; import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; diff --git a/lib/services/routines_api.dart b/lib/services/routines_api.dart index abf5099e..0ce106b2 100644 --- a/lib/services/routines_api.dart +++ b/lib/services/routines_api.dart @@ -93,7 +93,7 @@ class SceneApi { expectedResponseModel: (json) { List scenes = []; for (var scene in json) { - scenes.add(ScenesModel.fromJson(scene)); + scenes.add(ScenesModel.fromJson(scene, isAutomation: true)); } return scenes; }, diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index 6c542017..cffaf472 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -13,10 +13,12 @@ class Assets { static const String rightLine = "assets/images/right_line.png"; static const String google = "assets/images/google.svg"; static const String facebook = "assets/images/facebook.svg"; - static const String invisiblePassword = "assets/images/Password_invisible.svg"; + 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"; @@ -29,13 +31,15 @@ class Assets { static const String emptyTable = "assets/images/empty_table.svg"; // General assets - static const String motionlessDetection = "assets/icons/motionless_detection.svg"; + static const String motionlessDetection = + "assets/icons/motionless_detection.svg"; static const String acHeating = "assets/icons/ac_heating.svg"; static const String acPowerOff = "assets/icons/ac_power_off.svg"; 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,35 +60,56 @@ 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 - static const String tempPasswordUnlock = "assets/icons/automation_functions/temp_password_unlock.svg"; - static const String doorlockNormalOpen = "assets/icons/automation_functions/doorlock_normal_open.svg"; - 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 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 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 remoteUnlockRequest = "assets/icons/automation_functions/remote_unlock_req.svg"; - static const String cardUnlock = "assets/icons/automation_functions/card_unlock.svg"; + static const String tempPasswordUnlock = + "assets/icons/automation_functions/temp_password_unlock.svg"; + static const String doorlockNormalOpen = + "assets/icons/automation_functions/doorlock_normal_open.svg"; + 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 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 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 remoteUnlockRequest = + "assets/icons/automation_functions/remote_unlock_req.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"; + static const String fingerprintUnlock = + "assets/icons/automation_functions/fingerprint_unlock.svg"; // Presence Sensor Assets static const String sensorMotionIcon = "assets/icons/sensor_motion_ic.svg"; - static const String sensorPresenceIcon = "assets/icons/sensor_presence_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 presenceRecordIcon = "assets/icons/presence_record_ic.svg"; - static const String helpDescriptionIcon = "assets/icons/help_description_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"; static const String lightPulp = "assets/icons/light_pulb.svg"; static const String acDevice = "assets/icons/ac_device.svg"; @@ -158,7 +183,8 @@ class Assets { //assets/icons/water_leak_normal.svg static const String waterLeakNormal = 'assets/icons/water_leak_normal.svg'; //assets/icons/water_leak_detected.svg - static const String waterLeakDetected = 'assets/icons/water_leak_detected.svg'; + static const String waterLeakDetected = + 'assets/icons/water_leak_detected.svg'; //assets/icons/automation_records.svg static const String automationRecords = 'assets/icons/automation_records.svg'; @@ -212,57 +238,100 @@ 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 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 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 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 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 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 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 assetsSwitchAlarmSound = "assets/icons/functions_icons/switch_alarm_sound.svg"; - static const String assetsResetOff = "assets/icons/functions_icons/reset_off.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 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 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 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 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 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 assetsSwitchAlarmSound = + "assets/icons/functions_icons/switch_alarm_sound.svg"; + static const String assetsResetOff = + "assets/icons/functions_icons/reset_off.svg"; // Assets for automation_functions - static const String assetsCardUnlock = "assets/icons/functions_icons/automation_functions/card_unlock.svg"; - static const String assetsDoorbell = "assets/icons/functions_icons/automation_functions/doorbell.svg"; + static const String assetsCardUnlock = + "assets/icons/functions_icons/automation_functions/card_unlock.svg"; + static const String assetsDoorbell = + "assets/icons/functions_icons/automation_functions/doorbell.svg"; static const String assetsDoorlockNormalOpen = "assets/icons/functions_icons/automation_functions/doorlock_normal_open.svg"; - static const String assetsDoubleLock = "assets/icons/functions_icons/automation_functions/double_lock.svg"; + static const String assetsDoubleLock = + "assets/icons/functions_icons/automation_functions/double_lock.svg"; static const String assetsFingerprintUnlock = "assets/icons/functions_icons/automation_functions/fingerprint_unlock.svg"; - static const String assetsHijackAlarm = "assets/icons/functions_icons/automation_functions/hijack_alarm.svg"; - static const String assetsLockAlarm = "assets/icons/functions_icons/automation_functions/lock_alarm.svg"; - static const String assetsPasswordUnlock = "assets/icons/functions_icons/automation_functions/password_unlock.svg"; - static const String assetsRemoteUnlockReq = "assets/icons/functions_icons/automation_functions/remote_unlock_req.svg"; + static const String assetsHijackAlarm = + "assets/icons/functions_icons/automation_functions/hijack_alarm.svg"; + static const String assetsLockAlarm = + "assets/icons/functions_icons/automation_functions/lock_alarm.svg"; + static const String assetsPasswordUnlock = + "assets/icons/functions_icons/automation_functions/password_unlock.svg"; + static const String assetsRemoteUnlockReq = + "assets/icons/functions_icons/automation_functions/remote_unlock_req.svg"; static const String assetsRemoteUnlockViaApp = "assets/icons/functions_icons/automation_functions/remote_unlock_via_app.svg"; static const String assetsResidualElectricity = "assets/icons/functions_icons/automation_functions/residual_electricity.svg"; static const String assetsTempPasswordUnlock = "assets/icons/functions_icons/automation_functions/temp_password_unlock.svg"; - static const String assetsSelfTestResult = "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 assetsCurrentTemp = "assets/icons/functions_icons/automation_functions/current_temp.svg"; - static const String assetsPresenceState = "assets/icons/functions_icons/automation_functions/presence_state.svg"; + static const String assetsSelfTestResult = + "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 assetsCurrentTemp = + "assets/icons/functions_icons/automation_functions/current_temp.svg"; + static const String assetsPresenceState = + "assets/icons/functions_icons/automation_functions/presence_state.svg"; + //assets/icons/routine/automation.svg + static const String automation = 'assets/icons/routine/automation.svg'; } diff --git a/pubspec.lock b/pubspec.lock index 3e12d6c5..cfd8b310 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" cupertino_icons: dependency: "direct main" description: @@ -137,6 +145,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" fl_chart: dependency: "direct main" description: @@ -533,6 +549,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: @@ -589,6 +613,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" vector_graphics: dependency: transitive description: From 8b7642971418dc4b9846e88eae567c79a2c3c316 Mon Sep 17 00:00:00 2001 From: ashrafzarkanisala Date: Mon, 25 Nov 2024 10:15:31 +0300 Subject: [PATCH 22/23] push devices managment screen --- .../view/device_managment_page.dart | 47 +++++++++---------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/lib/pages/device_managment/all_devices/view/device_managment_page.dart b/lib/pages/device_managment/all_devices/view/device_managment_page.dart index 4044c056..ffc57131 100644 --- a/lib/pages/device_managment/all_devices/view/device_managment_page.dart +++ b/lib/pages/device_managment/all_devices/view/device_managment_page.dart @@ -88,32 +88,31 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout { ); }), rightBody: const NavigateHomeGridView(), - scaffoldBody: CreateNewRoutineView(), - // BlocBuilder( - // builder: (context, state) { - // if (state is SelectedTabState && state.selectedTab) { - // return const RoutinesView(); - // } - // if (state is ShowCreateRoutineState && state.showCreateRoutine) { - // return const CreateNewRoutineView(); - // } + scaffoldBody: BlocBuilder( + builder: (context, state) { + if (state is SelectedTabState && state.selectedTab) { + return const RoutinesView(); + } + if (state is ShowCreateRoutineState && state.showCreateRoutine) { + return const CreateNewRoutineView(); + } - // return BlocBuilder( - // builder: (context, deviceState) { - // if (deviceState is DeviceManagementLoading) { - // return const Center(child: CircularProgressIndicator()); - // } else if (deviceState is DeviceManagementLoaded || - // deviceState is DeviceManagementFiltered) { - // final devices = (deviceState as dynamic).devices ?? - // (deviceState as DeviceManagementFiltered).filteredDevices; + return BlocBuilder( + builder: (context, deviceState) { + if (deviceState is DeviceManagementLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (deviceState is DeviceManagementLoaded || + deviceState is DeviceManagementFiltered) { + final devices = (deviceState as dynamic).devices ?? + (deviceState as DeviceManagementFiltered).filteredDevices; - // return DeviceManagementBody(devices: devices); - // } else { - // return const Center(child: Text('Error fetching Devices')); - // } - // }, - // ); - // }), + return DeviceManagementBody(devices: devices); + } else { + return const Center(child: Text('Error fetching Devices')); + } + }, + ); + }), ), ); } From bbe00c0372f75ea591f472bb2c3ccd72d9fd81e6 Mon Sep 17 00:00:00 2001 From: ashrafzarkanisala Date: Mon, 25 Nov 2024 10:23:26 +0300 Subject: [PATCH 23/23] push 24 hour coundown --- .../widgets/routine_dialogs/one_gang_switch_dialog.dart | 2 +- .../widgets/routine_dialogs/three_gang_switch_dialog.dart | 2 +- .../widgets/routine_dialogs/two_gang_switch_dialog.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pages/routiens/widgets/routine_dialogs/one_gang_switch_dialog.dart b/lib/pages/routiens/widgets/routine_dialogs/one_gang_switch_dialog.dart index ad25c657..00f936d6 100644 --- a/lib/pages/routiens/widgets/routine_dialogs/one_gang_switch_dialog.dart +++ b/lib/pages/routiens/widgets/routine_dialogs/one_gang_switch_dialog.dart @@ -299,7 +299,7 @@ class OneGangSwitchHelper { description: "sec", value: 0.0, minValue: 0, - maxValue: 43200, + maxValue: 86400, stepValue: 1, ); return Slider( diff --git a/lib/pages/routiens/widgets/routine_dialogs/three_gang_switch_dialog.dart b/lib/pages/routiens/widgets/routine_dialogs/three_gang_switch_dialog.dart index 4f0d85a7..ae704ded 100644 --- a/lib/pages/routiens/widgets/routine_dialogs/three_gang_switch_dialog.dart +++ b/lib/pages/routiens/widgets/routine_dialogs/three_gang_switch_dialog.dart @@ -301,7 +301,7 @@ class ThreeGangSwitchHelper { description: "sec", value: 0.0, minValue: 0, - maxValue: 43200, + maxValue: 86400, stepValue: 1, ); return Slider( diff --git a/lib/pages/routiens/widgets/routine_dialogs/two_gang_switch_dialog.dart b/lib/pages/routiens/widgets/routine_dialogs/two_gang_switch_dialog.dart index 8282bc7b..d017103e 100644 --- a/lib/pages/routiens/widgets/routine_dialogs/two_gang_switch_dialog.dart +++ b/lib/pages/routiens/widgets/routine_dialogs/two_gang_switch_dialog.dart @@ -300,7 +300,7 @@ class TwoGangSwitchHelper { description: "sec", value: 0.0, minValue: 0, - maxValue: 43200, + maxValue: 86400, stepValue: 1, ); return Slider(