diff --git a/lib/features/scene/bloc/effective_period/effect_period_bloc.dart b/lib/features/scene/bloc/effective_period/effect_period_bloc.dart new file mode 100644 index 0000000..710b124 --- /dev/null +++ b/lib/features/scene/bloc/effective_period/effect_period_bloc.dart @@ -0,0 +1,30 @@ +import 'package:bloc/bloc.dart'; +import 'package:syncrow_app/features/scene/bloc/effective_period/effect_period_event.dart'; +import 'package:syncrow_app/features/scene/bloc/effective_period/effect_period_state.dart'; + +class EffectPeriodBloc extends Bloc { + EffectPeriodBloc() : super(EffectPeriodState.initial()) { + on(_onSetPeriod); + on(_onToggleDay); + on(_onSetCustomTime); + } + + void _onSetPeriod(SetPeriod event, Emitter emit) { + emit(state.copyWith(selectedPeriod: event.period)); + } + + void _onToggleDay(ToggleDay event, Emitter emit) { + final days = List.of(state.selectedDays); + if (days.contains(event.day)) { + days.remove(event.day); + } else { + days.add(event.day); + } + emit(state.copyWith(selectedDays: days)); + } + + void _onSetCustomTime(SetCustomTime event, Emitter emit) { + emit(state.copyWith( + customStartTime: event.startTime, customEndTime: event.endTime)); + } +} diff --git a/lib/features/scene/bloc/effective_period/effect_period_event.dart b/lib/features/scene/bloc/effective_period/effect_period_event.dart new file mode 100644 index 0000000..12ac37e --- /dev/null +++ b/lib/features/scene/bloc/effective_period/effect_period_event.dart @@ -0,0 +1,36 @@ +import 'package:equatable/equatable.dart'; + +abstract class EffectPeriodEvent extends Equatable { + const EffectPeriodEvent(); + + @override + List get props => []; +} + +class SetPeriod extends EffectPeriodEvent { + final String 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]; +} diff --git a/lib/features/scene/bloc/effective_period/effect_period_state.dart b/lib/features/scene/bloc/effective_period/effect_period_state.dart new file mode 100644 index 0000000..67c3424 --- /dev/null +++ b/lib/features/scene/bloc/effective_period/effect_period_state.dart @@ -0,0 +1,42 @@ +import 'package:equatable/equatable.dart'; + +class EffectPeriodState extends Equatable { + final String selectedPeriod; + final List selectedDays; + final String? customStartTime; + final String? customEndTime; + + const EffectPeriodState({ + required this.selectedPeriod, + required this.selectedDays, + this.customStartTime, + this.customEndTime, + }); + + factory EffectPeriodState.initial() { + return const EffectPeriodState( + selectedPeriod: 'All Day', + selectedDays: [], + customStartTime: null, + customEndTime: null, + ); + } + + EffectPeriodState copyWith({ + String? selectedPeriod, + List? selectedDays, + String? customStartTime, + String? customEndTime, + }) { + return EffectPeriodState( + selectedPeriod: selectedPeriod ?? this.selectedPeriod, + selectedDays: selectedDays ?? this.selectedDays, + customStartTime: customStartTime ?? this.customStartTime, + customEndTime: customEndTime ?? this.customEndTime, + ); + } + + @override + List get props => + [selectedPeriod, selectedDays, customStartTime, customEndTime]; +} diff --git a/lib/features/scene/view/scene_auto_settings.dart b/lib/features/scene/view/scene_auto_settings.dart new file mode 100644 index 0000000..cfffe92 --- /dev/null +++ b/lib/features/scene/view/scene_auto_settings.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_app/features/scene/view/scene_tasks_view.dart'; +import 'package:syncrow_app/features/scene/widgets/bottom_sheet/effective_period_bottom_sheet.dart'; +import 'package:syncrow_app/features/scene/widgets/scene_list_tile.dart'; +import 'package:syncrow_app/features/shared_widgets/default_container.dart'; +import 'package:syncrow_app/features/shared_widgets/text_widgets/body_large.dart'; +import 'package:syncrow_app/utils/context_extension.dart'; +import 'package:syncrow_app/utils/resource_manager/color_manager.dart'; +import 'package:syncrow_app/utils/resource_manager/font_manager.dart'; + +class SceneAutoSettings extends StatelessWidget { + const SceneAutoSettings({super.key}); + + @override + Widget build(BuildContext context) { + final sceneSettings = + ModalRoute.of(context)!.settings.arguments as Map? ?? + {}; + final sceneId = sceneSettings['sceneId'] as String? ?? ''; + final isAutomation = sceneSettings['isAutomation'] as bool? ?? false; + final sceneName = sceneSettings['sceneName'] as String? ?? ''; + + return Scaffold( + backgroundColor: ColorsManager.backgroundColor, + appBar: AppBar( + centerTitle: true, + backgroundColor: ColorsManager.backgroundColor, + title: const BodyLarge( + text: "Settings", + fontColor: ColorsManager.secondaryColor, + fontWeight: FontsManager.bold, + ), + ), + body: DefaultContainer( + padding: EdgeInsets.zero, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SceneListTile( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + titleString: "Effective Period", + trailingWidget: const Icon(Icons.arrow_forward_ios_rounded), + onPressed: () { + context.customBottomSheet( + child: const EffectPeriodBottomSheetContent(), + ); + }, + ), + Visibility( + visible: sceneName.isNotEmpty, + child: DeleteBottomSheetContent( + isAutomation: isAutomation, + sceneId: sceneId, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/scene/view/scene_tasks_view.dart b/lib/features/scene/view/scene_tasks_view.dart index 86863c4..22371b4 100644 --- a/lib/features/scene/view/scene_tasks_view.dart +++ b/lib/features/scene/view/scene_tasks_view.dart @@ -40,15 +40,17 @@ class SceneTasksView extends StatelessWidget { SizedBox( width: 40, child: GestureDetector( - onTap: sceneSettings.sceneName.isEmpty - ? null - : () { - context.customBottomSheet( - child: DeleteBottomSheetContent( - sceneId: sceneSettings.sceneId, - isAutomation: isAutomation), - ); - }, + onTap: () { + Navigator.pushNamed( + context, + Routes.sceneAutoSettingsRoute, + arguments: { + "sceneId": sceneSettings.sceneId, + "isAutomation": isAutomation, + "sceneName": sceneSettings.sceneName, + }, + ); + }, child: SvgPicture.asset( Assets.assetsIconsSettings, colorFilter: const ColorFilter.mode( @@ -109,48 +111,43 @@ class DeleteBottomSheetContent extends StatelessWidget { @override Widget build(BuildContext context) { return DefaultContainer( - height: context.height * 0.2, - child: ListView( - shrinkWrap: true, - children: [ - BlocConsumer( - listener: (context, state) { - if (state is DeleteSceneSuccess) { - if (state.success) { - navigateToRoute(context, Routes.homeRoute); - BlocProvider.of(context).add( - LoadScenes(HomeCubit.getInstance().selectedSpace!.id!)); - BlocProvider.of(context).add(LoadAutomation( - HomeCubit.getInstance().selectedSpace!.id!)); - } - } - }, - builder: (context, state) { - return DefaultContainer( - onTap: () { - context.read().add(DeleteSceneEvent( - sceneId: sceneId, - unitUuid: - HomeCubit.getInstance().selectedSpace!.id!, - )); - }, - child: SceneListTile( - titleString: isAutomation - ? StringsManager.deleteAutomation - : StringsManager.deleteScene, - leadingWidget: (state is DeleteSceneLoading) - ? const SizedBox( - height: 24, - width: 24, - child: CircularProgressIndicator()) - : SvgPicture.asset( - Assets.assetsDeleteIcon, - color: ColorsManager.red, - ), - )); - }, - ), - ], + padding: EdgeInsets.zero, + child: BlocConsumer( + listener: (context, state) { + if (state is DeleteSceneSuccess) { + if (state.success) { + navigateToRoute(context, Routes.homeRoute); + BlocProvider.of(context).add( + LoadScenes(HomeCubit.getInstance().selectedSpace!.id!)); + BlocProvider.of(context).add( + LoadAutomation(HomeCubit.getInstance().selectedSpace!.id!)); + } + } + }, + builder: (context, state) { + return DefaultContainer( + onTap: () { + context.read().add(DeleteSceneEvent( + sceneId: sceneId, + unitUuid: HomeCubit.getInstance().selectedSpace!.id!, + )); + }, + child: SceneListTile( + padding: const EdgeInsets.symmetric(horizontal: 8), + titleString: isAutomation + ? StringsManager.deleteAutomation + : StringsManager.deleteScene, + leadingWidget: (state is DeleteSceneLoading) + ? const SizedBox( + height: 24, + width: 24, + child: CircularProgressIndicator()) + : SvgPicture.asset( + Assets.assetsDeleteIcon, + color: ColorsManager.red, + ), + )); + }, )); } } diff --git a/lib/features/scene/widgets/bottom_sheet/effective_period_bottom_sheet.dart b/lib/features/scene/widgets/bottom_sheet/effective_period_bottom_sheet.dart new file mode 100644 index 0000000..c4c74e0 --- /dev/null +++ b/lib/features/scene/widgets/bottom_sheet/effective_period_bottom_sheet.dart @@ -0,0 +1,314 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_app/features/scene/bloc/effective_period/effect_period_event.dart'; +import 'package:syncrow_app/features/scene/bloc/effective_period/effect_period_state.dart'; +import 'package:syncrow_app/features/scene/bloc/effective_period/effect_period_bloc.dart'; +import 'package:syncrow_app/features/shared_widgets/text_widgets/body_medium.dart'; +import 'package:syncrow_app/features/shared_widgets/text_widgets/body_small.dart'; +import 'package:syncrow_app/utils/context_extension.dart'; +import 'package:syncrow_app/utils/resource_manager/color_manager.dart'; +import 'package:syncrow_app/utils/resource_manager/font_manager.dart'; +import 'package:time_picker_spinner/time_picker_spinner.dart'; + +class EffectPeriodBottomSheetContent extends StatefulWidget { + const EffectPeriodBottomSheetContent({super.key}); + + @override + State createState() => + _EffectPeriodBottomSheetContentState(); +} + +class _EffectPeriodBottomSheetContentState + extends State { + 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); // Fixed 00:00 + DateTime endDateTime = DateTime(2022, 1, 1, 23, 59); // Fixed 23:59 + + context.customAlertDialog( + alertBody: SizedBox( + height: 250, + child: PageView( + controller: pageController, + physics: const NeverScrollableScrollPhysics(), + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + onPressed: () {}, + child: BodyMedium( + text: "Start", + style: context.bodyMedium.copyWith( + color: ColorsManager.primaryColor, + ), + ), + ), + TextButton( + onPressed: () { + pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeIn, + ); + }, + child: const BodyMedium( + text: "End", + ), + ), + ], + ), + ), + 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: startDateTime, + onTimeChange: (time) { + selectedStartTime = + "${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}"; + }, + ), + const SizedBox(height: 16), + ], + ), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + onPressed: () { + pageController.previousPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeIn, + ); + }, + child: const BodyMedium(text: "Start"), + ), + TextButton( + onPressed: () {}, + child: BodyMedium( + text: "End", + style: context.bodyMedium.copyWith( + color: ColorsManager.primaryColor, + ), + ), + ), + ], + ), + ), + 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: endDateTime, + onTimeChange: (time) { + selectedEndTime = + "${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}"; + }, + ), + const SizedBox(height: 16), + ], + ), + ], + ), + ), + title: "Custom", + onConfirm: () { + context.read().add( + SetCustomTime(selectedStartTime, selectedEndTime), + ); + context.read().add( + const SetPeriod('Custom'), + ); + + Navigator.of(context).pop(); + }, + ); + return null; + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => EffectPeriodBloc(), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + BodyMedium( + text: 'Effective Period', + fontColor: ColorsManager.primaryColorWithOpacity, + fontWeight: FontsManager.bold, + ), + const Divider( + color: ColorsManager.backgroundColor, + ), + _buildPeriodOptions(context), + const SizedBox(height: 16), + _buildRepeatDays(context), + const SizedBox(height: 24), + ], + ), + ), + ); + } + + Widget _buildPeriodOptions(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Column( + children: [ + _buildRadioOption(context, 'All Day', '24 Hours'), + _buildRadioOption(context, 'Daytime', 'Sunrise to Sunset'), + _buildRadioOption(context, 'Night', 'Sunset to Sunrise'), + ListTile( + contentPadding: EdgeInsets.zero, + onTap: () => showCustomTimePicker(context), + title: BodyMedium( + text: 'Custom', + fontColor: ColorsManager.primaryColorWithOpacity, + ), + subtitle: + state.customStartTime != null && state.customEndTime != null + ? BodySmall( + text: + '${"${state.customStartTime} AM"} - ${"${state.customEndTime} PM"}', + style: context.bodySmall.copyWith(fontSize: 10), + ) + : BodySmall( + text: '00:00 AM - 11:59 PM', + style: context.bodySmall.copyWith(fontSize: 10), + ), + trailing: Radio( + value: 'Custom', + groupValue: state.selectedPeriod, + onChanged: (value) async {}, + ), + ), + ], + ); + }, + ); + } + + Widget _buildRadioOption( + BuildContext context, String value, String subtitle) { + return BlocBuilder( + builder: (context, state) { + return ListTile( + contentPadding: EdgeInsets.zero, + onTap: () { + context.read().add(SetPeriod(value)); + }, + title: BodyMedium(text: value), + subtitle: BodySmall( + text: subtitle, + style: context.bodySmall.copyWith(fontSize: 10), + ), + trailing: Radio( + value: value, + groupValue: state.selectedPeriod, + onChanged: (value) { + if (value != null) { + context.read().add(SetPeriod(value)); + } + }, + ), + ); + }, + ); + } + + Widget _buildRepeatDays(BuildContext context) { + final daysMap = { + 'Mon': 'M', + 'Tue': 'T', + 'Wed': 'W', + 'Thu': 'T', + 'Fri': 'F', + 'Sat': 'S', + 'Sun': 'S', + }; + + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const BodyMedium(text: 'Repeat'), + const SizedBox(width: 8), + BlocBuilder( + builder: (context, state) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: daysMap.entries.map((entry) { + final day = entry.key; + final abbreviation = entry.value; + final isSelected = state.selectedDays.contains(day); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 3.0), + child: GestureDetector( + onTap: () { + context.read().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: 20, + color: + isSelected ? Colors.grey : Colors.grey.shade300, + ), + ), + ), + ), + ), + ); + }).toList(), + ); + }, + ), + ], + ); + } +} diff --git a/lib/navigation/router.dart b/lib/navigation/router.dart index 1baf285..8bc2839 100644 --- a/lib/navigation/router.dart +++ b/lib/navigation/router.dart @@ -14,6 +14,7 @@ import 'package:syncrow_app/features/menu/view/widgets/profile/profile_view.dart import 'package:syncrow_app/features/scene/bloc/tab_change/tab_change_bloc.dart'; import 'package:syncrow_app/features/scene/bloc/tab_change/tab_change_event.dart'; import 'package:syncrow_app/features/scene/view/device_functions_view.dart'; +import 'package:syncrow_app/features/scene/view/scene_auto_settings.dart'; import 'package:syncrow_app/features/scene/view/scene_tasks_view.dart'; import 'package:syncrow_app/features/scene/view/scene_rooms_tabbar.dart'; import 'package:syncrow_app/features/scene/view/scene_view.dart'; @@ -95,6 +96,11 @@ class Router { builder: (_) => DeviceFunctionsView(), settings: settings, ); + case Routes.sceneAutoSettingsRoute: + return MaterialPageRoute( + builder: (_) => const SceneAutoSettings(), + settings: settings, + ); default: return MaterialPageRoute( builder: (_) => Scaffold( diff --git a/lib/navigation/routing_constants.dart b/lib/navigation/routing_constants.dart index 86826ca..1d2d363 100644 --- a/lib/navigation/routing_constants.dart +++ b/lib/navigation/routing_constants.dart @@ -19,6 +19,7 @@ class Routes { static const String createUnit = '/create-unit'; static const String sceneTasksRoute = '/scene-tasks'; static const String sceneControlDevicesRoute = '/scene-control-devices'; -static const String deviceFunctionsRoute = '/device-functions'; - + static const String deviceFunctionsRoute = '/device-functions'; + static const String sceneAutoSettingsRoute = + '/scene-automation-settings'; }