From e84df8b7a6a5d476270c91dd4e042b4e1a239b40 Mon Sep 17 00:00:00 2001 From: Abdullah Alassaf Date: Sat, 23 Nov 2024 21:44:47 +0300 Subject: [PATCH] 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: