diff --git a/assets/icons/offToggleSwitchSmall.svg b/assets/icons/offToggleSwitchSmall.svg new file mode 100644 index 0000000..44167b3 --- /dev/null +++ b/assets/icons/offToggleSwitchSmall.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/toggleSwitchSmall.svg b/assets/icons/toggleSwitchSmall.svg new file mode 100644 index 0000000..dc3f4a6 --- /dev/null +++ b/assets/icons/toggleSwitchSmall.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d865a3f..1edaab5 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -289,12 +289,12 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: - device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe + device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342 Firebase: 374a441a91ead896215703a674d58cdb3e9d772b - firebase_analytics: a5c6ef5a435d22870fe3cfdcb424f390f56ff752 - firebase_core: 2337982fb78ee4d8d91e608b0a3d4f44346a93c8 - firebase_crashlytics: 3b6a9a9cbdc5ab92afaf9b206e52c79c2321a0d4 - firebase_database: 8ba35f32ce38e53d81ac61fbe12e0e1b9277fe0f + firebase_analytics: 07bd7cfbac54bfcdccf2bb2530f9a65486f7ef3f + firebase_core: feb37e79f775c2bd08dd35e02d83678291317e10 + firebase_crashlytics: 609a5f6f4a2f5af9e40a68182e0c1be3ca2a02f6 + firebase_database: adc9efd0b70cdc8d1e6f3c9f6bb054a625c4f45d FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7 FirebaseAppCheckInterop: 347aa09a805219a31249b58fc956888e9fcb314b FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa @@ -307,23 +307,23 @@ SPEC CHECKSUMS: FirebaseSessions: 9529d14180868e29a8da164b3a729c036204918b FirebaseSharedSwift: a4e5dfca3e210633bb3a3dfb94176c019211948b Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_secure_storage: 2c2ff13db9e0a5647389bff88b0ecac56e3f3418 + flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be GoogleAppMeasurement: 6a9e6317b6a6d810ad03d4a66564ca6c4c5818a3 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d - image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a + image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 leveldb-library: cc8b8f8e013647a295ad3f8cd2ddf49a6f19be19 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 - onesignal_flutter: d86795eb74c65854b23169f131b4fb1ca3146659 + onesignal_flutter: 5ce68a29861960168e81101cb1bd685d264361de OneSignalXCFramework: bdf74fdc06888f9466dc21e826fe1549ed143095 - path_provider_foundation: 608fcb11be570ce83519b076ab6a1fffe2474f05 - permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 - share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a - shared_preferences_foundation: 0b09b969fb36da5551c0bc4a2dbd9d0ff9387478 - sqflite: c35dad70033b8862124f8337cc994a809fcd9fa3 - url_launcher_ios: 694010445543906933d732453a59da0a173ae33d + share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f + shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe PODFILE CHECKSUM: deba6d843ff3cf709e6e9051ce6601a587b24105 diff --git a/lib/features/app_layout/bloc/home_cubit.dart b/lib/features/app_layout/bloc/home_cubit.dart index dc951ab..adad8c4 100644 --- a/lib/features/app_layout/bloc/home_cubit.dart +++ b/lib/features/app_layout/bloc/home_cubit.dart @@ -7,10 +7,10 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:onesignal_flutter/onesignal_flutter.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:share_plus/share_plus.dart'; +import 'package:syncrow_app/features/app_layout/model/permission_model.dart'; import 'package:syncrow_app/features/app_layout/model/space_model.dart'; import 'package:syncrow_app/features/app_layout/view/widgets/app_bar_home_dropdown.dart'; import 'package:syncrow_app/features/auth/model/user_model.dart'; -import 'package:syncrow_app/features/dashboard/view/dashboard_view.dart'; import 'package:syncrow_app/features/devices/bloc/devices_cubit.dart'; import 'package:syncrow_app/features/devices/model/subspace_model.dart'; import 'package:syncrow_app/features/devices/view/widgets/devices_view_body.dart'; @@ -30,23 +30,30 @@ import 'package:syncrow_app/services/api/profile_api.dart'; import 'package:syncrow_app/services/api/spaces_api.dart'; import 'package:syncrow_app/utils/helpers/snack_bar.dart'; import 'package:syncrow_app/utils/resource_manager/color_manager.dart'; - part 'home_state.dart'; class HomeCubit extends Cubit { HomeCubit._() : super(HomeInitial()) { // checkIfNotificationPermissionGranted(); - fetchUserInfo(); - if (selectedSpace == null) { - fetchUnitsByUserId(); - // .then((value) { - // if (selectedSpace != null) { - // fetchRoomsByUnitId(selectedSpace!); - // } - // }); - } + fetchUserInfo().then( + (value) { + if (selectedSpace == null) { + fetchUnitsByUserId(); + fetchPermissions(); + + // .then((value) { + // if (selectedSpace != null) { + // fetchRoomsByUnitId(selectedSpace!); + // } + // }); + } + }, + ); } static UserModel? user; + + List? permissionModel = []; + static HomeCubit? _instance; static HomeCubit getInstance() { // If an instance already exists, return it @@ -56,15 +63,88 @@ class HomeCubit extends Cubit { Future fetchUserInfo() async { try { + emit(HomeLoading()); var uuid = await const FlutterSecureStorage().read(key: UserModel.userUuidKey); user = await ProfileApi().fetchUserInfo(uuid); - emit(HomeUserInfoLoaded(user!)); // Emit state after fetching user info + emit(HomeUserInfoLoaded(user!)); } catch (e) { return; } } + static bool manageSupSpace = false; + static bool manageScene = false; + static bool manageDeviceLocation = false; + static bool visitorPasswordManagement = false; + String errorMsg = ''; + + Future fetchPermissions() async { + try { + emit(HomeLoading()); + final response = await ProfileApi().fetchPermissions(user!.role!.uuid); + permissionModel = PermissionModel.fromJsonList(response); + hasViewPermission(); + emit(PermissionsRoleLoaded(permissionModel!)); + } catch (e) { + emit(HomeError(e.toString())); + } + } + + void hasViewPermission() { + emit(HomeLoading()); + + manageSupSpace = hasPermission( + permissionModel!, + 'SPACE_MANAGEMENT', + 'MANAGE_SPACE', + 'ASSIGN_USER_TO_SPACE', + ); + manageScene = hasPermission( + permissionModel!, + 'AUTOMATION_MANAGEMENT', + 'MANAGE_SCENES', + 'UPDATE', + ); + manageDeviceLocation = hasPermission( + permissionModel!, + 'DEVICE_MANAGEMENT', + 'MANAGE_DEVICE', + 'LOCATION_UPDATE', + ); + visitorPasswordManagement = hasPermission( + permissionModel!, + 'VISITOR_PASSWORD_MANAGEMENT', + 'MANAGE_VISITOR_PASSWORD', + 'VIEW', + ); + emit(HomePermissionUpdated()); + } + + bool hasPermission(List permissions, String mainTitle, + String subTitle, String finalTitle) { + try { + final mainOption = permissions.firstWhere( + (perm) => perm.title == mainTitle, + ); + + final subOption = mainOption.subOptions.firstWhere( + (sub) => sub.title == subTitle, + ); + + if (subOption.subOptions == null) { + return false; + } + + final finalOption = subOption.subOptions!.firstWhere( + (finalSub) => finalSub.title == finalTitle, + ); + return finalOption.isChecked == true; + } catch (e) { + return false; + } + } + void emitSafe(HomeState newState) { final cubit = this; if (!cubit.isClosed) { @@ -89,7 +169,7 @@ class HomeCubit extends Cubit { static HomeCubit get(context) => BlocProvider.of(context); - List? spaces; + List spaces = []; SpaceModel? selectedSpace; @@ -182,8 +262,10 @@ class HomeCubit extends Cubit { if (index == 0) { unselectRoom(); + } else if (index == 1) { + unselectRoom1(); } else { - selectedRoom = selectedSpace!.subspaces[index - 1]; + selectedRoom = selectedSpace!.subspaces[index - 2]; emitSafe(RoomSelected(selectedRoom!)); } } @@ -197,8 +279,10 @@ class HomeCubit extends Cubit { if (index <= 0) { unselectRoom(); + } else if (index == 1) { + unselectRoom1(); } else { - selectedRoom = selectedSpace!.subspaces[index - 1]; + selectedRoom = selectedSpace!.subspaces[index - 2]; emitSafe(RoomSelected(selectedRoom!)); } } @@ -220,6 +304,23 @@ class HomeCubit extends Cubit { emitSafe(RoomUnSelected()); } + unselectRoom1() { + // selectedRoom = null; + devicesPageController.animateToPage( + 1, + duration: duration, + curve: Curves.linear, + ); + + roomsPageController.animateToPage( + 1, + duration: duration, + curve: Curves.linear, + ); + + emitSafe(RoomUnSelected()); + } + //////////////////////////////////////// API //////////////////////////////////////// generateInvitation(SpaceModel unit) async { try { @@ -261,15 +362,16 @@ class HomeCubit extends Cubit { emitSafe(GetSpacesLoading()); try { spaces = await SpacesAPI.getSpacesByUserId(); + emitSafe(GetSpacesSuccess(spaces)); } catch (failure) { emitSafe(GetSpacesError("No units found")); return; } - if (spaces != null && spaces!.isNotEmpty) { - selectedSpace = spaces!.first; + if (spaces.isNotEmpty) { + selectedSpace = spaces.first; await fetchRoomsByUnitId(selectedSpace!); - emitSafe(GetSpacesSuccess(spaces!)); + emitSafe(GetSpacesSuccess(spaces)); } else { emitSafe(GetSpacesError("No spaces found")); } @@ -298,20 +400,21 @@ class HomeCubit extends Cubit { await const FlutterSecureStorage().read(key: UserModel.userUuidKey); var res = await SpacesAPI.activationCodeSpace( activationCode: activationCode, userUuid: uuid); + if (res['success'] == true) { fetchUserInfo(); fetchUnitsByUserId(); } emitSafe(GetSpacesSuccess(spaces!)); + return res['success']; } on DioException catch (e) { final errorMessage = e.response?.data['error']['message']; - emitSafe(ActivationError(errMessage: errorMessage)); - return false; - } catch (e) { - emitSafe(ActivationError(errMessage: e.toString())); + errorMsg = e.response?.data['error']['message']; + emitSafe(ActivationError(errMessage: errorMsg)); return false; } + } /////////////////////////////////////// Nav /////////////////////////////////////// @@ -378,42 +481,44 @@ class HomeCubit extends Cubit { // ), // onPressed: () {}, // ), - IconButton( - icon: const Icon( - Icons.add, - size: 32, - ), - style: ButtonStyle( - foregroundColor: - WidgetStateProperty.all(ColorsManager.textPrimaryColor), - ), - onPressed: () { - Navigator.pushNamed( - NavigationService.navigatorKey.currentContext!, - Routes.sceneTasksRoute, - arguments: SceneSettingsRouteArguments( - sceneType: '', - sceneId: '', - sceneName: '', - ), - ); - NavigationService.navigatorKey.currentContext! - .read() - .add(const ClearTaskListEvent()); - NavigationService.navigatorKey.currentContext! - .read() - .add(const SceneTypeEvent(CreateSceneEnum.none)); - NavigationService.navigatorKey.currentContext! - .read() - .add(const SmartSceneClearEvent()); - BlocProvider.of( - NavigationService.navigatorKey.currentState!.context) - .add(ResetEffectivePeriod()); - NavigationService.navigatorKey.currentContext! - .read() - .add(const ClearTabToRunSetting()); - }, - ), + manageScene + ? IconButton( + icon: const Icon( + Icons.add, + size: 32, + ), + style: ButtonStyle( + foregroundColor: + WidgetStateProperty.all(ColorsManager.textPrimaryColor), + ), + onPressed: () { + Navigator.pushNamed( + NavigationService.navigatorKey.currentContext!, + Routes.sceneTasksRoute, + arguments: SceneSettingsRouteArguments( + sceneType: '', + sceneId: '', + sceneName: '', + ), + ); + NavigationService.navigatorKey.currentContext! + .read() + .add(const ClearTaskListEvent()); + NavigationService.navigatorKey.currentContext! + .read() + .add(const SceneTypeEvent(CreateSceneEnum.none)); + NavigationService.navigatorKey.currentContext! + .read() + .add(const SmartSceneClearEvent()); + BlocProvider.of( + NavigationService.navigatorKey.currentState!.context) + .add(ResetEffectivePeriod()); + NavigationService.navigatorKey.currentContext! + .read() + .add(const ClearTabToRunSetting()); + }, + ) + : const SizedBox(), // IconButton( // icon: const Icon( // Icons.more_vert, @@ -503,3 +608,28 @@ BottomNavigationBarItem defaultBottomNavBarItem( label: label, ); } + + +// class PermissionUtils { +// // Check if the "VIEW" permission exists in "MANAGE_SUBSPACE" +// static bool hasViewPermission(List permissions) { +// return _hasPermission(permissions, 'MANAGE_SUBSPACE', 'VIEW'); +// } + +// // Generalized permission checker +// static bool _hasPermission( +// List permissions, String mainTitle, String subTitle) { +// final mainOption = permissions.firstWhere( +// (perm) => perm['title'] == mainTitle, +// orElse: () => null, +// ); +// if (mainOption != null) { +// final subOption = mainOption['subOptions'].firstWhere( +// (sub) => sub['title'] == subTitle, +// orElse: () => null, +// ); +// return subOption != null && subOption['isChecked'] == true; +// } +// return false; +// } +// } diff --git a/lib/features/app_layout/bloc/home_state.dart b/lib/features/app_layout/bloc/home_state.dart index 6498b0d..d9814e1 100644 --- a/lib/features/app_layout/bloc/home_state.dart +++ b/lib/features/app_layout/bloc/home_state.dart @@ -15,8 +15,8 @@ class HomeError extends HomeState { class HomeSuccess extends HomeState {} -///specific states -//get spaces +class ActivationSuccess extends HomeState {} + class GetSpacesLoading extends HomeLoading {} class GetSpacesSuccess extends HomeSuccess { @@ -65,9 +65,15 @@ class RoomUnSelected extends HomeState {} class NavChangePage extends HomeState {} -// Define new state classes +class HomePermissionUpdated extends HomeState {} + class HomeUserInfoLoaded extends HomeState { final UserModel user; HomeUserInfoLoaded(this.user); } + +class PermissionsRoleLoaded extends HomeState { + final List permissionModel; + PermissionsRoleLoaded(this.permissionModel); +} diff --git a/lib/features/app_layout/model/permission_model.dart b/lib/features/app_layout/model/permission_model.dart new file mode 100644 index 0000000..3965d27 --- /dev/null +++ b/lib/features/app_layout/model/permission_model.dart @@ -0,0 +1,39 @@ +class PermissionModel { + final String title; + final List subOptions; + + PermissionModel({required this.title, required this.subOptions}); + + factory PermissionModel.fromJson(Map json) { + return PermissionModel( + title: json['title'], + subOptions: (json['subOptions'] as List) + .map((e) => PermissionAttributes.fromJson(e)) + .toList(), + ); + } + + static List fromJsonList(List jsonList) { + return jsonList.map((json) => PermissionModel.fromJson(json)).toList(); + } +} + +class PermissionAttributes { + final String title; + final List? subOptions; + final bool? isChecked; + + PermissionAttributes({required this.title, this.subOptions, this.isChecked}); + + factory PermissionAttributes.fromJson(Map json) { + return PermissionAttributes( + title: json['title'], + isChecked: json['isChecked'], + subOptions: json['subOptions'] != null + ? (json['subOptions'] as List) + .map((e) => PermissionAttributes.fromJson(e)) + .toList() + : null, + ); + } +} diff --git a/lib/features/app_layout/view/app_layout.dart b/lib/features/app_layout/view/app_layout.dart index 214cce2..5dfe583 100644 --- a/lib/features/app_layout/view/app_layout.dart +++ b/lib/features/app_layout/view/app_layout.dart @@ -17,7 +17,10 @@ class AppLayout extends StatelessWidget { child: BlocBuilder( builder: (context, state) { return DefaultScaffold( - appBar: HomeCubit.getInstance().spaces != null ? const DefaultAppBar() : null, + appBar: HomeCubit.getInstance().spaces != null && + HomeCubit.getInstance().spaces!.isNotEmpty + ? const DefaultAppBar() + : null, bottomNavBar: const DefaultNavBar(), child: const AppBody(), ); diff --git a/lib/features/app_layout/view/widgets/default_app_bar.dart b/lib/features/app_layout/view/widgets/default_app_bar.dart index 2d29b96..2284543 100644 --- a/lib/features/app_layout/view/widgets/default_app_bar.dart +++ b/lib/features/app_layout/view/widgets/default_app_bar.dart @@ -35,8 +35,10 @@ class DefaultAppBar extends StatelessWidget implements PreferredSizeWidget { ) : null, ), - actions: HomeCubit.appBarActions[ - HomeCubit.bottomNavItems[HomeCubit.pageIndex].label], + actions: HomeCubit.manageScene + ? HomeCubit.appBarActions[ + HomeCubit.bottomNavItems[HomeCubit.pageIndex].label] + : null, )); }, ); diff --git a/lib/features/auth/bloc/auth_cubit.dart b/lib/features/auth/bloc/auth_cubit.dart index f4418b0..2d37c7d 100644 --- a/lib/features/auth/bloc/auth_cubit.dart +++ b/lib/features/auth/bloc/auth_cubit.dart @@ -221,6 +221,7 @@ class AuthCubit extends Cubit { List userFullName = fullName.split(' '); response = await AuthenticationAPI.signUp( model: SignUpModel( + hasAcceptedAppAgreement: true, email: email.toLowerCase(), password: signUpPassword, firstName: userFullName[0], diff --git a/lib/features/auth/model/signup_model.dart b/lib/features/auth/model/signup_model.dart index c4a0adf..c9b4fa1 100644 --- a/lib/features/auth/model/signup_model.dart +++ b/lib/features/auth/model/signup_model.dart @@ -3,10 +3,12 @@ class SignUpModel { final String password; final String firstName; final String lastName; + final bool hasAcceptedAppAgreement; SignUpModel( {required this.email, required this.password, + required this.hasAcceptedAppAgreement, required this.firstName, required this.lastName}); @@ -15,7 +17,8 @@ class SignUpModel { email: json['email'], password: json['password'], firstName: json['firstName'], - lastName: json['lastName']); + lastName: json['lastName'], + hasAcceptedAppAgreement: true); } Map toJson() { @@ -24,6 +27,7 @@ class SignUpModel { 'password': password, 'firstName': firstName, 'lastName': lastName, + "hasAcceptedAppAgreement": hasAcceptedAppAgreement }; } } diff --git a/lib/features/auth/model/user_model.dart b/lib/features/auth/model/user_model.dart index 5e055a7..143b114 100644 --- a/lib/features/auth/model/user_model.dart +++ b/lib/features/auth/model/user_model.dart @@ -15,6 +15,11 @@ class UserModel { final String? timeZone; final String? regionUuid; final bool? isAgreementAccepted; + final bool? hasAcceptedWebAgreement; + final DateTime? webAgreementAcceptedAt; + final bool? hasAcceptedAppAgreement; + final DateTime? appAgreementAcceptedAt; + final Role? role; UserModel({ required this.uuid, @@ -28,7 +33,11 @@ class UserModel { required this.isAgreementAccepted, required this.regionName, required this.timeZone, - // required this.role, + required this.hasAcceptedWebAgreement, + required this.webAgreementAcceptedAt, + required this.hasAcceptedAppAgreement, + required this.appAgreementAcceptedAt, + required this.role, }); factory UserModel.fromJson(Map json) { @@ -44,6 +53,15 @@ class UserModel { regionName: json['region']?['regionName'], timeZone: json['timeZone']?['timeZoneOffset'], regionUuid: json['region']?['uuid'], + hasAcceptedWebAgreement: json['hasAcceptedWebAgreement'], + webAgreementAcceptedAt: json['webAgreementAcceptedAt'] != null + ? DateTime.parse(json['webAgreementAcceptedAt']) + : null, + hasAcceptedAppAgreement: json['hasAcceptedAppAgreement'], + appAgreementAcceptedAt: json['appAgreementAcceptedAt'] != null + ? DateTime.parse(json['appAgreementAcceptedAt']) + : null, + role: json['role'] != null ? Role.fromJson(json['role']) : null, ); } @@ -61,6 +79,15 @@ class UserModel { regionUuid: null, regionName: tempJson['region']?['regionName'], timeZone: tempJson['timezone']?['timeZoneOffset'], + hasAcceptedWebAgreement: tempJson['hasAcceptedWebAgreement'], + webAgreementAcceptedAt: tempJson['webAgreementAcceptedAt'] != null + ? DateTime.parse(tempJson['webAgreementAcceptedAt']) + : null, + hasAcceptedAppAgreement: tempJson['hasAcceptedAppAgreement'], + appAgreementAcceptedAt: tempJson['appAgreementAcceptedAt'] != null + ? DateTime.parse(tempJson['appAgreementAcceptedAt']) + : null, + role: tempJson['role'] != null ? Role.fromJson(tempJson['role']) : null, ); } @@ -85,6 +112,43 @@ class UserModel { 'isAgreementAccepted': isAgreementAccepted, 'regionName': regionName, 'timeZone': timeZone, + 'hasAcceptedWebAgreement': hasAcceptedWebAgreement, + 'webAgreementAcceptedAt': webAgreementAcceptedAt?.toIso8601String(), + 'hasAcceptedAppAgreement': hasAcceptedAppAgreement, + 'appAgreementAcceptedAt': appAgreementAcceptedAt?.toIso8601String(), + 'role': role?.toJson(), + }; + } +} + +class Role { + final String? uuid; + final DateTime? createdAt; + final DateTime? updatedAt; + final String? type; + + Role({ + required this.uuid, + required this.createdAt, + required this.updatedAt, + required this.type, + }); + + factory Role.fromJson(Map json) { + return Role( + uuid: json['uuid'], + createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt']) : null, + updatedAt: json['updatedAt'] != null ? DateTime.parse(json['updatedAt']) : null, + type: json['type'], + ); + } + + Map toJson() { + return { + 'uuid': uuid, + 'createdAt': createdAt?.toIso8601String(), + 'updatedAt': updatedAt?.toIso8601String(), + 'type': type, }; } } diff --git a/lib/features/devices/bloc/acs_bloc/acs_bloc.dart b/lib/features/devices/bloc/acs_bloc/acs_bloc.dart index 4fd7fe0..b0b43af 100644 --- a/lib/features/devices/bloc/acs_bloc/acs_bloc.dart +++ b/lib/features/devices/bloc/acs_bloc/acs_bloc.dart @@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_app/features/app_layout/bloc/home_cubit.dart'; import 'package:syncrow_app/features/devices/bloc/acs_bloc/acs_event.dart'; import 'package:syncrow_app/features/devices/bloc/acs_bloc/acs_state.dart'; +import 'package:syncrow_app/features/devices/bloc/devices_cubit.dart'; import 'package:syncrow_app/features/devices/model/ac_model.dart'; import 'package:syncrow_app/features/devices/model/device_control_model.dart'; import 'package:syncrow_app/features/devices/model/device_model.dart'; @@ -135,6 +136,8 @@ class ACsBloc extends Bloc { ac.acSwitch = acSwitchValue; } } + + _setAllAcsTempsAndSwitches(); _emitAcsStatus(emit); } else { diff --git a/lib/features/devices/bloc/ceiling_bloc/ceiling_sensor_bloc.dart b/lib/features/devices/bloc/ceiling_bloc/ceiling_sensor_bloc.dart index ca1fadf..d8a463c 100644 --- a/lib/features/devices/bloc/ceiling_bloc/ceiling_sensor_bloc.dart +++ b/lib/features/devices/bloc/ceiling_bloc/ceiling_sensor_bloc.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:dio/dio.dart'; import 'package:firebase_database/firebase_database.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -112,7 +114,7 @@ class CeilingSensorBloc extends Bloc { code: 'presence_state', ); recordGroups = response; - // print('---${recordGroups.data!.first.eventTime}'); + // print('---${jsonEncode(recordGroups.data.first.)}'); emit(FitchData()); } on DioException catch (e) { final errorData = e.response!.data; diff --git a/lib/features/devices/bloc/devices_cubit.dart b/lib/features/devices/bloc/devices_cubit.dart index 9fe8020..6006e1a 100644 --- a/lib/features/devices/bloc/devices_cubit.dart +++ b/lib/features/devices/bloc/devices_cubit.dart @@ -31,6 +31,7 @@ class DevicesCubit extends Cubit { // Fetch groups based on the selected space ID await fetchGroups(selectedSpace.id); + await fetchAllDevices(selectedSpace); } DevicesCubit._() : super(DevicesInitial()) { @@ -47,6 +48,7 @@ class DevicesCubit extends Cubit { return _instance ??= DevicesCubit._(); } + @override Future close() { _instance = null; @@ -109,17 +111,6 @@ class DevicesCubit extends Cubit { } // Getter to retrieve all devices from HomeCubit - List get allDevices { - List devices = []; - if (HomeCubit.getInstance().selectedSpace != null) { - for (var room in HomeCubit.getInstance().selectedSpace!.subspaces) { - if (room.devices != null) { - devices.addAll(room.devices!); - } - } - } - return devices; - } // DevicesCategoryModel? get chosenCategory { // for (var category in allCategories!) { @@ -267,36 +258,36 @@ class DevicesCubit extends Cubit { } ///////////////////////// API CALLS ////////////////////////// - deviceControl(DeviceControlModel control, String deviceId) async { - emitSafe(DeviceControlLoading( - code: control.code, - )); - try { - var response = await DevicesAPI.controlDevice(control, deviceId); + // deviceControl(DeviceControlModel control, String deviceId) async { + // emitSafe(DeviceControlLoading( + // code: control.code, + // )); + // try { + // var response = await DevicesAPI.controlDevice(control, deviceId); - if (response['success'] ?? false) { - emitSafe(DeviceControlSuccess(code: control.code)); - //this delay is to give tuya server time to update the status - // Future.delayed(const Duration(milliseconds: 400), () { - fetchDevicesStatues( - deviceId, - HomeCubit.getInstance() - .selectedSpace! - .subspaces - .indexOf(HomeCubit.getInstance().selectedRoom!), - code: control.code); - // }); - } else { - emitSafe(DeviceControlError('Failed to control the device')); - } - } catch (failure) { - emitSafe(DeviceControlError(failure.toString())); - return; - } - } + // if (response['success'] ?? false) { + // emitSafe(DeviceControlSuccess(code: control.code)); + // //this delay is to give tuya server time to update the status + // // Future.delayed(const Duration(milliseconds: 400), () { + // fetchDevicesStatues( + // deviceId, + // HomeCubit.getInstance() + // .selectedSpace! + // .subspaces + // .indexOf(HomeCubit.getInstance().selectedRoom!), + // code: control.code); + // // }); + // } else { + // emitSafe(DeviceControlError('Failed to control the device')); + // } + // } catch (failure) { + // emitSafe(DeviceControlError(failure.toString())); + // return; + // } + // } fetchGroups(String spaceId) async { - emitSafe(DevicesCategoriesLoading()); + emitSafe(GetDevicesLoading()); try { allCategories = await DevicesAPI.fetchGroups(spaceId); } catch (e) { @@ -321,12 +312,15 @@ class DevicesCubit extends Cubit { try { HomeCubit.getInstance().selectedSpace!.subspaces[roomIndex].devices = await DevicesAPI.getDevicesByRoomId( - communityUuid: unit!.community.uuid, spaceUuid: unit.id, roomId: roomId); + communityUuid: unit!.community.uuid, + spaceUuid: unit.id, + roomId: roomId); } catch (e) { emitSafe(GetDevicesError(e.toString())); return; } - final devices = HomeCubit.getInstance().selectedSpace!.subspaces[roomIndex].devices; + final devices = + HomeCubit.getInstance().selectedSpace!.subspaces[roomIndex].devices; emitSafe(GetDevicesSuccess(devices)); //get status for each device @@ -356,8 +350,11 @@ class DevicesCubit extends Cubit { emitSafe(GetDeviceStatusError(e.toString())); return; } - HomeCubit.getInstance().selectedSpace!.subspaces[roomIndex].devices![deviceIndex].status = - statuses; + HomeCubit.getInstance() + .selectedSpace! + .subspaces[roomIndex] + .devices![deviceIndex] + .status = statuses; emitSafe(GetDeviceStatusSuccess(code: code)); } @@ -406,6 +403,345 @@ class DevicesCubit extends Cubit { // emitSafe(LightBrightnessChanged(value)); // } // } + // List _fetchedDevices = []; + + // List get allDevices => _fetchedDevices; + List allDevices = []; + + Future fetchAllDevices(SpaceModel? unit) async { + emitSafe(GetDevicesLoading()); + try { + final devices = await DevicesAPI.getAllDevices( + communityUuid: unit!.community.uuid, + spaceUuid: unit.id, + ); + allDevices = devices; + emitSafe(GetDevicesSuccess(allDevices)); + } catch (e) { + emitSafe(GetDevicesError(e.toString())); + } + } + + bool isDeviceOn(DeviceModel device) { + final switchStatuses = device.status.where( + (s) => (s.code?.startsWith('switch') ?? false), + ); + final anySwitchFalse = switchStatuses.any((s) => s.value == false); + return !anySwitchFalse; + } + + void updateDeviceStatus(String deviceId, bool newToggleStatus) { + emitSafe(GetDevicesLoading()); + + // Create a fresh copy of the device list. + final updatedDevices = List.from(allDevices); + + for (int i = 0; i < updatedDevices.length; i++) { + final device = updatedDevices[i]; + if (device.uuid == deviceId) { + updatedDevices[i] = DeviceModel( + activeTime: device.activeTime, + localKey: device.localKey, + model: device.model, + name: device.name, + icon: device.icon, + categoryName: device.categoryName, + type: device.type, + isOnline: device.isOnline, + status: device.status, // Make sure to keep the same status list + productName: device.productName, + timeZone: device.timeZone, + updateTime: device.updateTime, + uuid: device.uuid, + productUuid: device.productUuid, + productType: device.productType, + subspace: device.subspace, + toggleStatus: newToggleStatus, + ); + break; + } + } + emit(GetDevicesSuccess(updatedDevices)); + } + + Future threeGangToggle( + DeviceControlModel control, String deviceUuid) async { + emit(SwitchControlLoading(code: control.code)); + try { + final deviceIndex = allDevices.indexWhere((d) => d.uuid == deviceUuid); + if (deviceIndex == -1) { + throw Exception('Device not found'); + } + final device = allDevices[deviceIndex]; + final switches = ['switch_1', 'switch_2', 'switch_3']; + for (final switchCode in switches) { + final statusIndex = + device.status.indexWhere((s) => s.code == switchCode); + if (statusIndex != -1) { + final currentValue = device.status[statusIndex].value ?? false; + final toggledValue = !currentValue; + final controlRequest = DeviceControlModel( + code: switchCode, value: toggledValue, deviceId: deviceUuid); + final response = + await DevicesAPI.controlDevice(controlRequest, deviceUuid); + if (response['success'] != true) { + throw Exception('Failed to toggle $switchCode'); + } + device.status[statusIndex].value = toggledValue; + } + } + final anySwitchOff = device.status.any( + (s) => (s.code?.startsWith('switch_') ?? false) && (s.value == false), + ); + device.toggleStatus = !anySwitchOff; + allDevices[deviceIndex] = device; + emit(DeviceControlSuccess(code: control.code)); + } catch (failure) { + emit(DeviceControlError(failure.toString())); + } + } + + Future towGangToggle( + DeviceControlModel control, String deviceUuid) async { + emit(SwitchControlLoading(code: control.code)); + try { + final deviceIndex = allDevices.indexWhere((d) => d.uuid == deviceUuid); + if (deviceIndex == -1) { + throw Exception('Device not found'); + } + final device = allDevices[deviceIndex]; + final switches = [ + 'switch_1', + 'switch_2', + ]; + for (final switchCode in switches) { + final statusIndex = + device.status.indexWhere((s) => s.code == switchCode); + if (statusIndex != -1) { + final currentValue = device.status[statusIndex].value ?? false; + final toggledValue = !currentValue; + final controlRequest = DeviceControlModel( + code: switchCode, value: toggledValue, deviceId: deviceUuid); + final response = + await DevicesAPI.controlDevice(controlRequest, deviceUuid); + + device.status[statusIndex].value = response['result']; + } + } + final anySwitchOff = device.status.any( + (s) => (s.code?.startsWith('switch_') ?? false) && (s.value == false), + ); + device.toggleStatus = !anySwitchOff; + allDevices[deviceIndex] = device; + emit(DeviceControlSuccess(code: control.code)); + } catch (failure) { + emit(DeviceControlError(failure.toString())); + } + } + + Future towGTGangToggle( + DeviceControlModel control, String deviceUuid) async { + emit(SwitchControlLoading(code: control.code)); + try { + final deviceIndex = allDevices.indexWhere((d) => d.uuid == deviceUuid); + if (deviceIndex == -1) { + throw Exception('Device not found'); + } + final device = allDevices[deviceIndex]; + final switches = [ + 'switch_1', + 'switch_2', + ]; + for (final switchCode in switches) { + final statusIndex = + device.status.indexWhere((s) => s.code == switchCode); + if (statusIndex != -1) { + final currentValue = device.status[statusIndex].value ?? false; + final toggledValue = !currentValue; + final controlRequest = DeviceControlModel( + code: switchCode, value: toggledValue, deviceId: deviceUuid); + final response = + await DevicesAPI.controlDevice(controlRequest, deviceUuid); + + device.status[statusIndex].value = response['result']; + } + } + final anySwitchOff = device.status.any( + (s) => (s.code?.startsWith('switch_') ?? false) && (s.value == false), + ); + device.toggleStatus = !anySwitchOff; + allDevices[deviceIndex] = device; + emit(DeviceControlSuccess(code: control.code)); + } catch (failure) { + emit(DeviceControlError(failure.toString())); + } + } + + Future oneGangToggle( + DeviceControlModel control, String deviceUuid) async { + emit(SwitchControlLoading(code: control.code)); + try { + final deviceIndex = allDevices.indexWhere((d) => d.uuid == deviceUuid); + if (deviceIndex == -1) { + throw Exception('Device not found'); + } + final device = allDevices[deviceIndex]; + + final statusIndex = device.status.indexWhere((s) => s.code == 'switch_1'); + if (statusIndex != -1) { + final currentValue = device.status[statusIndex].value ?? false; + final toggledValue = !currentValue; + final controlRequest = DeviceControlModel( + code: 'switch_1', value: toggledValue, deviceId: deviceUuid); + final response = + await DevicesAPI.controlDevice(controlRequest, deviceUuid); + print(response); + if (response['result'] != true) { + throw Exception('Failed to toggle switch_1'); + } + device.status[statusIndex].value = response['result']; + } + + final anySwitchOff = device.status.any( + (s) => (s.code?.startsWith('switch_') ?? false) && (s.value == false), + ); + device.toggleStatus = !anySwitchOff; + allDevices[deviceIndex] = device; + emit(DeviceControlSuccess(code: control.code)); + } catch (failure) { + emit(DeviceControlError(failure.toString())); + } + } + + Future oneGTGangToggle( + DeviceControlModel control, String deviceUuid) async { + emit(SwitchControlLoading(code: control.code)); + try { + final deviceIndex = allDevices.indexWhere((d) => d.uuid == deviceUuid); + if (deviceIndex == -1) { + throw Exception('Device not found'); + } + final device = allDevices[deviceIndex]; + + final statusIndex = device.status.indexWhere((s) => s.code == 'switch_1'); + if (statusIndex != -1) { + final currentValue = device.status[statusIndex].value ?? false; + final toggledValue = !currentValue; + final controlRequest = DeviceControlModel( + code: 'switch_1', value: toggledValue, deviceId: deviceUuid); + final response = + await DevicesAPI.controlDevice(controlRequest, deviceUuid); + if (response['result'] != true) { + throw Exception('Failed to toggle switch_1'); + } + device.status[statusIndex].value = response['result']; + } + + final anySwitchOff = device.status.any( + (s) => (s.code?.startsWith('switch_') ?? false) && (s.value == false), + ); + device.toggleStatus = !anySwitchOff; + allDevices[deviceIndex] = device; + emit(DeviceControlSuccess(code: control.code)); + } catch (failure) { + emit(DeviceControlError(failure.toString())); + } + } + + Future deviceControl( + DeviceControlModel control, String deviceUuid) async { + emit(SwitchControlLoading(code: control.code)); + try { + final response = await DevicesAPI.controlDevice(control, deviceUuid); + if (response['success'] == true) { + final deviceIndex = allDevices.indexWhere((d) => d.uuid == deviceUuid); + if (deviceIndex != -1) { + final device = allDevices[deviceIndex]; + final statusIndex = + device.status.indexWhere((s) => s.code == control.code); + if (statusIndex != -1) { + device.status[statusIndex].value = control.value; + } + final anySwitchOff = device.status.any( + (s) => + (s.code?.startsWith('switch_') ?? false) && (s.value == false), + ); + device.toggleStatus = !anySwitchOff; + allDevices[deviceIndex] = device; + } + emit(DeviceControlSuccess(code: control.code)); + } else { + emit(DeviceControlError('Failed to control the device')); + } + } catch (failure) { + emit(DeviceControlError(failure.toString())); + } + } + + Future changeCurtainSwitch( + DeviceControlModel control, String deviceUuid) async { + emit(SwitchControlLoading(code: control.code)); + try { + final deviceIndex = allDevices.indexWhere((d) => d.uuid == deviceUuid); + if (deviceIndex == -1) { + throw Exception('Device not found'); + } + final device = allDevices[deviceIndex]; + final isOpen = control.value == "open"; + final newValue = isOpen ? 0 : 100; + final response = await DevicesAPI.deviceBatchController( + code: 'percent_control', + devicesUuid: [deviceUuid], + value: newValue, + ); + if (response['success'] == true) { + final statusIndex = + device.status.indexWhere((s) => s.code == 'percent_control'); + if (statusIndex != -1) { + device.status[statusIndex].value = newValue; + } + allDevices[deviceIndex] = device; + + emit(DeviceControlSuccess(code: control.code)); + } else { + emit(DeviceControlError('Failed to toggle curtain.')); + } + } catch (error) { + emit(DeviceControlError(error.toString())); + } + } + + Future changeGarageSwitch( + DeviceControlModel control, String deviceUuid) async { + emit(SwitchControlLoading(code: control.code)); + try { + final deviceIndex = allDevices.indexWhere((d) => d.uuid == deviceUuid); + if (deviceIndex == -1) { + throw Exception('Device not found'); + } + final device = allDevices[deviceIndex]; + final isOpen = control.value == "open"; + final newValue = isOpen ? 0 : 100; + final response = await DevicesAPI.deviceBatchController( + code: 'percent_control', + devicesUuid: [deviceUuid], + value: newValue, + ); + if (response['success'] == true) { + final statusIndex = + device.status.indexWhere((s) => s.code == 'percent_control'); + if (statusIndex != -1) { + device.status[statusIndex].value = newValue; + } + allDevices[deviceIndex] = device; + emit(DeviceControlSuccess(code: control.code)); + } else { + emit(DeviceControlError('Failed to toggle curtain.')); + } + } catch (error) { + emit(DeviceControlError(error.toString())); + } + } } enum LightMode { diff --git a/lib/features/devices/bloc/devices_state.dart b/lib/features/devices/bloc/devices_state.dart index cb23e24..5e54720 100644 --- a/lib/features/devices/bloc/devices_state.dart +++ b/lib/features/devices/bloc/devices_state.dart @@ -34,6 +34,7 @@ class GetDeviceStatusError extends DevicesState { } class GetDevicesLoading extends DevicesState {} +class RoomLoading extends DevicesState {} class GetDevicesSuccess extends DevicesState { GetDevicesSuccess(this.devices); @@ -55,10 +56,16 @@ class DeviceSwitchChanged extends DevicesState {} class DeviceSelected extends DevicesState {} // Device Control -class DeviceControlLoading extends DevicesState { - final String? code; +// class DeviceControlLoading extends DevicesState { +// final String? code; - DeviceControlLoading({this.code}); +// DeviceControlLoading({this.code}); +// } +class SwitchControlLoading extends DevicesState { + final String? code; + final String? deviceId; + + SwitchControlLoading({this.deviceId, this.code}); } class DeviceControlSuccess extends DevicesState { diff --git a/lib/features/devices/bloc/two_touch_bloc/two_touch_bloc.dart b/lib/features/devices/bloc/two_touch_bloc/two_touch_bloc.dart index 7f66724..56031c8 100644 --- a/lib/features/devices/bloc/two_touch_bloc/two_touch_bloc.dart +++ b/lib/features/devices/bloc/two_touch_bloc/two_touch_bloc.dart @@ -40,7 +40,8 @@ class TwoTouchBloc extends Bloc { bool createSchedule = false; List listSchedule = []; - TwoTouchBloc({required this.twoTouchId, required this.switchCode}) : super(InitialState()) { + TwoTouchBloc({required this.twoTouchId, required this.switchCode}) + : super(InitialState()) { on(_fetchTwoTouchStatus); on(_twoTouchUpdated); on(_changeFirstSwitch); @@ -71,13 +72,15 @@ class TwoTouchBloc extends Bloc { int selectedTabIndex = 0; - void toggleSelectedIndex(ToggleSelectedEvent event, Emitter emit) { + void toggleSelectedIndex( + ToggleSelectedEvent event, Emitter emit) { emit(LoadingInitialState()); selectedTabIndex = event.index; emit(ChangeSlidingSegmentState(value: selectedTabIndex)); } - void toggleCreateSchedule(ToggleCreateScheduleEvent event, Emitter emit) { + void toggleCreateSchedule( + ToggleCreateScheduleEvent event, Emitter emit) { emit(LoadingInitialState()); createSchedule = !createSchedule; selectedDays.clear(); @@ -85,7 +88,8 @@ class TwoTouchBloc extends Bloc { emit(UpdateCreateScheduleState(createSchedule)); } - void _fetchTwoTouchStatus(InitialEvent event, Emitter emit) async { + void _fetchTwoTouchStatus( + InitialEvent event, Emitter emit) async { emit(LoadingInitialState()); try { var response = await DevicesAPI.getDeviceStatus(twoTouchId); @@ -104,18 +108,21 @@ class TwoTouchBloc extends Bloc { _listenToChanges() { try { - DatabaseReference ref = FirebaseDatabase.instance.ref('device-status/$twoTouchId'); + DatabaseReference ref = + FirebaseDatabase.instance.ref('device-status/$twoTouchId'); Stream stream = ref.onValue; stream.listen((DatabaseEvent event) async { if (_timer != null) { await Future.delayed(const Duration(seconds: 2)); } - Map usersMap = event.snapshot.value as Map; + Map usersMap = + event.snapshot.value as Map; List statusList = []; usersMap['status'].forEach((element) { - statusList.add(StatusModel(code: element['code'], value: element['value'])); + statusList + .add(StatusModel(code: element['code'], value: element['value'])); }); deviceStatus = TwoTouchModel.fromJson(statusList); @@ -130,7 +137,8 @@ class TwoTouchBloc extends Bloc { emit(UpdateState(twoTouchModel: deviceStatus)); } - void _changeFirstSwitch(ChangeFirstSwitchStatusEvent event, Emitter emit) async { + void _changeFirstSwitch( + ChangeFirstSwitchStatusEvent event, Emitter emit) async { emit(LoadingNewSate(twoTouchModel: deviceStatus)); try { deviceStatus.firstSwitch = !event.value; @@ -141,7 +149,8 @@ class TwoTouchBloc extends Bloc { _timer = Timer(const Duration(milliseconds: 100), () async { final response = await DevicesAPI.controlDevice( - DeviceControlModel(deviceId: twoTouchId, code: 'switch_1', value: !event.value), + DeviceControlModel( + deviceId: twoTouchId, code: 'switch_1', value: !event.value), twoTouchId); if (!response['success']) { @@ -153,7 +162,8 @@ class TwoTouchBloc extends Bloc { } } - void _changeSecondSwitch(ChangeSecondSwitchStatusEvent event, Emitter emit) async { + void _changeSecondSwitch( + ChangeSecondSwitchStatusEvent event, Emitter emit) async { emit(LoadingNewSate(twoTouchModel: deviceStatus)); try { deviceStatus.secondSwitch = !event.value; @@ -163,7 +173,8 @@ class TwoTouchBloc extends Bloc { } _timer = Timer(const Duration(milliseconds: 100), () async { final response = await DevicesAPI.controlDevice( - DeviceControlModel(deviceId: twoTouchId, code: 'switch_2', value: !event.value), + DeviceControlModel( + deviceId: twoTouchId, code: 'switch_2', value: !event.value), twoTouchId); if (!response['success']) { @@ -186,11 +197,15 @@ class TwoTouchBloc extends Bloc { final response = await Future.wait([ DevicesAPI.controlDevice( DeviceControlModel( - deviceId: twoTouchId, code: 'switch_1', value: deviceStatus.firstSwitch), + deviceId: twoTouchId, + code: 'switch_1', + value: deviceStatus.firstSwitch), twoTouchId), DevicesAPI.controlDevice( DeviceControlModel( - deviceId: twoTouchId, code: 'switch_2', value: deviceStatus.secondSwitch), + deviceId: twoTouchId, + code: 'switch_2', + value: deviceStatus.secondSwitch), twoTouchId), ]); @@ -213,11 +228,15 @@ class TwoTouchBloc extends Bloc { final response = await Future.wait([ DevicesAPI.controlDevice( DeviceControlModel( - deviceId: twoTouchId, code: 'switch_1', value: deviceStatus.firstSwitch), + deviceId: twoTouchId, + code: 'switch_1', + value: deviceStatus.firstSwitch), twoTouchId), DevicesAPI.controlDevice( DeviceControlModel( - deviceId: twoTouchId, code: 'switch_2', value: deviceStatus.secondSwitch), + deviceId: twoTouchId, + code: 'switch_2', + value: deviceStatus.secondSwitch), twoTouchId), ]); if (response.every((element) => !element['success'])) { @@ -237,8 +256,10 @@ class TwoTouchBloc extends Bloc { groupTwoTouchList[i].firstSwitch = true; groupTwoTouchList[i].secondSwitch = true; } - emit(UpdateGroupState(twoTouchList: groupTwoTouchList, allSwitches: true)); - List allDeviceIds = groupTwoTouchList.map((device) => device.deviceId).toList(); + emit( + UpdateGroupState(twoTouchList: groupTwoTouchList, allSwitches: true)); + List allDeviceIds = + groupTwoTouchList.map((device) => device.deviceId).toList(); final response1 = await DevicesAPI.deviceBatchController( code: 'switch_1', @@ -271,9 +292,11 @@ class TwoTouchBloc extends Bloc { groupTwoTouchList[i].secondSwitch = false; } - emit(UpdateGroupState(twoTouchList: groupTwoTouchList, allSwitches: false)); + emit(UpdateGroupState( + twoTouchList: groupTwoTouchList, allSwitches: false)); - List allDeviceIds = groupTwoTouchList.map((device) => device.deviceId).toList(); + List allDeviceIds = + groupTwoTouchList.map((device) => device.deviceId).toList(); final response1 = await DevicesAPI.deviceBatchController( code: 'switch_1', @@ -298,17 +321,20 @@ class TwoTouchBloc extends Bloc { } } - void _changeSliding(ChangeSlidingSegment event, Emitter emit) async { + void _changeSliding( + ChangeSlidingSegment event, Emitter emit) async { emit(ChangeSlidingSegmentState(value: event.value)); } - void _setCounterValue(SetCounterValue event, Emitter emit) async { + void _setCounterValue( + SetCounterValue event, Emitter emit) async { emit(LoadingNewSate(twoTouchModel: deviceStatus)); int seconds = 0; try { seconds = event.duration.inSeconds; final response = await DevicesAPI.controlDevice( - DeviceControlModel(deviceId: twoTouchId, code: event.deviceCode, value: seconds), + DeviceControlModel( + deviceId: twoTouchId, code: event.deviceCode, value: seconds), twoTouchId); if (response['success'] ?? false) { @@ -333,7 +359,8 @@ class TwoTouchBloc extends Bloc { } } - void _getCounterValue(GetCounterEvent event, Emitter emit) async { + void _getCounterValue( + GetCounterEvent event, Emitter emit) async { emit(LoadingInitialState()); try { add(GetScheduleEvent()); @@ -441,7 +468,8 @@ class TwoTouchBloc extends Bloc { deviceId: twoTouchId, ); List jsonData = response; - listSchedule = jsonData.map((item) => ScheduleModel.fromJson(item)).toList(); + listSchedule = + jsonData.map((item) => ScheduleModel.fromJson(item)).toList(); emit(InitialState()); } on DioException catch (e) { final errorData = e.response!.data; @@ -452,12 +480,13 @@ class TwoTouchBloc extends Bloc { int? getTimeStampWithoutSeconds(DateTime? dateTime) { if (dateTime == null) return null; - DateTime dateTimeWithoutSeconds = - DateTime(dateTime.year, dateTime.month, dateTime.day, dateTime.hour, dateTime.minute); + DateTime dateTimeWithoutSeconds = DateTime(dateTime.year, dateTime.month, + dateTime.day, dateTime.hour, dateTime.minute); return dateTimeWithoutSeconds.millisecondsSinceEpoch ~/ 1000; } - Future toggleRepeat(ToggleScheduleEvent event, Emitter emit) async { + Future toggleRepeat( + ToggleScheduleEvent event, Emitter emit) async { try { emit(LoadingInitialState()); final response = await DevicesAPI.changeSchedule( @@ -476,7 +505,8 @@ class TwoTouchBloc extends Bloc { } } - Future deleteSchedule(DeleteScheduleEvent event, Emitter emit) async { + Future deleteSchedule( + DeleteScheduleEvent event, Emitter emit) async { try { emit(LoadingInitialState()); final response = await DevicesAPI.deleteSchedule( @@ -496,7 +526,8 @@ class TwoTouchBloc extends Bloc { } } - void _fetchTwoTouchWizardStatus(InitialWizardEvent event, Emitter emit) async { + void _fetchTwoTouchWizardStatus( + InitialWizardEvent event, Emitter emit) async { emit(LoadingInitialState()); try { devicesList = []; @@ -506,7 +537,8 @@ class TwoTouchBloc extends Bloc { HomeCubit.getInstance().selectedSpace?.id ?? '', '2GT'); for (int i = 0; i < devicesList.length; i++) { - var response = await DevicesAPI.getDeviceStatus(devicesList[i].uuid ?? ''); + var response = + await DevicesAPI.getDeviceStatus(devicesList[i].uuid ?? ''); List statusModelList = []; for (var status in response['status']) { statusModelList.add(StatusModel.fromJson(status)); @@ -529,15 +561,16 @@ class TwoTouchBloc extends Bloc { return true; }); } - emit(UpdateGroupState(twoTouchList: groupTwoTouchList, allSwitches: allSwitchesOn)); + emit(UpdateGroupState( + twoTouchList: groupTwoTouchList, allSwitches: allSwitchesOn)); } catch (e) { emit(FailedState(error: e.toString())); return; } } - void _changeFirstWizardSwitch( - ChangeFirstWizardSwitchStatusEvent event, Emitter emit) async { + void _changeFirstWizardSwitch(ChangeFirstWizardSwitchStatusEvent event, + Emitter emit) async { emit(LoadingNewSate(twoTouchModel: deviceStatus)); try { bool allSwitchesValue = true; @@ -550,7 +583,8 @@ class TwoTouchBloc extends Bloc { } }); - emit(UpdateGroupState(twoTouchList: groupTwoTouchList, allSwitches: allSwitchesValue)); + emit(UpdateGroupState( + twoTouchList: groupTwoTouchList, allSwitches: allSwitchesValue)); final response = await DevicesAPI.deviceBatchController( code: 'switch_1', devicesUuid: [event.deviceId], @@ -565,8 +599,8 @@ class TwoTouchBloc extends Bloc { } } - void _changeSecondWizardSwitch( - ChangeSecondWizardSwitchStatusEvent event, Emitter emit) async { + void _changeSecondWizardSwitch(ChangeSecondWizardSwitchStatusEvent event, + Emitter emit) async { emit(LoadingNewSate(twoTouchModel: deviceStatus)); try { bool allSwitchesValue = true; @@ -579,7 +613,8 @@ class TwoTouchBloc extends Bloc { } }); - emit(UpdateGroupState(twoTouchList: groupTwoTouchList, allSwitches: allSwitchesValue)); + emit(UpdateGroupState( + twoTouchList: groupTwoTouchList, allSwitches: allSwitchesValue)); final response = await DevicesAPI.deviceBatchController( code: 'switch_2', @@ -598,7 +633,8 @@ class TwoTouchBloc extends Bloc { String statusSelected = ''; String optionSelected = ''; - Future _changeStatus(ChangeStatusEvent event, Emitter emit) async { + Future _changeStatus( + ChangeStatusEvent event, Emitter emit) async { try { emit(LoadingInitialState()); final Map> controlMap = { @@ -627,11 +663,15 @@ class TwoTouchBloc extends Bloc { final selectedControl = controlMap[optionSelected]?[statusSelected]; if (selectedControl != null) { await DevicesAPI.controlDevice( - DeviceControlModel(deviceId: twoTouchId, code: optionSelected, value: selectedControl), + DeviceControlModel( + deviceId: twoTouchId, + code: optionSelected, + value: selectedControl), twoTouchId, ); } else { - emit(const FailedState(error: 'Invalid statusSelected or optionSelected')); + emit(const FailedState( + error: 'Invalid statusSelected or optionSelected')); } } on DioException catch (e) { final errorData = e.response!.data; diff --git a/lib/features/devices/model/device_model.dart b/lib/features/devices/model/device_model.dart index 557e83f..9824468 100644 --- a/lib/features/devices/model/device_model.dart +++ b/lib/features/devices/model/device_model.dart @@ -10,6 +10,9 @@ class DeviceModel { String? model; String? name; String? icon; + String? categoryName; + bool? toggleStatus = false; + String? type; bool? isOnline; List status = []; @@ -21,6 +24,8 @@ class DeviceModel { DeviceType? productType; bool isSelected = false; late List functions; + DeviceSubspace? subspace; + DeviceModel( {this.activeTime, this.productUuid, @@ -34,6 +39,9 @@ class DeviceModel { this.updateTime, this.uuid, this.productType, + this.categoryName, + this.subspace, + this.toggleStatus, this.icon, this.type}) { functions = getFunctions(productType!); @@ -87,23 +95,74 @@ class DeviceModel { } else { tempIcon = Assets.assetsIconsLogo; } + // Step 1: Parse `status` as before + final statusList = (json['status'] as List?) + ?.map((st) => StatusModel.fromJson(st as Map)) + .toList(); + +// Step 2: Check whether ANY status means "off" + final anyOff = statusList?.any((s) { + final code = s.code; + if (code == null) return false; + + // 1) Handle "switch" or "switch_x" + if (code == 'switch' || code.startsWith('switch_')) { + // If it's false, we consider that "off" + return s.value == false; + } + + // 2) If code == "control" and value == "stop", consider "off" + if (s.value == 'open') { + return true; + } + + // 3) If code == "percent_control" and value == 0, maybe "off" + if (code == 'percent_control' && s.value == 0) { + return true; + } + + // Add more conditions for other codes as needed + + // Default: if none of the above apply, it's not "off" + return false; + }); + +// Step 3: Decide final toggleStatus (true = fully "on", false = partially/fully "off") + bool computedToggleStatus = !(anyOff ?? false); return DeviceModel( - icon: tempIcon, - activeTime: json['activeTime'], - // id: json['id'], - localKey: json['localKey'], - model: json['model'], - name: json['name'], - isOnline: json['online'], - productName: json['productName'], - timeZone: json['timeZone'], - updateTime: json['updateTime'], - uuid: json['uuid'], - productType: type, - type: json['productType'], - status: [], - productUuid: json['productUuid']); + icon: tempIcon, + activeTime: json['activeTime'], + categoryName: json['categoryName'], + localKey: json['localKey'], + model: json['model'], + name: json['name'], + isOnline: json['online'], + productName: json['productName'], + timeZone: json['timeZone'], + updateTime: json['updateTime'], + uuid: json['uuid'], + productType: type, + type: json['productType'], + + // Use the newly computed toggleStatus: + toggleStatus: computedToggleStatus, + + // Or if you prefer to use the value returned by the backend when available: + // toggleStatus: json['toggleStatus'] ?? computedToggleStatus, + + status: statusList ?? [], + subspace: json['subspace'] != null + ? DeviceSubspace.fromJson(json['subspace']) + : DeviceSubspace( + createdAt: null, + disabled: false, + subspaceName: '', + updatedAt: null, + uuid: '', + ), + productUuid: json['productUuid'], + ); } Map toJson() { @@ -118,9 +177,50 @@ class DeviceModel { 'updateTime': updateTime, 'uuid': uuid, 'productType': productType, + 'subspace': subspace?.toJson(), // serialize subspace }; } List getFunctions(DeviceType type) => devicesFunctionsMap[productType] ?? []; } + +class DeviceSubspace { + String? uuid; + DateTime? createdAt; + DateTime? updatedAt; + String? subspaceName; + bool? disabled; + + DeviceSubspace({ + this.uuid, + this.createdAt, + this.updatedAt, + this.subspaceName, + this.disabled, + }); + + factory DeviceSubspace.fromJson(Map json) { + return DeviceSubspace( + uuid: json['uuid'], + createdAt: json['createdAt'] != null + ? DateTime.tryParse(json['createdAt']) + : null, + updatedAt: json['updatedAt'] != null + ? DateTime.tryParse(json['updatedAt']) + : null, + subspaceName: json['subspaceName'], + disabled: json['disabled'], + ); + } + + Map toJson() { + return { + 'uuid': uuid, + 'createdAt': createdAt?.toIso8601String(), + 'updatedAt': updatedAt?.toIso8601String(), + 'subspaceName': subspaceName, + 'disabled': disabled, + }; + } +} diff --git a/lib/features/devices/view/device_settings/profile_page.dart b/lib/features/devices/view/device_settings/profile_page.dart index 6e1f254..bb1d1df 100644 --- a/lib/features/devices/view/device_settings/profile_page.dart +++ b/lib/features/devices/view/device_settings/profile_page.dart @@ -38,8 +38,10 @@ class SettingProfilePage extends StatelessWidget { final _bloc = BlocProvider.of(context); return state is DeviceSettingLoadingState ? const Center( - child: - DefaultContainer(width: 50, height: 50, child: CircularProgressIndicator()), + child: DefaultContainer( + width: 50, + height: 50, + child: CircularProgressIndicator()), ) : RefreshIndicator( onRefresh: () async { @@ -55,15 +57,18 @@ class SettingProfilePage extends StatelessWidget { child: SvgPicture.asset( Assets.sosHomeIcon, fit: BoxFit.fitHeight, - height: MediaQuery.of(context).size.height * 0.13, + height: + MediaQuery.of(context).size.height * 0.13, )) : CircleAvatar( radius: 55, backgroundColor: ColorsManager.graysColor, child: ClipOval( child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: + CrossAxisAlignment.center, + mainAxisAlignment: + MainAxisAlignment.center, children: [ Center( child: SvgPicture.asset( @@ -71,7 +76,10 @@ class SettingProfilePage extends StatelessWidget { ? Assets.fourSceneIcon : Assets.sixSceneIcon, fit: BoxFit.contain, - height: MediaQuery.of(context).size.height * 0.08, + height: MediaQuery.of(context) + .size + .height * + 0.08, ), ), ], @@ -89,7 +97,8 @@ class SettingProfilePage extends StatelessWidget { children: [ IntrinsicWidth( child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 200), + constraints: + const BoxConstraints(maxWidth: 200), child: TextFormField( maxLength: 30, style: const TextStyle( @@ -122,7 +131,8 @@ class SettingProfilePage extends StatelessWidget { Assets.sosEditProfile, color: Colors.grey, fit: BoxFit.contain, - height: MediaQuery.of(context).size.height * 0.02, + height: MediaQuery.of(context).size.height * + 0.02, ), ), ), @@ -141,15 +151,17 @@ class SettingProfilePage extends StatelessWidget { padding: const EdgeInsets.all(20), child: InkWell( onTap: () async { - bool? val = await Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => LocationSettingPage( - space: spaces!.first, - deviceId: device?.uuid ?? '', - )), - ); - if (val != null && val == true) { - _bloc.add(const DeviceSettingInitialInfo()); + if (HomeCubit.visitorPasswordManagement) { + bool? val = await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => LocationSettingPage( + space: spaces!.first, + deviceId: device?.uuid ?? '', + )), + ); + if (val != null && val == true) { + _bloc.add(const DeviceSettingInitialInfo()); + } } }, child: Row( @@ -162,7 +174,8 @@ class SettingProfilePage extends StatelessWidget { children: [ SizedBox( child: BodyMedium( - text: _bloc.deviceInfo.subspace.subspaceName, + text: _bloc + .deviceInfo.subspace.subspaceName, fontColor: ColorsManager.textGray, ), ), diff --git a/lib/features/devices/view/widgets/ACs/ac_interface.dart b/lib/features/devices/view/widgets/ACs/ac_interface.dart index 218b820..3f54a80 100644 --- a/lib/features/devices/view/widgets/ACs/ac_interface.dart +++ b/lib/features/devices/view/widgets/ACs/ac_interface.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_app/features/app_layout/bloc/home_cubit.dart'; import 'package:syncrow_app/features/devices/bloc/acs_bloc/acs_bloc.dart'; import 'package:syncrow_app/features/devices/bloc/acs_bloc/acs_event.dart'; import 'package:syncrow_app/features/devices/bloc/acs_bloc/acs_state.dart'; +import 'package:syncrow_app/features/devices/bloc/devices_cubit.dart'; import 'package:syncrow_app/features/devices/model/ac_model.dart'; import 'package:syncrow_app/features/devices/model/device_model.dart'; import 'package:syncrow_app/features/devices/view/widgets/ACs/ac_interface_controls.dart'; @@ -71,6 +73,7 @@ class AcInterface extends StatelessWidget { onTap: () { BlocProvider.of(context) .add(AcSwitch(acSwitch: statusModel.acSwitch)); + }, child: SvgPicture.asset(Assets.acSwitchIcon)) ], diff --git a/lib/features/devices/view/widgets/ACs/acs_view.dart b/lib/features/devices/view/widgets/ACs/acs_view.dart index 7680ba7..5bde723 100644 --- a/lib/features/devices/view/widgets/ACs/acs_view.dart +++ b/lib/features/devices/view/widgets/ACs/acs_view.dart @@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_app/features/devices/bloc/acs_bloc/acs_bloc.dart'; import 'package:syncrow_app/features/devices/bloc/acs_bloc/acs_event.dart'; import 'package:syncrow_app/features/devices/bloc/acs_bloc/acs_state.dart'; +import 'package:syncrow_app/features/devices/model/ac_model.dart'; import 'package:syncrow_app/features/devices/model/device_model.dart'; import 'package:syncrow_app/features/devices/view/widgets/ACs/ac_interface.dart'; import 'package:syncrow_app/features/devices/view/widgets/ACs/acs_list.dart'; @@ -40,6 +41,8 @@ class ACsView extends StatelessWidget { extendBody: true, appBar: deviceModel != null ? DeviceAppbar( + //BlocProvider.of(context).deviceStatus.acSwitch.toString() + value: true, deviceName: deviceModel!.name!, deviceUuid: deviceModel!.uuid!, ) diff --git a/lib/features/devices/view/widgets/all_devices.dart b/lib/features/devices/view/widgets/all_devices.dart new file mode 100644 index 0000000..6549d90 --- /dev/null +++ b/lib/features/devices/view/widgets/all_devices.dart @@ -0,0 +1,110 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_app/features/devices/model/device_model.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_app/features/devices/view/widgets/room_page_switch.dart'; +import 'package:syncrow_app/generated/assets.dart'; +import 'package:syncrow_app/utils/resource_manager/color_manager.dart'; + +class AllDevices extends StatefulWidget { + const AllDevices({super.key, required this.allDevices}); + + final List allDevices; + + @override + _AllDevicesState createState() => _AllDevicesState(); +} + +class _AllDevicesState extends State { + final TextEditingController _searchController = TextEditingController(); + List _filteredDevices = []; + + @override + void initState() { + super.initState(); + _filteredDevices = widget.allDevices ?? []; + _searchController.addListener(_filterDevices); + } + + @override + void dispose() { + _searchController.removeListener(_filterDevices); + _searchController.dispose(); + super.dispose(); + } + + void _filterDevices() { + final query = _searchController.text.toLowerCase(); + setState(() { + _filteredDevices = widget.allDevices! + .where((device) => device.name!.toLowerCase().contains(query)) + .toList(); + }); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + if (widget.allDevices.isNotEmpty) + TextFormField( + controller: _searchController, + decoration: InputDecoration( + hintText: 'Search', + hintStyle: const TextStyle( + color: ColorsManager.textGray, + fontSize: 16, + fontWeight: FontWeight.w400), + prefixIcon: Container( + padding: const EdgeInsets.all(5.0), + margin: const EdgeInsets.all(10.0), + child: SvgPicture.asset( + Assets.searchIcon, + fit: BoxFit.contain, + ), + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + ), + _filteredDevices.isNotEmpty + ? Expanded( + child: GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 10, + mainAxisSpacing: 10, + childAspectRatio: 1.5, + ), + padding: const EdgeInsets.only(top: 10), + itemCount: _filteredDevices.length, + itemBuilder: (context, index) { + return RoomPageSwitch( + allDevices: _filteredDevices, + isAllDevices: true, + device: _filteredDevices[index]); + }, + ), + ) + : widget.allDevices.isNotEmpty + ? const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Center( + child: Text( + 'No Results Found', + style: TextStyle( + color: ColorsManager.grayColor, + fontSize: 14, + fontWeight: FontWeight.w400), + )), + ], + ), + ) + : const SizedBox(), + ], + ); + } +} diff --git a/lib/features/devices/view/widgets/ceiling_sensor/ceiling_sensor_interface.dart b/lib/features/devices/view/widgets/ceiling_sensor/ceiling_sensor_interface.dart index 17ba0d0..77101a7 100644 --- a/lib/features/devices/view/widgets/ceiling_sensor/ceiling_sensor_interface.dart +++ b/lib/features/devices/view/widgets/ceiling_sensor/ceiling_sensor_interface.dart @@ -250,7 +250,9 @@ class CeilingSensorInterface extends StatelessWidget { ), ] else if (button['title'] == 'Space Type') ...[ Text( - model.spaceType.name.toString(), + model.spaceType.name.toString() == "none" + ? "Office" + : model.spaceType.name.toString(), style: const TextStyle(color: Colors.black), ), ] else ...[ @@ -299,7 +301,13 @@ class CeilingSensorInterface extends StatelessWidget { ); if (result != null) { bloc.add(ChangeValueEvent( - type: title.toString(), value: result, code: 'nobody_time')); + type: title.toString(), + value: result + .toString() + .toLowerCase() + .replaceAll('sec', 's') + .replaceAll('1hr', '1hour'), // Replacing "sec" with "s" + code: 'nobody_time')); } } else if (title == 'Maximum Distance') { final result = await _showDialog( diff --git a/lib/features/devices/view/widgets/ceiling_sensor/presence_record.dart b/lib/features/devices/view/widgets/ceiling_sensor/presence_record.dart index 9599676..3595cac 100644 --- a/lib/features/devices/view/widgets/ceiling_sensor/presence_record.dart +++ b/lib/features/devices/view/widgets/ceiling_sensor/presence_record.dart @@ -90,9 +90,7 @@ class PresenceRecord extends StatelessWidget { : Colors.grey, ), title: Text( - record.value == 'true' - ? "Opened" - : "Closed", + record.value.toString(), style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 18, diff --git a/lib/features/devices/view/widgets/ceiling_sensor/presence_space_type.dart b/lib/features/devices/view/widgets/ceiling_sensor/presence_space_type.dart index 970f62c..79dbe6e 100644 --- a/lib/features/devices/view/widgets/ceiling_sensor/presence_space_type.dart +++ b/lib/features/devices/view/widgets/ceiling_sensor/presence_space_type.dart @@ -106,12 +106,11 @@ class _PresenceSpaceTypeDialogState extends State { padding: const EdgeInsets.all(10), child: SvgPicture.asset( icon, - ), ), const SizedBox(height: 4), Text( - title, + title == "None" ? "Office" : title, style: Theme.of(context) .textTheme .bodySmall diff --git a/lib/features/devices/view/widgets/curtains/curtain_view.dart b/lib/features/devices/view/widgets/curtains/curtain_view.dart index 6965008..8778f7d 100644 --- a/lib/features/devices/view/widgets/curtains/curtain_view.dart +++ b/lib/features/devices/view/widgets/curtains/curtain_view.dart @@ -6,6 +6,7 @@ import 'package:syncrow_app/features/devices/bloc/curtain_bloc/curtain_event.dar import 'package:syncrow_app/features/devices/bloc/curtain_bloc/curtain_state.dart'; import 'package:syncrow_app/features/devices/model/device_model.dart'; import 'package:syncrow_app/features/devices/view/widgets/curtains/curtain_buttons.dart'; +import 'package:syncrow_app/features/devices/view/widgets/device_appbar.dart'; import 'package:syncrow_app/features/shared_widgets/default_container.dart'; import 'package:syncrow_app/features/shared_widgets/default_scaffold.dart'; import 'package:syncrow_app/generated/assets.dart'; @@ -33,11 +34,17 @@ class CurtainView extends StatelessWidget { // blindHeight = state.blindHeight; } return DefaultScaffold( + appBar: DeviceAppbar( + deviceName: curtain!.name!, + deviceUuid: curtain!.uuid!, + ), title: curtain.name, child: state is CurtainLoadingState ? const Center( - child: - DefaultContainer(width: 50, height: 50, child: CircularProgressIndicator()), + child: DefaultContainer( + width: 50, + height: 50, + child: CircularProgressIndicator()), ) : RefreshIndicator( onRefresh: () async { @@ -62,37 +69,56 @@ class CurtainView extends StatelessWidget { children: [ SvgPicture.asset( Assets.assetsIconsCurtainsIconCurtainHolder, - width: MediaQuery.sizeOf(context).width * 0.75, + width: + MediaQuery.sizeOf(context).width * 0.75, ), SizedBox( - width: MediaQuery.sizeOf(context).width * 0.75, + width: + MediaQuery.sizeOf(context).width * 0.75, child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, children: [ SizedBox( - width: MediaQuery.sizeOf(context).width * 0.025, + width: + MediaQuery.sizeOf(context).width * + 0.025, ), AnimatedContainer( - duration: const Duration(milliseconds: 200), + duration: + const Duration(milliseconds: 200), curve: Curves.linear, - height: MediaQuery.sizeOf(context).height * 0.35, - width: MediaQuery.sizeOf(context).width * 0.35, + height: MediaQuery.sizeOf(context) + .height * + 0.35, + width: + MediaQuery.sizeOf(context).width * + 0.35, child: Stack( children: List.generate( 4, (index) { - double spacing = curtainWidth / 7.5; - double leftMostPosition = index * spacing; + double spacing = + curtainWidth / 7.5; + double leftMostPosition = + index * spacing; return AnimatedPositioned( - duration: const Duration(milliseconds: 200), + duration: const Duration( + milliseconds: 200), curve: Curves.linear, left: leftMostPosition, child: SizedBox( - height: - MediaQuery.sizeOf(context).height * 0.35, - width: MediaQuery.sizeOf(context).width * 0.08, + height: MediaQuery.sizeOf( + context) + .height * + 0.35, + width: MediaQuery.sizeOf( + context) + .width * + 0.08, child: SvgPicture.asset( - Assets.assetsIconsCurtainsIconVerticalBlade, + Assets + .assetsIconsCurtainsIconVerticalBlade, fit: BoxFit.fill, ), ), @@ -102,23 +128,37 @@ class CurtainView extends StatelessWidget { ), ), AnimatedContainer( - duration: const Duration(milliseconds: 200), + duration: + const Duration(milliseconds: 200), curve: Curves.linear, - height: MediaQuery.sizeOf(context).height * 0.35, - width: MediaQuery.sizeOf(context).width * 0.35, + height: MediaQuery.sizeOf(context) + .height * + 0.35, + width: + MediaQuery.sizeOf(context).width * + 0.35, child: Stack( children: List.generate( 4, (index) { - double spacing = curtainWidth / 7.5; - double rightMostPosition = index * spacing; + double spacing = + curtainWidth / 7.5; + double rightMostPosition = + index * spacing; return AnimatedPositioned( - duration: const Duration(milliseconds: 200), + duration: const Duration( + milliseconds: 200), curve: Curves.linear, right: rightMostPosition, child: SizedBox( - height: MediaQuery.sizeOf(context).height * 0.35, - width: MediaQuery.sizeOf(context).width * 0.08, + height: MediaQuery.sizeOf( + context) + .height * + 0.35, + width: MediaQuery.sizeOf( + context) + .width * + 0.08, child: SvgPicture.asset( Assets.rightVerticalBlade, fit: BoxFit.fill, @@ -130,7 +170,9 @@ class CurtainView extends StatelessWidget { ), ), SizedBox( - width: MediaQuery.sizeOf(context).width * 0.025, + width: + MediaQuery.sizeOf(context).width * + 0.025, ), ], ), diff --git a/lib/features/devices/view/widgets/device_appbar.dart b/lib/features/devices/view/widgets/device_appbar.dart index 0e43711..bc64615 100644 --- a/lib/features/devices/view/widgets/device_appbar.dart +++ b/lib/features/devices/view/widgets/device_appbar.dart @@ -1,3 +1,4 @@ +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:syncrow_app/features/shared_widgets/text_widgets/body_large.dart'; import 'package:syncrow_app/utils/resource_manager/color_manager.dart'; @@ -6,14 +7,27 @@ import 'package:syncrow_app/utils/resource_manager/font_manager.dart'; class DeviceAppbar extends StatelessWidget implements PreferredSizeWidget { final String deviceName; final String deviceUuid; + final bool? value; + final double appBarHeight = 56.0; final void Function()? onPressed; const DeviceAppbar( - {super.key, required this.deviceName, required this.deviceUuid, this.onPressed}); + {super.key, + required this.deviceName, + this.value, + required this.deviceUuid, + this.onPressed}); @override Widget build(BuildContext context) { return AppBar( + leading: IconButton( + onPressed: () { + Navigator.of(context).pop(value ?? true); + }, + icon: Icon( + Platform.isIOS ? Icons.arrow_back_ios : Icons.arrow_back, + )), backgroundColor: Colors.transparent, centerTitle: true, title: BodyLarge( diff --git a/lib/features/devices/view/widgets/devices_view_body.dart b/lib/features/devices/view/widgets/devices_view_body.dart index 930f5a5..bca3173 100644 --- a/lib/features/devices/view/widgets/devices_view_body.dart +++ b/lib/features/devices/view/widgets/devices_view_body.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:smooth_page_indicator/smooth_page_indicator.dart'; -import 'package:syncrow_app/features/app_layout/bloc/home_cubit.dart'; import 'package:syncrow_app/features/devices/bloc/devices_cubit.dart'; +import 'package:syncrow_app/features/app_layout/bloc/home_cubit.dart'; +import 'package:syncrow_app/features/devices/view/widgets/all_devices.dart'; import 'package:syncrow_app/features/devices/view/widgets/room_page.dart'; import 'package:syncrow_app/features/devices/view/widgets/rooms_slider.dart'; import 'package:syncrow_app/features/devices/view/widgets/wizard_page.dart'; -import 'package:syncrow_app/features/scene/bloc/scene_bloc/scene_bloc.dart'; import 'package:syncrow_app/features/scene/view/scene_view.dart'; import 'package:syncrow_app/features/shared_widgets/create_unit.dart'; import 'package:syncrow_app/features/shared_widgets/text_widgets/body_large.dart'; @@ -14,103 +14,130 @@ import 'package:syncrow_app/features/shared_widgets/text_widgets/title_medium.da import 'package:syncrow_app/utils/context_extension.dart'; import 'package:syncrow_app/utils/resource_manager/strings_manager.dart'; -class DevicesViewPage extends StatelessWidget { - const DevicesViewPage({super.key}); - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => SceneBloc(), // Initialize your SceneBloc here - child: DevicesViewBody(), - ); - } -} - class DevicesViewBody extends StatelessWidget { - const DevicesViewBody({ - super.key, - }); - + const DevicesViewBody({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - if (state is DevicesLoading || - state is GetDevicesLoading || - state is DevicesCategoriesLoading) { - return const Center(child: CircularProgressIndicator()); - } else { - return HomeCubit.getInstance().spaces?.isEmpty ?? true - ? const CreateUnitWidget() - : Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Row( - children: [ - TitleMedium( - text: StringsManager.devices, - style: context.titleMedium.copyWith( - fontSize: 25, - ), - ), - ], - ), - SizedBox( - height: MediaQuery.of(context).size.height * 0.1, - child: const SceneView( - pageType: true, - )), - const SizedBox( - height: 20, - ), - const RoomsSlider(), - const SizedBox( - height: 10, - ), - Expanded( - child: PageView( - controller: HomeCubit.getInstance().devicesPageController, - onPageChanged: (index) { - HomeCubit.getInstance().devicesPageChanged(index); - }, - children: [ - WizardPage( - groupsList: DevicesCubit.getInstance().allCategories ?? [], - ), - if (HomeCubit.getInstance().selectedSpace != null) - ...HomeCubit.getInstance().selectedSpace!.subspaces.map( - (room) { - return RoomPage( - room: room, - ); - }, - ) - ], - ), - ), - HomeCubit.getInstance().selectedSpace != null - ? Padding( - padding: const EdgeInsets.symmetric( - vertical: 7, - ), - child: SmoothPageIndicator( - controller: HomeCubit.getInstance().devicesPageController, - count: HomeCubit.getInstance().selectedSpace!.subspaces.length + 1, - effect: const WormEffect( - paintStyle: PaintingStyle.stroke, - dotHeight: 8, - dotWidth: 8, - ), - ), - ) - : const Center( - child: BodyLarge(text: 'No Home Found'), - ), - ], - ); + return BlocBuilder( + builder: (context, homeState) { + final homeCubit = HomeCubit.getInstance(); + + // Handle state priority: Errors first + if (homeState is ActivationError) { + return const CreateUnitWidget(); } + + // Handle loading states + if (homeState is GetSpacesLoading || homeState is HomeLoading) { + return const Center(child: CircularProgressIndicator()); + } + + // Handle error states + if (homeState is GetSpacesError) { + return const CreateUnitWidget(); + } + + // Handle success states + if (homeState is GetSpacesSuccess || + homeState is RoomUnSelected || + homeState is RoomSelected || + homeState is NavChangePage) { + // Show empty state if no spaces + if (homeCubit.spaces.isEmpty) { + return const CreateUnitWidget(); + } + + return BlocBuilder( + builder: (context, devicesState) { + // Devices loading states + if (devicesState is DevicesLoading || + devicesState is DevicesCategoriesLoading || + devicesState is GetDevicesLoading) { + return const Center(child: CircularProgressIndicator()); + } + + // Devices error state + if (devicesState is GetDevicesError) { + return Center(child: BodyLarge(text: devicesState.errorMsg)); + } + // Main content for both GetSpacesSuccess and RoomUnSelected + return _buildMainContent(context, homeCubit); + }, + ); + } + + // Fallback for unknown states + return const Center(child: BodyLarge(text: '')); }, ); } + + Widget _buildMainContent(BuildContext context, HomeCubit homeCubit) { + final devicesCubit = context.read(); + + return Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + children: [ + TitleMedium( + text: StringsManager.devices, + style: context.titleMedium.copyWith(fontSize: 25), + ), + ], + ), + SizedBox( + height: MediaQuery.of(context).size.height * 0.1, + child: const SceneView(pageType: true), + ), + const SizedBox(height: 20), + const RoomsSlider(), + const SizedBox(height: 10), + Expanded( + child: PageView( + controller: homeCubit.devicesPageController, + onPageChanged: (index) { + homeCubit.devicesPageChanged(index); + if (index == 0) { + devicesCubit.fetchAllDevices(homeCubit.selectedSpace); + } + }, + children: [ + AllDevices( + allDevices: devicesCubit.allDevices, + ), + WizardPage( + groupsList: devicesCubit.allCategories ?? [], + ), + if (homeCubit.selectedSpace != null) + ...homeCubit.selectedSpace!.subspaces.map( + (room) => RoomPage(room: room), + ), + ], + ), + ), + _buildPageIndicator(homeCubit), + ], + ); + } + + Widget _buildPageIndicator(HomeCubit homeCubit) { + return homeCubit.selectedSpace != null + ? Padding( + padding: const EdgeInsets.symmetric(vertical: 7), + child: SmoothPageIndicator( + controller: homeCubit.devicesPageController, + count: homeCubit.selectedSpace!.subspaces.length + 2, + effect: const WormEffect( + paintStyle: PaintingStyle.stroke, + dotHeight: 8, + dotWidth: 8, + ), + ), + ) + : const Center( + child: BodyLarge(text: 'No Home Found'), + ); + } } diff --git a/lib/features/devices/view/widgets/garage_door/garage_door_screen.dart b/lib/features/devices/view/widgets/garage_door/garage_door_screen.dart index 3d59c4a..0a84a9f 100644 --- a/lib/features/devices/view/widgets/garage_door/garage_door_screen.dart +++ b/lib/features/devices/view/widgets/garage_door/garage_door_screen.dart @@ -5,6 +5,7 @@ import 'package:syncrow_app/features/devices/bloc/garage_door_bloc/garage_door_b import 'package:syncrow_app/features/devices/bloc/garage_door_bloc/garage_door_event.dart'; import 'package:syncrow_app/features/devices/bloc/garage_door_bloc/garage_door_state.dart'; import 'package:syncrow_app/features/devices/model/device_model.dart'; +import 'package:syncrow_app/features/devices/view/widgets/device_appbar.dart'; import 'package:syncrow_app/features/devices/view/widgets/garage_door/garage_preferences_settings.dart'; import 'package:syncrow_app/features/devices/view/widgets/garage_door/garage_records_screen.dart'; import 'package:syncrow_app/features/devices/view/widgets/garage_door/schedule_garage_screen.dart'; @@ -22,6 +23,10 @@ class GarageDoorScreen extends StatelessWidget { @override Widget build(BuildContext context) { return DefaultScaffold( + appBar: DeviceAppbar( + deviceName: device!.name!, + deviceUuid: device!.uuid!, + ), title: 'Garage Door Opener', child: BlocProvider( create: (context) => GarageDoorBloc(GDId: device?.uuid ?? '') diff --git a/lib/features/devices/view/widgets/room_page_switch.dart b/lib/features/devices/view/widgets/room_page_switch.dart index af1bfd5..2893b63 100644 --- a/lib/features/devices/view/widgets/room_page_switch.dart +++ b/lib/features/devices/view/widgets/room_page_switch.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_app/features/app_layout/bloc/home_cubit.dart'; import 'package:syncrow_app/features/devices/bloc/devices_cubit.dart'; import 'package:syncrow_app/features/devices/model/device_model.dart'; import 'package:syncrow_app/features/devices/view/widgets/6_scene_switch/six_scene_screen.dart'; @@ -28,23 +29,31 @@ import 'package:syncrow_app/features/devices/view/widgets/smart_door/door_interf import 'package:syncrow_app/features/devices/view/widgets/three_gang/three_gang_interface.dart'; import 'package:syncrow_app/features/devices/view/widgets/water_heater/water_heater_page.dart'; import 'package:syncrow_app/features/devices/view/widgets/water_leak/water_leak_screen.dart'; +import 'package:syncrow_app/features/shared_widgets/custom_switch.dart'; import 'package:syncrow_app/features/shared_widgets/default_container.dart'; import 'package:syncrow_app/utils/context_extension.dart'; import 'package:syncrow_app/utils/helpers/custom_page_route.dart'; import 'package:syncrow_app/utils/resource_manager/constants.dart'; class RoomPageSwitch extends StatelessWidget { - const RoomPageSwitch({ - super.key, - required this.device, - }); + const RoomPageSwitch( + {super.key, + required this.device, + this.isAllDevices = false, + this.allDevices}); final DeviceModel device; + final List? allDevices; + final bool isAllDevices; @override Widget build(BuildContext context) { return GestureDetector( onTap: () { - showDeviceInterface(device, context); + showDeviceInterface( + device: device, + context: context, + isAllDevices: isAllDevices, + allDevices: allDevices); }, child: DefaultContainer( padding: const EdgeInsets.all(15), @@ -60,22 +69,38 @@ class RoomPageSwitch extends StatelessWidget { device.icon!, fit: BoxFit.contain, ), - // CustomSwitch( - // device: device, - // ), + isAllDevices + ? CustomSwitch( + device: device, + ) + : const SizedBox(), ], ), Flexible( child: FittedBox( - child: Text( - device.name ?? "", - overflow: TextOverflow.ellipsis, - maxLines: 2, - style: context.bodyLarge.copyWith( - fontWeight: FontWeight.bold, - fontSize: 20, - color: Colors.grey, - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + device.name ?? "", + overflow: TextOverflow.ellipsis, + maxLines: 2, + style: context.bodyLarge.copyWith( + fontWeight: FontWeight.bold, + fontSize: 20, + color: Colors.grey, + ), + ), + Text( + device.subspace!.subspaceName ?? '', + overflow: TextOverflow.ellipsis, + style: context.bodySmall.copyWith( + fontWeight: FontWeight.w400, + fontSize: 10, + color: Colors.grey, + ), + ), + ], ), ), ), @@ -89,14 +114,23 @@ class RoomPageSwitch extends StatelessWidget { /// Shows the device interface based on the product type of the device. /// /// The [device] parameter represents the device model. -void showDeviceInterface(DeviceModel device, BuildContext context) { +Future showDeviceInterface( + {required DeviceModel device, + required BuildContext context, + required isAllDevices, + List? allDevices}) async { + final devicesCubit = context.read(); + switch (device.productType) { case DeviceType.AC: - Navigator.push( + var value = await Navigator.push( context, PageRouteBuilder( pageBuilder: (context, animation1, animation2) => ACsView(deviceModel: device))); + if (value && isAllDevices) { + devicesCubit.fetchAllDevices(HomeCubit.getInstance().selectedSpace); + } // navigateToInterface(ACsView(deviceModel: device), context); break; case DeviceType.WallSensor: @@ -116,12 +150,15 @@ void showDeviceInterface(DeviceModel device, BuildContext context) { // navigateToInterface(CeilingSensorInterface(ceilingSensor: device), context); break; case DeviceType.Curtain: - Navigator.push( + var value = await Navigator.push( context, PageRouteBuilder( pageBuilder: (context, animation1, animation2) => CurtainView( curtain: device, ))); + if (value && isAllDevices) { + devicesCubit.fetchAllDevices(HomeCubit.getInstance().selectedSpace); + } break; case DeviceType.Blind: break; @@ -143,30 +180,43 @@ void showDeviceInterface(DeviceModel device, BuildContext context) { case DeviceType.LightBulb: navigateToInterface(LightInterface(light: device), context); case DeviceType.OneGang: - Navigator.push( + var value = await Navigator.push( context, PageRouteBuilder( pageBuilder: (context, animation1, animation2) => OneGangInterface(gangSwitch: device))); + + if (value && isAllDevices) { + devicesCubit.fetchAllDevices(HomeCubit.getInstance().selectedSpace); + } case DeviceType.TwoGang: - Navigator.push( + var value = await Navigator.push( context, PageRouteBuilder( pageBuilder: (context, animation1, animation2) => TwoGangInterface(gangSwitch: device))); + if (value && isAllDevices) { + devicesCubit.fetchAllDevices(HomeCubit.getInstance().selectedSpace); + } case DeviceType.ThreeGang: - Navigator.push( + var value = await Navigator.push( context, PageRouteBuilder( pageBuilder: (context, animation1, animation2) => ThreeGangInterface(gangSwitch: device))); + if (value && isAllDevices) { + devicesCubit.fetchAllDevices(HomeCubit.getInstance().selectedSpace); + } case DeviceType.WH: - Navigator.push( + var value = await Navigator.push( context, PageRouteBuilder( pageBuilder: (context, animation1, animation2) => WaterHeaterPage(device: device))); + if (value && isAllDevices) { + devicesCubit.fetchAllDevices(HomeCubit.getInstance().selectedSpace); + } case DeviceType.DS: Navigator.push( context, @@ -182,31 +232,43 @@ void showDeviceInterface(DeviceModel device, BuildContext context) { PowerClampPage(device: device))); case DeviceType.OneTouch: - Navigator.push( + var value = await Navigator.push( context, PageRouteBuilder( pageBuilder: (context, animation1, animation2) => OneTouchScreen(device: device))); + if (value && isAllDevices) { + devicesCubit.fetchAllDevices(HomeCubit.getInstance().selectedSpace); + } case DeviceType.TowTouch: - Navigator.push( + var value = await Navigator.push( context, PageRouteBuilder( pageBuilder: (context, animation1, animation2) => TwoTouchInterface(touchSwitch: device))); + if (value && isAllDevices) { + devicesCubit.fetchAllDevices(HomeCubit.getInstance().selectedSpace); + } case DeviceType.ThreeTouch: - Navigator.push( + var value = await Navigator.push( context, PageRouteBuilder( pageBuilder: (context, animation1, animation2) => ThreeTouchInterface(touchSwitch: device))); + if (value && isAllDevices) { + devicesCubit.fetchAllDevices(HomeCubit.getInstance().selectedSpace); + } case DeviceType.GarageDoor: - Navigator.push( + var value = await Navigator.push( context, PageRouteBuilder( pageBuilder: (context, animation1, animation2) => GarageDoorScreen(device: device))); + if (value && isAllDevices) { + devicesCubit.fetchAllDevices(HomeCubit.getInstance().selectedSpace); + } case DeviceType.WaterLeak: Navigator.push( @@ -216,18 +278,24 @@ void showDeviceInterface(DeviceModel device, BuildContext context) { WaterLeakScreen(device: device))); case DeviceType.SixScene: - Navigator.push( + var value = await Navigator.push( context, PageRouteBuilder( pageBuilder: (context, animation1, animation2) => SixSceneScreen(device: device))); + if (value && isAllDevices) { + devicesCubit.fetchAllDevices(HomeCubit.getInstance().selectedSpace); + } case DeviceType.FourScene: - Navigator.push( + var value = await Navigator.push( context, PageRouteBuilder( pageBuilder: (context, animation1, animation2) => FourSceneScreen(device: device))); + if (value && isAllDevices) { + devicesCubit.fetchAllDevices(HomeCubit.getInstance().selectedSpace); + } case DeviceType.SOS: Navigator.push( diff --git a/lib/features/devices/view/widgets/rooms_slider.dart b/lib/features/devices/view/widgets/rooms_slider.dart index cfe1f26..06c34d9 100644 --- a/lib/features/devices/view/widgets/rooms_slider.dart +++ b/lib/features/devices/view/widgets/rooms_slider.dart @@ -23,6 +23,21 @@ class RoomsSlider extends StatelessWidget { HomeCubit.getInstance().roomSliderPageChanged(index); }, children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: InkWell( + onTap: () { + HomeCubit.getInstance().unselectRoom(); + }, + child: TitleMedium( + text: 'All Devices', + style: context.titleMedium.copyWith( + fontSize: 25, + color: ColorsManager.textPrimaryColor, + ), + ), + ), + ), Padding( padding: const EdgeInsets.symmetric(horizontal: 15), child: InkWell( @@ -43,7 +58,10 @@ class RoomsSlider extends StatelessWidget { (room) => InkWell( onTap: () { HomeCubit.getInstance().roomSliderPageChanged( - HomeCubit.getInstance().selectedSpace!.subspaces.indexOf(room)); + HomeCubit.getInstance() + .selectedSpace! + .subspaces + .indexOf(room)); }, child: TitleMedium( text: room.name!, @@ -51,7 +69,8 @@ class RoomsSlider extends StatelessWidget { fontSize: 25, color: HomeCubit.getInstance().selectedRoom == room ? ColorsManager.textPrimaryColor - : ColorsManager.textPrimaryColor.withOpacity(.2), + : ColorsManager.textPrimaryColor + .withOpacity(.2), ), ), ), diff --git a/lib/features/devices/view/widgets/smart_door/door_grid.dart b/lib/features/devices/view/widgets/smart_door/door_grid.dart index a37be12..6d49a31 100644 --- a/lib/features/devices/view/widgets/smart_door/door_grid.dart +++ b/lib/features/devices/view/widgets/smart_door/door_grid.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_app/features/app_layout/bloc/home_cubit.dart'; import 'package:syncrow_app/features/devices/view/widgets/smart_door/members_management_view.dart'; import 'package:syncrow_app/features/devices/view/widgets/smart_door/smart_linkage_view.dart'; import 'package:syncrow_app/features/devices/view/widgets/smart_door/temporary_password_page.dart'; @@ -14,6 +15,8 @@ class DoorLockGrid extends StatelessWidget { @override Widget build(BuildContext context) { + final buttons = doorLockButtons(val: uuid); + return GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), @@ -23,14 +26,15 @@ class DoorLockGrid extends StatelessWidget { crossAxisSpacing: 10, childAspectRatio: 1.75 / 1, ), - itemCount: 4, + itemCount: buttons.length, itemBuilder: (context, index) => DefaultContainer( onTap: () { //TODO: remove checking after adding the pages doorLockButtons()[index]['page'] != null ? Navigator.of(context).push( MaterialPageRoute( - builder: (context) => doorLockButtons(val: uuid)[index]['page'] as Widget, + builder: (context) => + doorLockButtons(val: uuid)[index]['page'] as Widget, ), ) : null; @@ -75,11 +79,12 @@ List> doorLockButtons({val}) => [ 'image': Assets.assetsIconsDoorlockAssetsMembersManagement, 'page': const MembersManagementView(), }, - { - 'title': 'Temporary Password', - 'image': Assets.assetsIconsDoorlockAssetsTemporaryPassword, - 'page': TemporaryPasswordPage(deviceId: val), - }, + if (HomeCubit.manageDeviceLocation) + { + 'title': 'Temporary Password', + 'image': Assets.assetsIconsDoorlockAssetsTemporaryPassword, + 'page': TemporaryPasswordPage(deviceId: val), + }, { 'title': 'Smart Linkage', 'image': Assets.assetsIconsDoorlockAssetsSmartLinkage, diff --git a/lib/features/devices/view/widgets/two_touch/two_touch_Interface.dart b/lib/features/devices/view/widgets/two_touch/two_touch_Interface.dart index 649b6b1..e55ffbc 100644 --- a/lib/features/devices/view/widgets/two_touch/two_touch_Interface.dart +++ b/lib/features/devices/view/widgets/two_touch/two_touch_Interface.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_app/features/devices/bloc/two_touch_bloc/two_touch_bloc.dart'; import 'package:syncrow_app/features/devices/model/device_model.dart'; import 'package:syncrow_app/features/devices/view/widgets/device_appbar.dart'; import 'package:syncrow_app/features/devices/view/widgets/two_touch/two_touch_screen.dart'; @@ -26,6 +28,7 @@ class TwoTouchInterface extends StatelessWidget { extendBody: true, appBar: touchSwitch != null ? DeviceAppbar( + value: true, deviceName: touchSwitch!.name!, deviceUuid: touchSwitch!.uuid!, ) diff --git a/lib/features/menu/bloc/menu_cubit.dart b/lib/features/menu/bloc/menu_cubit.dart index 3850734..b868624 100644 --- a/lib/features/menu/bloc/menu_cubit.dart +++ b/lib/features/menu/bloc/menu_cubit.dart @@ -1,5 +1,15 @@ +import 'dart:ui'; + import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_app/features/app_layout/bloc/home_cubit.dart'; +import 'package:syncrow_app/features/menu/bloc/privacy_policy.dart'; +import 'package:syncrow_app/features/menu/bloc/user_agreement.dart'; +import 'package:syncrow_app/features/menu/view/widgets/join_home/join_home_view.dart'; +import 'package:syncrow_app/features/menu/view/widgets/manage_home/manage_home_view.dart'; +import 'package:syncrow_app/features/menu/view/widgets/securty/view/securty_view.dart'; +import 'package:syncrow_app/generated/assets.dart'; import 'package:syncrow_app/services/api/profile_api.dart'; +import 'package:syncrow_app/utils/resource_manager/color_manager.dart'; part 'menu_state.dart'; @@ -32,4 +42,143 @@ class MenuCubit extends Cubit { emit(MenuError(error.toString())); } } + + List> menuSections = [ + //Home Management + { + 'title': 'Home Management', + 'color': ColorsManager.primaryColor, + 'buttons': [ + // { + // 'title': 'Create a Unit', + // 'Icon': Assets.assetsIconsMenuIconsHomeManagementIconsCreateHome, + // 'page': const CreateUnitView() + // }, + { + 'title': 'Join a Unit', + 'Icon': Assets.assetsIconsMenuIconsHomeManagementIconsJoinAHome, + 'page': const JoinHomeView() + }, + if (HomeCubit.manageSupSpace) + { + 'title': 'Manage Your Units', + 'Icon': + Assets.assetsIconsMenuIconsHomeManagementIconsManageYourHome, + 'page': const ManageHomeView() + }, + ], + }, + //General Settings + // { + // 'title': 'General Settings', + // 'color': const Color(0xFF023DFE), + // 'buttons': [ + // { + // 'title': 'Voice Assistant', + // 'Icon': Assets.assetsIconsMenuIconsGeneralSettingsIconsVoiceAssistant, + // 'page': null + // }, + // { + // 'title': 'Temperature unit', + // 'Icon': Assets.assetsIconsMenuIconsGeneralSettingsIconsTemperatureUnit, + // 'page': null + // }, + // { + // 'title': 'Touch tone on panel', + // 'Icon': Assets.assetsIconsMenuIconsGeneralSettingsIconsTouchTone, + // 'page': null + // }, + // { + // 'title': 'Language', + // 'Icon': Assets.assetsIconsMenuIconsGeneralSettingsIconsLanguage, + // 'page': null + // }, + // { + // 'title': 'Network Diagnosis', + // 'Icon': Assets.assetsIconsMenuIconsGeneralSettingsIconsNetworkDiagnosis, + // 'page': null + // }, + // { + // 'title': 'Clear Cache', + // 'Icon': Assets.assetsIconsMenuIconsGeneralSettingsIconsClearCach, + // 'page': null + // }, + // ], + // }, + // //Messages Center + // { + // 'title': 'Messages Center', + // 'color': const Color(0xFF0088FF), + // 'buttons': [ + // { + // 'title': 'Alerts', + // 'Icon': Assets.assetsIconsMenuIconsMessagesCenterIconsAlerts, + // 'page': null + // }, + // { + // 'title': 'Messages', + // 'Icon': Assets.assetsIconsMenuIconsMessagesCenterIconsMessages, + // 'page': null + // }, + // { + // 'title': 'FAQs', + // 'Icon': Assets.assetsIconsMenuIconsMessagesCenterIconsFAQs, + // 'page': null + // }, + // { + // 'title': 'Help & Feedback', + // 'Icon': Assets.assetsIconsMenuIconsMessagesCenterIconsHelpAndFeedback, + // 'page': null + // }, + // ], + // }, + //Security And Privacy + { + 'title': 'Security And Privacy', + 'color': const Color(0xFF8AB9FF), + 'buttons': [ + { + 'title': 'Security', + 'Icon': Assets.assetsIconsMenuIconsSecurityAndPrivacyIconsSecurty, + 'page': const SecurtyView() + }, + // { + // 'title': 'Privacy', + // 'Icon': Assets.assetsIconsMenuIconsSecurityAndPrivacyIconsPrivacy, + // 'page': const PrivacyView() + // }, + ], + }, + //Legal Information + { + 'title': 'Legal Information', + 'color': const Color(0xFF001B72), + 'buttons': [ + { + 'title': 'About', + 'Icon': Assets.assetsIconsMenuIconsLeagalInfoIconsAbout, + 'page': null + }, + { + 'title': 'Privacy Policy', + 'Icon': Assets.assetsIconsMenuIconsLeagalInfoIconsPrivacyPolicy, + 'page': const PrivacyPolicy() + }, + { + 'title': 'User Agreement', + 'Icon': Assets.assetsIconsMenuIconsLeagalInfoIconsUserAgreement, + 'page': const UserAgreement() + }, + ], + }, + ]; + + Future fetchMenuSections() async { + emit(MenuLoading()); + try { + emit(MenuItemsLoaded(menuSections)); + } catch (e) { + emit(MenuError(e.toString())); + } + } } diff --git a/lib/features/menu/bloc/menu_state.dart b/lib/features/menu/bloc/menu_state.dart index 9d6e0ca..f3cdbef 100644 --- a/lib/features/menu/bloc/menu_state.dart +++ b/lib/features/menu/bloc/menu_state.dart @@ -11,6 +11,11 @@ class MenuLoaded extends MenuState { MenuLoaded(this.userAgreementHtml); } + +class MenuItemsLoaded extends MenuState { +final List> menuSections; + MenuItemsLoaded(this.menuSections); +} class MenuError extends MenuState { final String message; diff --git a/lib/features/menu/view/menu_view.dart b/lib/features/menu/view/menu_view.dart index aa1ae86..c5e3d54 100644 --- a/lib/features/menu/view/menu_view.dart +++ b/lib/features/menu/view/menu_view.dart @@ -1,69 +1,62 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:syncrow_app/features/auth/bloc/auth_cubit.dart'; import 'package:syncrow_app/features/menu/bloc/menu_cubit.dart'; import 'package:syncrow_app/features/menu/view/widgets/menu_list.dart'; import 'package:syncrow_app/features/menu/view/widgets/profile/profile_tab.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/utils/context_extension.dart'; -import 'package:syncrow_app/utils/resource_manager/constants.dart'; class MenuView extends StatelessWidget { - const MenuView({super.key}); + const MenuView({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return BlocProvider( - create: (BuildContext context) => MenuCubit(), + create: (BuildContext context) => MenuCubit()..fetchMenuSections(), child: BlocBuilder( - builder: (context, state) { - return BlocBuilder( - builder: (context, state) { - return SingleChildScrollView( - physics: const BouncingScrollPhysics(), - child: Column( - children: [ - const ProfileTab(), - for (var section in menuSections) - MenuList( - section: section, - ), - const SizedBox( - height: 15, - ), - BodyMedium(text: dotenv.env['ENV_NAME'] ?? ''), - const SizedBox( - height: 15, - ), - InkWell( - onTap: () { - AuthCubit.get(context).logout(); - }, - child: Row( - children: [ - Expanded( - child: DefaultContainer( - child: Center( - child: BodyLarge( - text: 'Logout', - style: context.bodyLarge.copyWith( - color: Colors.red, - ), + builder: (context, menuState) { + if (menuState is MenuLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (menuState is MenuError) { + return Center(child: Text(menuState.message)); + } else if (menuState is MenuItemsLoaded) { + final sections = menuState.menuSections; + return SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + children: [ + const ProfileTab(), + for (var section in sections) MenuList(section: section), + const SizedBox(height: 15), + InkWell( + onTap: () { + AuthCubit.get(context).logout(); + }, + child: Row( + children: [ + Expanded( + child: DefaultContainer( + child: Center( + child: BodyLarge( + text: 'Logout', + style: context.bodyLarge.copyWith( + color: Colors.red, ), ), ), ), - ], - ), - ) - ], - ), - ); - }, - ); + ), + ], + ), + ), + ], + ), + ); + } + // Fallback in case no states match + return const SizedBox.shrink(); }, ), ); diff --git a/lib/features/menu/view/widgets/join_home/join_home_view.dart b/lib/features/menu/view/widgets/join_home/join_home_view.dart index 4f3a60e..5ee0d10 100644 --- a/lib/features/menu/view/widgets/join_home/join_home_view.dart +++ b/lib/features/menu/view/widgets/join_home/join_home_view.dart @@ -3,10 +3,12 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:syncrow_app/features/app_layout/bloc/home_cubit.dart'; import 'package:syncrow_app/features/shared_widgets/default_container.dart'; import 'package:syncrow_app/features/shared_widgets/default_scaffold.dart'; +import 'package:syncrow_app/features/shared_widgets/text_widgets/body_large.dart'; import 'package:syncrow_app/features/shared_widgets/text_widgets/body_medium.dart'; import 'package:syncrow_app/generated/assets.dart'; import 'package:syncrow_app/utils/context_extension.dart'; import 'package:syncrow_app/utils/helpers/snack_bar.dart'; +import 'package:syncrow_app/utils/resource_manager/color_manager.dart'; class JoinHomeView extends StatelessWidget { const JoinHomeView({super.key}); @@ -57,14 +59,93 @@ class JoinHomeView extends StatelessWidget { 'Please enter the invitation code'); return; } - if (await HomeCubit.getInstance().activationCode(textEditingController.text)) { + if (await HomeCubit.getInstance() + .activationCode(textEditingController.text)) { await HomeCubit.getInstance().fetchUnitsByUserId(); - CustomSnackBar.displaySnackBar('Done successfully'); - Navigator.of(context).pop(); } else { - CustomSnackBar.displaySnackBar('Wrong code!'); + // CustomSnackBar.displaySnackBar('Wrong code!'); + showDialog( + context: context, + barrierDismissible: false, + builder: (context) { + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: SizedBox( + height: MediaQuery.of(context).size.height * 0.2, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + height: 10, + ), + const BodyLarge( + text: "Warning", + fontWeight: FontWeight.w700, + fontColor: ColorsManager.red, + fontSize: 16, + ), + const Padding( + padding: + EdgeInsets.only(left: 15, right: 15), + child: Divider( + color: ColorsManager.textGray, + ), + ), + Padding( + padding: const EdgeInsets.only( + left: 15, + right: 20, + top: 15, + bottom: 20), + child: Column( + children: [ + Center( + child: Text( + HomeCubit.getInstance().errorMsg, + textAlign: TextAlign.center, + )), + ], + ), + ), + Expanded( + child: Container( + decoration: const BoxDecoration( + border: Border( + top: BorderSide( + color: ColorsManager.textGray, + width: 1.0, + ), + )), + child: InkWell( + onTap: () { + Navigator.of(context).pop(); + }, + child: Padding( + padding: const EdgeInsets.only( + top: 5, bottom: 5), + child: Center( + child: Text( + 'Ok', + style: TextStyle( + color: ColorsManager + .switchButton + .withOpacity(0.6), + fontSize: 14, + fontWeight: + FontWeight.w400), + ), + ), + )), + ), + ) + ], + ), + ), + ); + }, + ); } }, icon: const Icon( diff --git a/lib/features/scene/view/scene_rooms_tabbar.dart b/lib/features/scene/view/scene_rooms_tabbar.dart index 09a508e..9208d06 100644 --- a/lib/features/scene/view/scene_rooms_tabbar.dart +++ b/lib/features/scene/view/scene_rooms_tabbar.dart @@ -16,10 +16,12 @@ class SceneRoomsTabBarDevicesView extends StatefulWidget { const SceneRoomsTabBarDevicesView({super.key}); @override - State createState() => _SceneRoomsTabBarDevicesViewState(); + State createState() => + _SceneRoomsTabBarDevicesViewState(); } -class _SceneRoomsTabBarDevicesViewState extends State +class _SceneRoomsTabBarDevicesViewState + extends State with SingleTickerProviderStateMixin { late final TabController _tabController; List? rooms = []; @@ -35,14 +37,15 @@ class _SceneRoomsTabBarDevicesViewState extends State().allDevices, id: '-1', ), ); } } - _tabController = TabController(length: rooms!.length, vsync: this, initialIndex: 0); + _tabController = + TabController(length: rooms!.length, vsync: this, initialIndex: 0); _tabController.addListener(_handleTabSwitched); super.initState(); } @@ -52,8 +55,10 @@ class _SceneRoomsTabBarDevicesViewState extends State().add( - TabChanged(selectedIndex: value, roomId: rooms?[value].id ?? '', unit: selectedSpace)); + context.read().add(TabChanged( + selectedIndex: value, + roomId: rooms?[value].id ?? '', + unit: selectedSpace)); return; } } diff --git a/lib/features/scene/widgets/scene_view_widget/scene_item.dart b/lib/features/scene/widgets/scene_view_widget/scene_item.dart index ccedaac..38edbbd 100644 --- a/lib/features/scene/widgets/scene_view_widget/scene_item.dart +++ b/lib/features/scene/widgets/scene_view_widget/scene_item.dart @@ -36,34 +36,38 @@ class SceneItem extends StatelessWidget { Widget build(BuildContext context) { return DefaultContainer( onTap: () { - context.read().add(const SmartSceneClearEvent()); - if (disablePlayButton == false) { - BlocProvider.of(context) - .add(FetchSceneTasksEvent(sceneId: scene.id, isAutomation: false)); + if (HomeCubit.manageScene) { + context + .read() + .add(const SmartSceneClearEvent()); + if (disablePlayButton == false) { + BlocProvider.of(context).add( + FetchSceneTasksEvent(sceneId: scene.id, isAutomation: false)); - /// the state to set the scene type must be after the fetch - BlocProvider.of(context) - .add(const SceneTypeEvent(CreateSceneEnum.tabToRun)); - } else { - BlocProvider.of(context) - .add(FetchSceneTasksEvent(sceneId: scene.id, isAutomation: true)); + /// the state to set the scene type must be after the fetch + BlocProvider.of(context) + .add(const SceneTypeEvent(CreateSceneEnum.tabToRun)); + } else { + BlocProvider.of(context).add( + FetchSceneTasksEvent(sceneId: scene.id, isAutomation: true)); - /// the state to set the scene type must be after the fetch - BlocProvider.of(context) - .add(const SceneTypeEvent(CreateSceneEnum.deviceStatusChanges)); + /// the state to set the scene type must be after the fetch + BlocProvider.of(context) + .add(const SceneTypeEvent(CreateSceneEnum.deviceStatusChanges)); + } + + Navigator.pushNamed( + context, + Routes.sceneTasksRoute, + arguments: SceneSettingsRouteArguments( + sceneType: disablePlayButton == false + ? CreateSceneEnum.tabToRun.name + : CreateSceneEnum.deviceStatusChanges.name, + sceneId: scene.id, + sceneName: scene.name, + ), + ); } - - Navigator.pushNamed( - context, - Routes.sceneTasksRoute, - arguments: SceneSettingsRouteArguments( - sceneType: disablePlayButton == false - ? CreateSceneEnum.tabToRun.name - : CreateSceneEnum.deviceStatusChanges.name, - sceneId: scene.id, - sceneName: scene.name, - ), - ); }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -79,8 +83,11 @@ class SceneItem extends StatelessWidget { height: 32, width: 32, fit: BoxFit.fill, - errorBuilder: (context, error, stackTrace) => - Image.asset(Assets.assetsIconsLogo, height: 32, width: 32, fit: BoxFit.fill), + errorBuilder: (context, error, stackTrace) => Image.asset( + Assets.assetsIconsLogo, + height: 32, + width: 32, + fit: BoxFit.fill), ), if (disablePlayButton || scene.iconInBytes.isEmpty) SvgPicture.asset( @@ -93,7 +100,9 @@ class SceneItem extends StatelessWidget { ? IconButton( padding: EdgeInsets.zero, onPressed: () { - context.read().add(SceneTrigger(scene.id, scene.name)); + context + .read() + .add(SceneTrigger(scene.id, scene.name)); }, icon: isLoading ? const Center( @@ -115,11 +124,15 @@ class SceneItem extends StatelessWidget { activeColor: ColorsManager.primaryColor, value: scene.status == 'enable' ? true : false, onChanged: (value) { - context.read().add(UpdateAutomationStatus( - automationStatusUpdate: AutomationStatusUpdate( - isEnable: value, - spaceUuid: HomeCubit.getInstance().selectedSpace!.id), - automationId: scene.id)); + context.read().add( + UpdateAutomationStatus( + automationStatusUpdate: + AutomationStatusUpdate( + isEnable: value, + spaceUuid: HomeCubit.getInstance() + .selectedSpace! + .id), + automationId: scene.id)); }, ), ], diff --git a/lib/features/shared_widgets/create_unit.dart b/lib/features/shared_widgets/create_unit.dart index 05db4dd..3b8a50c 100644 --- a/lib/features/shared_widgets/create_unit.dart +++ b/lib/features/shared_widgets/create_unit.dart @@ -16,7 +16,8 @@ class CreateUnitWidget extends StatelessWidget { TextEditingController textEditingController = TextEditingController(); return BlocConsumer( listener: (context, state) { - if (state is ActivationError) {} + // if (state is ActivationError) { + // } }, builder: (context, state) { return SingleChildScrollView( @@ -96,11 +97,12 @@ class CreateUnitWidget extends StatelessWidget { textEditingController.text)) { CustomSnackBar.displaySnackBar( 'Done successfully'); - Navigator.of(context).pop(); - } else { - CustomSnackBar.displaySnackBar( - 'Wrong code!'); - } + Future.delayed( + const Duration(milliseconds: 500), + () { + Navigator.of(context).pop(); + }); + } }, icon: const Icon( Icons.arrow_right_alt, @@ -136,42 +138,42 @@ class CreateUnitWidget extends StatelessWidget { } } - // return SizedBox( - // width: MediaQuery.sizeOf(context).width, - // height: MediaQuery.sizeOf(context).height, - // child: Column( - // mainAxisAlignment: MainAxisAlignment.center, - // children: [ - // SvgPicture.asset( - // Assets.noUnitsIconDashboard, - // width: 100, - // height: 100, - // ), - // const SizedBox( - // height: 50, - // ), - // Flexible( - // child: GestureDetector( - // onTap: () { - // Navigator.pushNamed(context, Routes.createUnit); - // }, - // child: Container( - // padding: const EdgeInsets.symmetric(horizontal: 34, vertical: 14), - // decoration: ShapeDecoration( - // color: const Color(0x99023DFE), - // shape: RoundedRectangleBorder( - // borderRadius: BorderRadius.circular(20), - // ), - // ), - // child: const TitleMedium( - // text: 'Create a unit', - // style: TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.white), - // ), - // ), - // ), - // ), - // ], - // ), - // ); +// return SizedBox( +// width: MediaQuery.sizeOf(context).width, +// height: MediaQuery.sizeOf(context).height, +// child: Column( +// mainAxisAlignment: MainAxisAlignment.center, +// children: [ +// SvgPicture.asset( +// Assets.noUnitsIconDashboard, +// width: 100, +// height: 100, +// ), +// const SizedBox( +// height: 50, +// ), +// Flexible( +// child: GestureDetector( +// onTap: () { +// Navigator.pushNamed(context, Routes.createUnit); +// }, +// child: Container( +// padding: const EdgeInsets.symmetric(horizontal: 34, vertical: 14), +// decoration: ShapeDecoration( +// color: const Color(0x99023DFE), +// shape: RoundedRectangleBorder( +// borderRadius: BorderRadius.circular(20), +// ), +// ), +// child: const TitleMedium( +// text: 'Create a unit', +// style: TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.white), +// ), +// ), +// ), +// ), +// ], +// ), +// ); // } // } diff --git a/lib/features/shared_widgets/custom_switch.dart b/lib/features/shared_widgets/custom_switch.dart index 85262d0..ab8dfdd 100644 --- a/lib/features/shared_widgets/custom_switch.dart +++ b/lib/features/shared_widgets/custom_switch.dart @@ -1,81 +1,146 @@ 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/devices_cubit.dart'; import 'package:syncrow_app/features/devices/model/device_control_model.dart'; import 'package:syncrow_app/features/devices/model/device_model.dart'; +import 'package:syncrow_app/generated/assets.dart'; import 'package:syncrow_app/utils/resource_manager/color_manager.dart'; -import 'package:collection/collection.dart'; -class CustomSwitch extends StatelessWidget { - const CustomSwitch({super.key, required this.device}); +class CustomSwitch extends StatefulWidget { + const CustomSwitch({Key? key, required this.device}) : super(key: key); final DeviceModel device; + + @override + _CustomSwitchState createState() => _CustomSwitchState(); +} + +bool isCurtainOpen(DeviceModel device) { + // Find the status with code == 'percent_control' + final curtainStatus = device.status.firstWhere( + (s) => s.code == 'percent_control', + // orElse: () => null, + ); + + // We consider it "on/open" if percent_control == 100 + return curtainStatus.value >= 20; +} + +class _CustomSwitchState extends State { + bool _isLoading = false; + + bool isDeviceOn(DeviceModel device) { + // If it's a curtain, check percent_control + if (device.type == "CUR") { + return isCurtainOpen(device); + } + + // Otherwise, default to your existing switch logic + final switchStatuses = device.status.where( + (s) => (s.code?.startsWith('switch') ?? false), + ); + final anySwitchFalse = switchStatuses.any((s) => s.value == false); + return !anySwitchFalse; + } + + Widget _buildLoadingIndicator() { + return const SizedBox( + width: 45, + height: 28, + child: Center( + child: SizedBox( + width: 22, + height: 22, + child: CircularProgressIndicator( + strokeWidth: 2, + color: ColorsManager.primaryColor, + ), + ), + ), + ); + } + @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - bool? status; - if (device.status.isNotEmpty) { - status = device.status - .firstWhereOrNull((status) => status.code == "switch") - ?.value; + final isSwitch = widget.device.categoryName == 'Switch'; + if (!isSwitch && + widget.device.type != "AC" && + widget.device.type != "GD" && + widget.device.type != "CUR") { + return const SizedBox(); + } + final isOn = isDeviceOn(widget.device); + + return GestureDetector( + onTap: () async { + setState(() { + _isLoading = true; + }); + + final newValue = !isOn; + String code; + dynamic controlValue; + + if (widget.device.type == "AC") { + code = "switch"; + controlValue = newValue; + } else if (widget.device.type == "CUR") { + code = "percent_control"; + controlValue = newValue ? "close" : "open"; + } else { + code = "switch_1"; + controlValue = newValue; + } + + final control = DeviceControlModel( + code: code, + value: controlValue, + deviceId: widget.device.uuid!, + ); + try { + if (widget.device.type == "CUR") { + await context + .read() + .changeCurtainSwitch(control, widget.device.uuid!); + } else if (widget.device.type == "1GT") { + await context + .read() + .oneGTGangToggle(control, widget.device.uuid!); + } else if (widget.device.type == "2GT") { + await context + .read() + .towGTGangToggle(control, widget.device.uuid!); + } else if (widget.device.type == "3G") { + await context + .read() + .threeGangToggle(control, widget.device.uuid!); + } else if (widget.device.type == "2G") { + await context + .read() + .towGangToggle(control, widget.device.uuid!); + } else if (widget.device.type == "1G") { + await context + .read() + .oneGangToggle(control, widget.device.uuid!); + } else { + await context + .read() + .deviceControl(control, widget.device.uuid!); + } + } finally { + setState(() { + _isLoading = false; + }); } - return status == null - ? const SizedBox() - : GestureDetector( - onTap: () { - DevicesCubit.getInstance().deviceControl( - DeviceControlModel( - deviceId: device.uuid, - code: device.status - .firstWhere((status) => status.code == "switch") - .code, - value: !device.status - .firstWhere((status) => status.code == "switch") - .value!, - ), - device.uuid!, - ); - }, - child: Container( - width: 45.0, - height: 28.0, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24.0), - color: status - ? ColorsManager.primaryColor - : const Color(0xFFD9D9D9)), - child: Center( - child: Container( - width: 40.0, - height: 23.0, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24.0), - color: Colors.white, - ), - child: Padding( - padding: const EdgeInsets.all(2.0), - child: Container( - alignment: status - ? Alignment.centerRight - : Alignment.centerLeft, - child: Container( - width: 20.0, - height: 20.0, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: status - ? ColorsManager.primaryColor - : Colors.grey, - ), - ), - ), - ), - ), - ), - ), - ); }, + child: _isLoading + ? _buildLoadingIndicator() + : SizedBox( + child: SvgPicture.asset( + isOn ? Assets.toggleSwitchSmall : Assets.offToggleSwitchSmall, + ), + ), ); } } diff --git a/lib/generated/assets.dart b/lib/generated/assets.dart index 9e51f50..8b21b17 100644 --- a/lib/generated/assets.dart +++ b/lib/generated/assets.dart @@ -1139,4 +1139,8 @@ class Assets { static const String office = 'assets/icons/office.svg'; static const String parlour = 'assets/icons/parlour.svg'; static const String grid = 'assets/images/grid.svg'; + + static const String toggleSwitchSmall = 'assets/icons/toggleSwitchSmall.svg'; + static const String offToggleSwitchSmall = 'assets/icons/offToggleSwitchSmall.svg'; + } diff --git a/lib/my_app.dart b/lib/my_app.dart index a750c79..9457926 100644 --- a/lib/my_app.dart +++ b/lib/my_app.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_app/features/auth/bloc/auth_cubit.dart'; +import 'package:syncrow_app/features/devices/bloc/devices_cubit.dart'; import 'package:syncrow_app/features/menu/bloc/menu_cubit.dart'; import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_bloc.dart'; import 'package:syncrow_app/features/scene/bloc/create_scene/create_scene_bloc.dart'; @@ -27,6 +28,9 @@ class MyApp extends StatelessWidget { Constants.bottomNavBarHeightPercentage; return MultiBlocProvider( providers: [ + BlocProvider( + create: (context) => DevicesCubit.getInstance(), + ), BlocProvider(create: (context) => AuthCubit()), BlocProvider( create: (context) => EffectPeriodBloc(), @@ -35,7 +39,7 @@ class MyApp extends StatelessWidget { BlocProvider(create: (context) => CreateSceneBloc()), BlocProvider(create: (context) => SceneBloc()), BlocProvider(create: (context) => ProfileBloc()), - BlocProvider(create: (context) => MenuCubit()), + BlocProvider(create: (context) => MenuCubit()), //DevicesCubit ], child: MaterialApp( navigatorKey: NavigationService.navigatorKey, diff --git a/lib/services/api/api_links_endpoints.dart b/lib/services/api/api_links_endpoints.dart index b72cc20..65dcc18 100644 --- a/lib/services/api/api_links_endpoints.dart +++ b/lib/services/api/api_links_endpoints.dart @@ -225,4 +225,7 @@ abstract class ApiEndpoints { static const String getDeviceLogs = '/device/report-logs/{uuid}?code={code}'; static const String terms = '/terms'; static const String policy = '/policy'; + static const String getPermission = '/permission/{roleUuid}'; + static const String getAllDevices = + '/projects/{projectUuid}/communities/{communityUuid}/spaces/{spaceUuid}/devices'; } diff --git a/lib/services/api/devices_api.dart b/lib/services/api/devices_api.dart index 2110a37..a6ff88b 100644 --- a/lib/services/api/devices_api.dart +++ b/lib/services/api/devices_api.dart @@ -51,11 +51,14 @@ class DevicesAPI { static Future> controlDevice( DeviceControlModel controlModel, String deviceId) async { try { + print('object-*/-*/-*/${controlModel.toJson()}'); final response = await _httpService.post( path: ApiEndpoints.controlDevice.replaceAll('{deviceUuid}', deviceId), body: controlModel.toJson(), showServerMessage: true, expectedResponseModel: (json) { + print('object-*/-*/-*/${json}'); + return json; }, ); @@ -88,7 +91,8 @@ class DevicesAPI { static Future> getDeviceStatus(String deviceId) async { final response = await _httpService.get( - path: ApiEndpoints.deviceFunctionsStatus.replaceAll('{deviceUuid}', deviceId), + path: ApiEndpoints.deviceFunctionsStatus + .replaceAll('{deviceUuid}', deviceId), showServerMessage: false, expectedResponseModel: (json) { return json; @@ -97,7 +101,8 @@ class DevicesAPI { return response; } - static Future> getPowerClampStatus(String deviceId) async { + static Future> getPowerClampStatus( + String deviceId) async { final response = await _httpService.get( path: ApiEndpoints.powerClamp.replaceAll('{powerClampUuid}', deviceId), showServerMessage: false, @@ -109,7 +114,9 @@ class DevicesAPI { } static Future> renamePass( - {required String name, required String doorLockUuid, required String passwordId}) async { + {required String name, + required String doorLockUuid, + required String passwordId}) async { final response = await _httpService.put( path: ApiEndpoints.renamePassword .replaceAll('{doorLockUuid}', doorLockUuid) @@ -144,7 +151,8 @@ class DevicesAPI { return response; } - static Future getSceneBySwitchName({String? deviceId, String? switchName}) async { + static Future getSceneBySwitchName( + {String? deviceId, String? switchName}) async { final response = await _httpService.get( path: ApiEndpoints.fourSceneByName .replaceAll('{deviceUuid}', deviceId!) @@ -165,7 +173,11 @@ class DevicesAPI { final response = await _httpService.post( path: ApiEndpoints.deviceScene.replaceAll('{deviceUuid}', deviceId!), body: jsonEncode( - {"switchName": switchName, "sceneUuid": sceneUuid, "spaceUuid": spaceUuid}, + { + "switchName": switchName, + "sceneUuid": sceneUuid, + "spaceUuid": spaceUuid + }, ), showServerMessage: false, expectedResponseModel: (json) { @@ -185,7 +197,8 @@ class DevicesAPI { return response; } - static Future> getDeviceByGroupName(String unitId, String groupName) async { + static Future> getDeviceByGroupName( + String unitId, String groupName) async { final response = await _httpService.get( path: ApiEndpoints.devicesByGroupName .replaceAll('{unitUuid}', unitId) @@ -230,7 +243,9 @@ class DevicesAPI { if (json == null || json.isEmpty || json == []) { return []; } - return data.map((device) => DeviceModel.fromJson(device)).toList(); + return data + .map((device) => DeviceModel.fromJson(device)) + .toList(); }, ); @@ -242,7 +257,8 @@ class DevicesAPI { } } - static Future> getDevicesByGatewayId(String gatewayId) async { + static Future> getDevicesByGatewayId( + String gatewayId) async { final response = await _httpService.get( path: ApiEndpoints.gatewayApi.replaceAll('{gatewayUuid}', gatewayId), showServerMessage: false, @@ -264,7 +280,8 @@ class DevicesAPI { String deviceId, ) async { final response = await _httpService.get( - path: ApiEndpoints.getTemporaryPassword.replaceAll('{doorLockUuid}', deviceId), + path: ApiEndpoints.getTemporaryPassword + .replaceAll('{doorLockUuid}', deviceId), showServerMessage: false, expectedResponseModel: (json) { return json; @@ -275,7 +292,8 @@ class DevicesAPI { static Future getOneTimePasswords(String deviceId) async { final response = await _httpService.get( - path: ApiEndpoints.getOneTimeTemporaryPassword.replaceAll('{doorLockUuid}', deviceId), + path: ApiEndpoints.getOneTimeTemporaryPassword + .replaceAll('{doorLockUuid}', deviceId), showServerMessage: false, expectedResponseModel: (json) { return json; @@ -286,7 +304,8 @@ class DevicesAPI { static Future getTimeLimitPasswords(String deviceId) async { final response = await _httpService.get( - path: ApiEndpoints.getMultipleTimeTemporaryPassword.replaceAll('{doorLockUuid}', deviceId), + path: ApiEndpoints.getMultipleTimeTemporaryPassword + .replaceAll('{doorLockUuid}', deviceId), showServerMessage: false, expectedResponseModel: (json) { return json; @@ -311,10 +330,12 @@ class DevicesAPI { "invalidTime": invalidTime, }; if (scheduleList != null) { - body["scheduleList"] = scheduleList.map((schedule) => schedule.toJson()).toList(); + body["scheduleList"] = + scheduleList.map((schedule) => schedule.toJson()).toList(); } final response = await _httpService.post( - path: ApiEndpoints.addTemporaryPassword.replaceAll('{doorLockUuid}', deviceId), + path: ApiEndpoints.addTemporaryPassword + .replaceAll('{doorLockUuid}', deviceId), body: body, showServerMessage: false, expectedResponseModel: (json) => json, @@ -325,7 +346,8 @@ class DevicesAPI { static Future generateOneTimePassword({deviceId}) async { try { final response = await _httpService.post( - path: ApiEndpoints.addOneTimeTemporaryPassword.replaceAll('{doorLockUuid}', deviceId), + path: ApiEndpoints.addOneTimeTemporaryPassword + .replaceAll('{doorLockUuid}', deviceId), showServerMessage: false, expectedResponseModel: (json) { return json; @@ -337,10 +359,12 @@ class DevicesAPI { } } - static Future generateMultiTimePassword({deviceId, effectiveTime, invalidTime}) async { + static Future generateMultiTimePassword( + {deviceId, effectiveTime, invalidTime}) async { try { final response = await _httpService.post( - path: ApiEndpoints.addMultipleTimeTemporaryPassword.replaceAll('{doorLockUuid}', deviceId), + path: ApiEndpoints.addMultipleTimeTemporaryPassword + .replaceAll('{doorLockUuid}', deviceId), showServerMessage: true, body: {"effectiveTime": effectiveTime, "invalidTime": invalidTime}, expectedResponseModel: (json) { @@ -530,7 +554,9 @@ class DevicesAPI { String code, ) async { final response = await HTTPService().get( - path: ApiEndpoints.getDeviceLogs.replaceAll('{uuid}', uuid).replaceAll('{code}', code), + path: ApiEndpoints.getDeviceLogs + .replaceAll('{uuid}', uuid) + .replaceAll('{code}', code), showServerMessage: false, expectedResponseModel: (json) { return DeviceReport.fromJson(json); @@ -538,4 +564,45 @@ class DevicesAPI { ); return response; } + + static Future> getAllDevices({ + required String communityUuid, + required String spaceUuid, + }) async { + print('communityUuid=$communityUuid'); + print('spaceUuid=$spaceUuid'); + print('projectUuid=${TempConst.projectId}'); + + try { + final String path = ApiEndpoints.getAllDevices + .replaceAll('{communityUuid}', communityUuid) + .replaceAll('{spaceUuid}', spaceUuid) + .replaceAll('{projectUuid}', TempConst.projectId); + + final response = await _httpService.get( + path: path, + showServerMessage: false, + expectedResponseModel: (json) { + print('response-*/-*/$json'); + final data = json['data']; + + if (data == null || data.isEmpty) { + return []; + } + if (json == null || json.isEmpty || json == []) { + return []; + } + return data + .map((device) => DeviceModel.fromJson(device)) + .toList(); + }, + ); + + return response; + } catch (e) { + // Log the error if needed + // Return an empty list in case of error + return []; + } + } } diff --git a/lib/services/api/profile_api.dart b/lib/services/api/profile_api.dart index 0c86fef..dcaf168 100644 --- a/lib/services/api/profile_api.dart +++ b/lib/services/api/profile_api.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:developer'; import 'package:syncrow_app/features/app_layout/bloc/home_cubit.dart'; import 'package:syncrow_app/features/auth/model/user_model.dart'; import 'package:syncrow_app/features/menu/model/region_model.dart'; @@ -95,6 +94,16 @@ class ProfileApi { return response; } + Future fetchPermissions(roleId) async { + final response = await _httpService.get( + path: ApiEndpoints.getPermission.replaceAll('{roleUuid}', roleId!), + showServerMessage: true, + expectedResponseModel: (json) { + return json; + }); + return response; + } + static Future> fetchRegion() async { final response = await _httpService.get( path: ApiEndpoints.getRegion, @@ -117,7 +126,7 @@ class ProfileApi { return response as List; } - Future fetchUserAgreement() async { + Future fetchUserAgreement() async { final response = await _httpService.get( path: ApiEndpoints.terms, showServerMessage: true, @@ -127,7 +136,7 @@ class ProfileApi { return response; } - Future fetchPrivacyPolicy() async { + Future fetchPrivacyPolicy() async { final response = await _httpService.get( path: ApiEndpoints.policy, showServerMessage: true, diff --git a/lib/utils/resource_manager/constants.dart b/lib/utils/resource_manager/constants.dart index 4d099c3..01dde3c 100644 --- a/lib/utils/resource_manager/constants.dart +++ b/lib/utils/resource_manager/constants.dart @@ -1,5 +1,6 @@ //ignore_for_file: constant_identifier_names import 'dart:ui'; +import 'package:syncrow_app/features/app_layout/bloc/home_cubit.dart'; import 'package:syncrow_app/features/devices/model/function_model.dart'; import 'package:syncrow_app/features/menu/bloc/privacy_policy.dart'; import 'package:syncrow_app/features/menu/bloc/user_agreement.dart'; @@ -677,134 +678,6 @@ K? getNextItem(Map map, V value) { return null; } -List> menuSections = [ - //Home Management - { - 'title': 'Home Management', - 'color': ColorsManager.primaryColor, - 'buttons': [ - // { - // 'title': 'Create a Unit', - // 'Icon': Assets.assetsIconsMenuIconsHomeManagementIconsCreateHome, - // 'page': const CreateUnitView() - // }, - { - 'title': 'Join a Unit', - 'Icon': Assets.assetsIconsMenuIconsHomeManagementIconsJoinAHome, - 'page': const JoinHomeView() - }, - { - 'title': 'Manage Your Units', - 'Icon': Assets.assetsIconsMenuIconsHomeManagementIconsManageYourHome, - 'page': const ManageHomeView() - }, - ], - }, - //General Settings - // { - // 'title': 'General Settings', - // 'color': const Color(0xFF023DFE), - // 'buttons': [ - // { - // 'title': 'Voice Assistant', - // 'Icon': Assets.assetsIconsMenuIconsGeneralSettingsIconsVoiceAssistant, - // 'page': null - // }, - // { - // 'title': 'Temperature unit', - // 'Icon': Assets.assetsIconsMenuIconsGeneralSettingsIconsTemperatureUnit, - // 'page': null - // }, - // { - // 'title': 'Touch tone on panel', - // 'Icon': Assets.assetsIconsMenuIconsGeneralSettingsIconsTouchTone, - // 'page': null - // }, - // { - // 'title': 'Language', - // 'Icon': Assets.assetsIconsMenuIconsGeneralSettingsIconsLanguage, - // 'page': null - // }, - // { - // 'title': 'Network Diagnosis', - // 'Icon': Assets.assetsIconsMenuIconsGeneralSettingsIconsNetworkDiagnosis, - // 'page': null - // }, - // { - // 'title': 'Clear Cache', - // 'Icon': Assets.assetsIconsMenuIconsGeneralSettingsIconsClearCach, - // 'page': null - // }, - // ], - // }, - // //Messages Center - // { - // 'title': 'Messages Center', - // 'color': const Color(0xFF0088FF), - // 'buttons': [ - // { - // 'title': 'Alerts', - // 'Icon': Assets.assetsIconsMenuIconsMessagesCenterIconsAlerts, - // 'page': null - // }, - // { - // 'title': 'Messages', - // 'Icon': Assets.assetsIconsMenuIconsMessagesCenterIconsMessages, - // 'page': null - // }, - // { - // 'title': 'FAQs', - // 'Icon': Assets.assetsIconsMenuIconsMessagesCenterIconsFAQs, - // 'page': null - // }, - // { - // 'title': 'Help & Feedback', - // 'Icon': Assets.assetsIconsMenuIconsMessagesCenterIconsHelpAndFeedback, - // 'page': null - // }, - // ], - // }, - //Security And Privacy - { - 'title': 'Security And Privacy', - 'color': const Color(0xFF8AB9FF), - 'buttons': [ - { - 'title': 'Security', - 'Icon': Assets.assetsIconsMenuIconsSecurityAndPrivacyIconsSecurty, - 'page': const SecurtyView() - }, - // { - // 'title': 'Privacy', - // 'Icon': Assets.assetsIconsMenuIconsSecurityAndPrivacyIconsPrivacy, - // 'page': const PrivacyView() - // }, - ], - }, - //Legal Information - { - 'title': 'Legal Information', - 'color': const Color(0xFF001B72), - 'buttons': [ - { - 'title': 'About', - 'Icon': Assets.assetsIconsMenuIconsLeagalInfoIconsAbout, - 'page': null - }, - { - 'title': 'Privacy Policy', - 'Icon': Assets.assetsIconsMenuIconsLeagalInfoIconsPrivacyPolicy, - 'page': const PrivacyPolicy() - }, - { - 'title': 'User Agreement', - 'Icon': Assets.assetsIconsMenuIconsLeagalInfoIconsUserAgreement, - 'page': const UserAgreement() - }, - ], - }, -]; - enum MemberRole { FamilyMember, OtherMember, diff --git a/pubspec.yaml b/pubspec.yaml index fbcd8b3..221c843 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ description: This is the mobile application project, developed with Flutter for # pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: "none" # Remove this line if you wish to publish to pub.dev -version: 1.0.16+53 +version: 1.0.18+55 environment: sdk: ">=3.0.6 <4.0.0"