diff --git a/lib/features/scene/bloc/tab_change/tab_change_bloc.dart b/lib/features/scene/bloc/tab_change/tab_change_bloc.dart index 7fc18db..62f84bb 100644 --- a/lib/features/scene/bloc/tab_change/tab_change_bloc.dart +++ b/lib/features/scene/bloc/tab_change/tab_change_bloc.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_app/features/devices/bloc/device_manager_bloc/device_manager_bloc.dart'; import 'package:syncrow_app/features/devices/bloc/device_manager_bloc/device_manager_event.dart'; @@ -7,19 +5,31 @@ import 'package:syncrow_app/features/scene/bloc/tab_change/tab_change_event.dart import 'package:syncrow_app/features/scene/bloc/tab_change/tab_change_state.dart'; class TabBarBloc extends Bloc { - final DeviceManagerBloc deviceManagerBloc; - TabBarBloc(this.deviceManagerBloc) : super(const Initial()) { - on(_handleTabChanged); + TabBarBloc(this.deviceManagerBloc) : super(const TabBarInitialState()) { + on(_onTabBarTabChangedEvent); } - FutureOr _handleTabChanged( - TabChanged event, Emitter emit) { + final DeviceManagerBloc deviceManagerBloc; + + void _onTabBarTabChangedEvent( + TabBarTabChangedEvent event, + Emitter emit, + ) { + _getDevices(event); + + emit( + TabBarTabSelectedState( + roomId: event.roomId, + selectedTabIndex: event.selectedIndex, + ), + ); + } + + void _getDevices(TabBarTabChangedEvent event) { if (event.roomId == "-1") { deviceManagerBloc.add(FetchAllDevices()); } else { - deviceManagerBloc.add(FetchDevicesByRoomId(event.roomId,event.unit)); + deviceManagerBloc.add(FetchDevicesByRoomId(event.roomId, event.unit)); } - emit(TabSelected( - roomId: event.roomId, selectedTabIndex: event.selectedIndex)); } } diff --git a/lib/features/scene/bloc/tab_change/tab_change_event.dart b/lib/features/scene/bloc/tab_change/tab_change_event.dart index f39d823..aeeda5e 100644 --- a/lib/features/scene/bloc/tab_change/tab_change_event.dart +++ b/lib/features/scene/bloc/tab_change/tab_change_event.dart @@ -4,10 +4,14 @@ abstract class TabBarEvent { const TabBarEvent(); } -class TabChanged extends TabBarEvent { +class TabBarTabChangedEvent extends TabBarEvent { + const TabBarTabChangedEvent({ + required this.selectedIndex, + required this.roomId, + required this.unit, + }); + final int selectedIndex; final String roomId; final SpaceModel unit; - const TabChanged( - {required this.selectedIndex, required this.roomId, required this.unit}); } diff --git a/lib/features/scene/bloc/tab_change/tab_change_state.dart b/lib/features/scene/bloc/tab_change/tab_change_state.dart index 4a42c44..4970f21 100644 --- a/lib/features/scene/bloc/tab_change/tab_change_state.dart +++ b/lib/features/scene/bloc/tab_change/tab_change_state.dart @@ -2,12 +2,16 @@ abstract class TabBarState { const TabBarState(); } -class Initial extends TabBarState { - const Initial(); +class TabBarInitialState extends TabBarState { + const TabBarInitialState(); } -class TabSelected extends TabBarState { +class TabBarTabSelectedState extends TabBarState { + const TabBarTabSelectedState({ + required this.roomId, + required this.selectedTabIndex, + }); + final int selectedTabIndex; final String roomId; - const TabSelected({required this.roomId, required this.selectedTabIndex}); } diff --git a/lib/features/scene/view/scene_rooms_tabbar.dart b/lib/features/scene/view/scene_rooms_tabbar.dart index c677a72..d47f43f 100644 --- a/lib/features/scene/view/scene_rooms_tabbar.dart +++ b/lib/features/scene/view/scene_rooms_tabbar.dart @@ -20,82 +20,69 @@ class SceneRoomsTabBarDevicesView extends StatefulWidget { _SceneRoomsTabBarDevicesViewState(); } -class _SceneRoomsTabBarDevicesViewState - extends State +class _SceneRoomsTabBarDevicesViewState extends State with SingleTickerProviderStateMixin { late final TabController _tabController; - List? rooms = []; late final SpaceModel selectedSpace; + var rooms = []; @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - selectedSpace = HomeCubit.getInstance().selectedSpace!; + selectedSpace = HomeCubit.getInstance().selectedSpace!; - rooms = List.from(selectedSpace.subspaces ?? []); + rooms = List.from(selectedSpace.subspaces); + final defaultSubSpaceModel = SubSpaceModel( + name: 'All Devices', + devices: context.read().allDevices, + id: '-1', + ); - if (rooms != null && rooms!.isNotEmpty) { - if (rooms![0].id != '-1') { - rooms?.insert( - 0, - SubSpaceModel( - name: 'All Devices', - devices: context.read().allDevices, - id: '-1', - ), - ); - } - } else { - rooms = [ - SubSpaceModel( - name: 'All Devices', - devices: context.read().allDevices, - id: '-1', - ) - ]; + if (rooms.isNotEmpty) { + final isFirstRoomIdValid = rooms[0].id != '-1'; + if (isFirstRoomIdValid) { + rooms.insert(0, defaultSubSpaceModel); } - - _tabController = TabController(length: rooms!.length, vsync: this); - _tabController.addListener(_handleTabSwitched); - setState(() {}); - }); - } - - void _handleTabSwitched() { - if (_tabController.indexIsChanging) { - final value = _tabController.index; - - /// select tab - context.read().add(TabChanged( - selectedIndex: value, - roomId: rooms?[value].id ?? '', - unit: selectedSpace)); - return; + } else { + rooms = [defaultSubSpaceModel]; } + + _tabController = TabController(length: rooms.length, vsync: this); + _tabController.addListener(_handleTabSwitched); } @override void dispose() { super.dispose(); - _tabController.dispose(); _tabController.removeListener(() {}); + _tabController.dispose(); + } + + void _handleTabSwitched() { + if (_tabController.indexIsChanging) { + final index = _tabController.index; + + context.read().add( + TabBarTabChangedEvent( + selectedIndex: index, + roomId: rooms[index].id ?? '', + unit: selectedSpace, + ), + ); + return; + } } @override Widget build(BuildContext context) { return DefaultScaffold( - title: StringsManager.createScene, - padding: EdgeInsets.zero, + padding: EdgeInsetsDirectional.zero, leading: IconButton( - onPressed: () { - navigateToRoute(context, Routes.sceneTasksRoute); - }, - icon: const Icon( - Icons.arrow_back_ios, - ), + onPressed: () => navigateToRoute(context, Routes.sceneTasksRoute), + icon: const Icon(Icons.arrow_back_ios), ), + title: StringsManager.createScene, child: SceneDevicesBody(tabController: _tabController, rooms: rooms), ); } diff --git a/lib/features/scene/widgets/scene_devices/scene_devices_body.dart b/lib/features/scene/widgets/scene_devices/scene_devices_body.dart index 0348c93..25fe8e6 100644 --- a/lib/features/scene/widgets/scene_devices/scene_devices_body.dart +++ b/lib/features/scene/widgets/scene_devices/scene_devices_body.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:syncrow_app/features/devices/bloc/device_manager_bloc/device_manager_bloc.dart'; import 'package:syncrow_app/features/devices/bloc/device_manager_bloc/device_manager_state.dart'; import 'package:syncrow_app/features/devices/model/subspace_model.dart'; @@ -8,60 +7,44 @@ import 'package:syncrow_app/features/scene/bloc/tab_change/tab_change_bloc.dart' import 'package:syncrow_app/features/scene/bloc/tab_change/tab_change_state.dart'; import 'package:syncrow_app/features/scene/enum/create_scene_enum.dart'; import 'package:syncrow_app/features/scene/model/scene_settings_route_arguments.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/features/shared_widgets/text_widgets/body_medium.dart'; -import 'package:syncrow_app/navigation/routing_constants.dart'; -import 'package:syncrow_app/utils/context_extension.dart'; -import 'package:syncrow_app/utils/resource_manager/color_manager.dart'; +import 'package:syncrow_app/features/scene/widgets/scene_devices/scene_devices_body_tab_bar.dart'; +import 'package:syncrow_app/features/scene/widgets/scene_devices/scene_devices_list.dart'; +import 'package:syncrow_app/features/shared_widgets/app_loading_indicator.dart'; class SceneDevicesBody extends StatelessWidget { const SceneDevicesBody({ - super.key, - required TabController tabController, + required this.tabController, required this.rooms, - }) : _tabController = tabController; + super.key, + }); - final TabController _tabController; - final List? rooms; + final TabController tabController; + final List rooms; @override Widget build(BuildContext context) { - final isAutomationDeviceStatus = - ((ModalRoute.of(context)?.settings.arguments as SceneSettingsRouteArguments?)?.sceneType == - CreateSceneEnum.deviceStatusChanges.name); return BlocBuilder( - builder: (context, tabState) { + builder: (context, state) { return Column( mainAxisSize: MainAxisSize.min, children: [ - TabBar( - controller: _tabController, - dividerColor: Colors.transparent, - indicatorColor: Colors.transparent, - tabs: [ - ...rooms!.map((e) => Tab( - child: BodyLarge( - text: e.name ?? '', - textAlign: TextAlign.start, - style: context.bodyLarge.copyWith( - color: (tabState is TabSelected) && tabState.roomId == e.id - ? ColorsManager.textPrimaryColor - : ColorsManager.textPrimaryColor.withOpacity(0.2), - ), - ), - )), - ], - isScrollable: true, - tabAlignment: TabAlignment.start, + SceneDevicesBodyTabBar( + tabController: tabController, + rooms: rooms, + selectedRoomId: state is TabBarTabSelectedState ? state.roomId : '-1', ), Expanded( child: TabBarView( - controller: _tabController, physics: const NeverScrollableScrollPhysics(), - children: - rooms!.map((e) => _buildRoomTab(e, context, isAutomationDeviceStatus)).toList(), + controller: tabController, + children: rooms + .map( + (room) => _buildRoomTab( + room, + _isAutomationDeviceStatus(context), + ), + ) + .toList(), ), ), ], @@ -70,52 +53,46 @@ class SceneDevicesBody extends StatelessWidget { ); } - Widget _buildRoomTab(SubSpaceModel room, BuildContext context, bool isAutomationDeviceStatus) { + bool _isAutomationDeviceStatus(BuildContext context) { + final routeArguments = + ModalRoute.of(context)?.settings.arguments as SceneSettingsRouteArguments?; + final deviceStatusChangesScene = CreateSceneEnum.deviceStatusChanges.name; + final sceneType = routeArguments?.sceneType; + + return sceneType == deviceStatusChangesScene; + } + + Widget _buildRoomTab( + SubSpaceModel room, + bool isAutomationDeviceStatus, + ) { return BlocBuilder( builder: (context, state) { - if (state.loading && state.devices == null) { - return const Center(child: CircularProgressIndicator()); - } else if (state.devices != null && state.devices!.isNotEmpty) { - return ListView.builder( - itemCount: state.devices!.length, - itemBuilder: (context, index) { - final device = state.devices![index]; - return DefaultContainer( - child: SceneListTile( - minLeadingWidth: 40, - leadingWidget: SvgPicture.asset(device.icon ?? ''), - titleWidget: BodyMedium( - text: device.name ?? '', - style: context.titleSmall.copyWith( - color: ColorsManager.secondaryTextColor, - fontWeight: FontWeight.w400, - fontSize: 20, - ), - ), - trailingWidget: const Icon( - Icons.arrow_forward_ios_rounded, - color: ColorsManager.greyColor, - size: 16, - ), - onPressed: () { - Navigator.pushNamed( - context, - Routes.deviceFunctionsRoute, - arguments: { - "device": device, - "isAutomationDeviceStatus": isAutomationDeviceStatus - }, - ); - }, - ), - ); - }, - ); - } else if (state.error != null) { - return const Center(child: Text('Something went wrong')); - } else { - return const SizedBox(); - } + final isLoading = state.loading && state.devices == null; + final hasData = + state.devices != null && (state.devices?.isNotEmpty ?? false); + final hasError = state.error != null; + + final widgets = { + isLoading: const AppLoadingIndicator(), + hasError: Center(child: Text('${state.error}')), + hasData: SceneDevicesList( + devices: state.devices ?? [], + isAutomationDeviceStatus: isAutomationDeviceStatus, + ), + }; + + final invalidWidgetEntry = MapEntry( + true, + Center(child: Text('This subspace has no devices')), + ); + + final validWidgetEntry = widgets.entries.firstWhere( + (entry) => entry.key == true, + orElse: () => invalidWidgetEntry, + ); + + return validWidgetEntry.value; }, ); } diff --git a/lib/features/scene/widgets/scene_devices/scene_devices_body_tab_bar.dart b/lib/features/scene/widgets/scene_devices/scene_devices_body_tab_bar.dart new file mode 100644 index 0000000..744a0ff --- /dev/null +++ b/lib/features/scene/widgets/scene_devices/scene_devices_body_tab_bar.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_app/features/devices/model/subspace_model.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'; + +class SceneDevicesBodyTabBar extends StatelessWidget { + const SceneDevicesBodyTabBar({ + required this.tabController, + required this.rooms, + required this.selectedRoomId, + super.key, + }); + + final String selectedRoomId; + + final TabController tabController; + final List rooms; + + @override + Widget build(BuildContext context) { + return TabBar( + controller: tabController, + dividerColor: Colors.transparent, + indicatorColor: Colors.transparent, + isScrollable: true, + tabAlignment: TabAlignment.start, + tabs: rooms.map((e) { + final isSelected = selectedRoomId == e.id; + return Tab( + child: BodyLarge( + text: e.name ?? '', + textAlign: TextAlign.start, + style: context.bodyLarge.copyWith( + color: isSelected + ? ColorsManager.textPrimaryColor + : ColorsManager.textPrimaryColor.withValues( + alpha: 0.2, + ), + ), + ), + ); + }).toList(), + ); + } +} diff --git a/lib/features/scene/widgets/scene_devices/scene_devices_list.dart b/lib/features/scene/widgets/scene_devices/scene_devices_list.dart new file mode 100644 index 0000000..a55e70d --- /dev/null +++ b/lib/features/scene/widgets/scene_devices/scene_devices_list.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_app/features/devices/model/device_model.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_medium.dart'; +import 'package:syncrow_app/navigation/routing_constants.dart'; +import 'package:syncrow_app/utils/context_extension.dart'; +import 'package:syncrow_app/utils/resource_manager/color_manager.dart'; + +class SceneDevicesList extends StatelessWidget { + const SceneDevicesList({ + required this.isAutomationDeviceStatus, + required this.devices, + super.key, + }); + final List devices; + final bool isAutomationDeviceStatus; + + @override + Widget build(BuildContext context) { + return ListView.builder( + itemCount: devices.length, + itemBuilder: (context, index) => _buildDeviceTile(context, devices[index]), + ); + } + + Widget _buildDeviceTile( + BuildContext context, + DeviceModel device, + ) { + return DefaultContainer( + child: SceneListTile( + minLeadingWidth: 40, + leadingWidget: SvgPicture.asset(device.icon ?? ''), + titleWidget: BodyMedium( + text: device.name ?? '', + style: context.titleSmall.copyWith( + color: ColorsManager.secondaryTextColor, + fontWeight: FontWeight.w400, + fontSize: 20, + ), + ), + trailingWidget: const Icon( + Icons.arrow_forward_ios_rounded, + color: ColorsManager.greyColor, + size: 16, + ), + onPressed: () => _navigateToDeviceFunctions(context, device), + ), + ); + } + + void _navigateToDeviceFunctions( + BuildContext context, + DeviceModel device, + ) { + Navigator.of(context).pushNamed( + Routes.deviceFunctionsRoute, + arguments: { + "device": device, + "isAutomationDeviceStatus": isAutomationDeviceStatus, + }, + ); + } +} diff --git a/lib/navigation/router.dart b/lib/navigation/router.dart index c09a307..2ab5b39 100644 --- a/lib/navigation/router.dart +++ b/lib/navigation/router.dart @@ -3,8 +3,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_app/features/app_layout/model/community_model.dart'; import 'package:syncrow_app/features/app_layout/model/space_model.dart'; import 'package:syncrow_app/features/app_layout/view/app_layout.dart'; -import 'package:syncrow_app/features/auth/view/otp_view.dart'; import 'package:syncrow_app/features/auth/view/login_view.dart'; +import 'package:syncrow_app/features/auth/view/otp_view.dart'; import 'package:syncrow_app/features/auth/view/sign_up_view.dart'; import 'package:syncrow_app/features/dashboard/view/dashboard_view.dart'; import 'package:syncrow_app/features/devices/bloc/device_manager_bloc/device_manager_bloc.dart'; @@ -17,11 +17,12 @@ 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_tasks_view.dart'; import 'package:syncrow_app/features/scene/view/scene_view.dart'; import 'package:syncrow_app/features/scene/view/smart_automation_select_route.dart'; import 'package:syncrow_app/features/splash/view/splash_view.dart'; + import 'routing_constants.dart'; class Router { @@ -88,7 +89,7 @@ class Router { BlocProvider( create: (BuildContext context) => TabBarBloc(context.read()) - ..add(TabChanged( + ..add(TabBarTabChangedEvent( selectedIndex: 0, roomId: '-1', unit: SpaceModel(