diff --git a/lib/pages/auth/view/forget_password_web_page.dart b/lib/pages/auth/view/forget_password_web_page.dart index f389f44f..7686ea8f 100644 --- a/lib/pages/auth/view/forget_password_web_page.dart +++ b/lib/pages/auth/view/forget_password_web_page.dart @@ -201,20 +201,17 @@ class ForgetPasswordWebPage extends StatelessWidget { !state.isButtonEnabled && state.remainingTime != 1 ? null - : () { + : + () { if (forgetBloc - .forgetEmailKey.currentState! - .validate() || - forgetBloc - .forgetRegionKey.currentState! - .validate()) { - if (forgetBloc - .forgetRegionKey.currentState! - .validate()) { - forgetBloc.add(StartTimerEvent()); - } + .forgetEmailKey + .currentState! + .validate()) { + forgetBloc.add( + StartTimerEvent()); } }, + child: Text( 'Get Code ${state is TimerState && !state.isButtonEnabled && state.remainingTime != 1 ? "(${forgetBloc.formattedTime(state.remainingTime)}) " : ""}', style: TextStyle( diff --git a/lib/pages/device_managment/ac/bloc/ac_bloc.dart b/lib/pages/device_managment/ac/bloc/ac_bloc.dart index 7c35034e..076e9050 100644 --- a/lib/pages/device_managment/ac/bloc/ac_bloc.dart +++ b/lib/pages/device_managment/ac/bloc/ac_bloc.dart @@ -13,6 +13,7 @@ class AcBloc extends Bloc { late AcStatusModel deviceStatus; final String deviceId; Timer? _timer; + Timer? _countdownTimer; AcBloc({required this.deviceId}) : super(AcsInitialState()) { on(_onFetchAcStatus); @@ -21,7 +22,16 @@ class AcBloc extends Bloc { on(_onAcBatchControl); on(_onFactoryReset); on(_onAcStatusUpdated); + on(_onClose); + on(_handleIncreaseTime); + on(_handleDecreaseTime); + on(_handleUpdateTimer); + on(_handleToggleTimer); + on(_handleApiCountdownValue); } + bool timerActive = false; + int scheduledHours = 0; + int scheduledMinutes = 0; FutureOr _onFetchAcStatus( AcFetchDeviceStatusEvent event, Emitter emit) async { @@ -30,8 +40,23 @@ class AcBloc extends Bloc { final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); deviceStatus = AcStatusModel.fromJson(event.deviceId, status.status); + if (deviceStatus.countdown1 != 0) { + // Convert API value to minutes + final totalMinutes = deviceStatus.countdown1 * 6; + scheduledHours = totalMinutes ~/ 60; + scheduledMinutes = totalMinutes % 60; + timerActive = true; + _startCountdownTimer(emit); + } + + emit(ACStatusLoaded( + status: deviceStatus, + scheduledHours: scheduledHours, + scheduledMinutes: scheduledMinutes, + isTimerActive: timerActive, + )); + _listenToChanges(event.deviceId); - emit(ACStatusLoaded(deviceStatus)); } catch (e) { emit(AcsFailedState(error: e.toString())); } @@ -70,31 +95,16 @@ class AcBloc extends Bloc { void _onAcStatusUpdated(AcStatusUpdated event, Emitter emit) { deviceStatus = event.deviceStatus; - emit(ACStatusLoaded(deviceStatus)); + emit(ACStatusLoaded(status: deviceStatus)); } - // Future testFirebaseConnection() async { - // // Reference to a test node in your database - // final testRef = FirebaseDatabase.instance.ref("test"); - - // // Write a test value - // await testRef.set("Hello, Firebase!"); - - // // Listen for changes on the test node - // testRef.onValue.listen((DatabaseEvent event) { - // final data = event.snapshot.value; - // print("Data from Firebase: $data"); - // // If you see "Hello, Firebase!" printed in your console, it means the connection works. - // }); - // } - FutureOr _onAcControl( AcControlEvent event, Emitter emit) async { final oldValue = _getValueByCode(event.code); _updateLocalValue(event.code, event.value, emit); - emit(ACStatusLoaded(deviceStatus)); + emit(ACStatusLoaded(status: deviceStatus)); await _runDebounce( isBatch: false, @@ -151,7 +161,7 @@ class AcBloc extends Bloc { void _revertValueAndEmit( String deviceId, String code, dynamic oldValue, Emitter emit) { _updateLocalValue(code, oldValue, emit); - emit(ACStatusLoaded(deviceStatus)); + emit(ACStatusLoaded(status: deviceStatus)); } void _updateLocalValue(String code, dynamic value, Emitter emit) { @@ -184,11 +194,16 @@ class AcBloc extends Bloc { if (value is bool) { deviceStatus = deviceStatus.copyWith(childLock: value); } + + case 'countdown_time': + if (value is int) { + deviceStatus = deviceStatus.copyWith(countdown1: value); + } break; default: break; } - emit(ACStatusLoaded(deviceStatus)); + emit(ACStatusLoaded(status: deviceStatus)); } dynamic _getValueByCode(String code) { @@ -203,6 +218,8 @@ class AcBloc extends Bloc { return deviceStatus.fanSpeedsString; case 'child_lock': return deviceStatus.childLock; + case 'countdown_time': + return deviceStatus.countdown1; default: return null; } @@ -216,7 +233,7 @@ class AcBloc extends Bloc { await DevicesManagementApi().getBatchStatus(event.devicesIds); deviceStatus = AcStatusModel.fromJson(event.devicesIds.first, status.status); - emit(ACStatusLoaded(deviceStatus)); + emit(ACStatusLoaded(status: deviceStatus)); } catch (e) { emit(AcsFailedState(error: e.toString())); } @@ -228,7 +245,7 @@ class AcBloc extends Bloc { _updateLocalValue(event.code, event.value, emit); - emit(ACStatusLoaded(deviceStatus)); + emit(ACStatusLoaded(status: deviceStatus)); await _runDebounce( isBatch: true, @@ -257,4 +274,136 @@ class AcBloc extends Bloc { emit(AcsFailedState(error: e.toString())); } } + + void _onClose(OnClose event, Emitter emit) { + _countdownTimer?.cancel(); + _timer?.cancel(); + } + + void _handleIncreaseTime(IncreaseTimeEvent event, Emitter emit) { + if (state is! ACStatusLoaded) return; + final currentState = state as ACStatusLoaded; + int newHours = scheduledHours; + int newMinutes = scheduledMinutes + 30; + newHours += newMinutes ~/ 60; + newMinutes = newMinutes % 60; + if (newHours > 23) { + newHours = 23; + newMinutes = 59; + } + scheduledHours = newHours; + scheduledMinutes = newMinutes; + + emit(currentState.copyWith( + scheduledHours: scheduledHours, + scheduledMinutes: scheduledMinutes, + )); + } + + void _handleDecreaseTime(DecreaseTimeEvent event, Emitter emit) { + if (state is! ACStatusLoaded) return; + final currentState = state as ACStatusLoaded; + int totalMinutes = (scheduledHours * 60) + scheduledMinutes; + totalMinutes = (totalMinutes - 30).clamp(0, 1440); + scheduledHours = totalMinutes ~/ 60; + scheduledMinutes = totalMinutes % 60; + + emit(currentState.copyWith( + scheduledHours: scheduledHours, + scheduledMinutes: scheduledMinutes, + )); + } + + Future _handleToggleTimer( + ToggleScheduleEvent event, Emitter emit) async { + if (state is! ACStatusLoaded) return; + final currentState = state as ACStatusLoaded; + + timerActive = !timerActive; + + if (timerActive) { + final totalMinutes = scheduledHours * 60 + scheduledMinutes; + if (totalMinutes <= 0) { + timerActive = false; + emit(currentState.copyWith(isTimerActive: timerActive)); + return; + } + + try { + final scaledValue = totalMinutes ~/ 6; + await DevicesManagementApi().deviceControl( + deviceId, + Status(code: 'countdown_time', value: scaledValue), + ); + _startCountdownTimer(emit); + emit(currentState.copyWith(isTimerActive: timerActive)); + } catch (e) { + timerActive = false; + emit(AcsFailedState(error: e.toString())); + } + } else { + await DevicesManagementApi().deviceControl( + deviceId, + Status(code: 'countdown_time', value: 0), + ); + _countdownTimer?.cancel(); + scheduledHours = 0; + scheduledMinutes = 0; + emit(currentState.copyWith( + isTimerActive: timerActive, + scheduledHours: 0, + scheduledMinutes: 0, + )); + } + } + + void _startCountdownTimer(Emitter emit) { + _countdownTimer?.cancel(); + int totalSeconds = (scheduledHours * 3600) + (scheduledMinutes * 60); + + _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (totalSeconds > 0) { + totalSeconds--; + scheduledHours = totalSeconds ~/ 3600; + scheduledMinutes = (totalSeconds % 3600) ~/ 60; + add(UpdateTimerEvent()); + } else { + _countdownTimer?.cancel(); + timerActive = false; + scheduledHours = 0; + scheduledMinutes = 0; + add(TimerCompletedEvent()); + } + }); + } + + void _handleUpdateTimer(UpdateTimerEvent event, Emitter emit) { + if (state is ACStatusLoaded) { + final currentState = state as ACStatusLoaded; + emit(currentState.copyWith( + scheduledHours: scheduledHours, + scheduledMinutes: scheduledMinutes, + isTimerActive: timerActive, + )); + } + } + + void _handleApiCountdownValue( + ApiCountdownValueEvent event, Emitter emit) { + if (state is ACStatusLoaded) { + final totalMinutes = event.apiValue * 6; + final scheduledHours = totalMinutes ~/ 60; + scheduledMinutes = totalMinutes % 60; + _startCountdownTimer( + emit, + ); + add(UpdateTimerEvent()); + } + } + + @override + Future close() { + add(OnClose()); + return super.close(); + } } diff --git a/lib/pages/device_managment/ac/bloc/ac_event.dart b/lib/pages/device_managment/ac/bloc/ac_event.dart index 5492e198..9764f3ed 100644 --- a/lib/pages/device_managment/ac/bloc/ac_event.dart +++ b/lib/pages/device_managment/ac/bloc/ac_event.dart @@ -8,6 +8,7 @@ sealed class AcsEvent extends Equatable { @override List get props => []; } + class AcUpdated extends AcsEvent {} class AcFetchDeviceStatusEvent extends AcsEvent { @@ -18,10 +19,12 @@ class AcFetchDeviceStatusEvent extends AcsEvent { @override List get props => [deviceId]; } + class AcStatusUpdated extends AcsEvent { final AcStatusModel deviceStatus; AcStatusUpdated(this.deviceStatus); } + class AcFetchBatchStatusEvent extends AcsEvent { final List devicesIds; @@ -73,3 +76,30 @@ class AcFactoryResetEvent extends AcsEvent { @override List get props => [deviceId, factoryResetModel]; } + + + +class OnClose extends AcsEvent {} + +class IncreaseTimeEvent extends AcsEvent { + @override + List get props => []; +} + +class DecreaseTimeEvent extends AcsEvent { + @override + List get props => []; +} + +class ToggleScheduleEvent extends AcsEvent {} + +class TimerCompletedEvent extends AcsEvent {} + +class UpdateTimerEvent extends AcsEvent { +} + +class ApiCountdownValueEvent extends AcsEvent { + final int apiValue; + + const ApiCountdownValueEvent(this.apiValue); +} diff --git a/lib/pages/device_managment/ac/bloc/ac_state.dart b/lib/pages/device_managment/ac/bloc/ac_state.dart index dfd12e6d..3e1e2c68 100644 --- a/lib/pages/device_managment/ac/bloc/ac_state.dart +++ b/lib/pages/device_managment/ac/bloc/ac_state.dart @@ -2,8 +2,9 @@ import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/device_managment/ac/model/ac_model.dart'; abstract class AcsState extends Equatable { - const AcsState(); + final bool isTimerActive; + const AcsState({this.isTimerActive = false}); @override List get props => []; } @@ -15,8 +16,30 @@ class AcsLoadingState extends AcsState {} class ACStatusLoaded extends AcsState { final AcStatusModel status; final DateTime timestamp; + final int scheduledHours; + final int scheduledMinutes; + final bool isTimerActive; - ACStatusLoaded(this.status) : timestamp = DateTime.now(); + ACStatusLoaded({ + required this.status, + this.scheduledHours = 0, + this.scheduledMinutes = 0, + this.isTimerActive = false, + }) : timestamp = DateTime.now(); + ACStatusLoaded copyWith({ + AcStatusModel? status, + int? scheduledHours, + int? scheduledMinutes, + bool? isTimerActive, + int? remainingTime, + }) { + return ACStatusLoaded( + status: status ?? this.status, + scheduledHours: scheduledHours ?? this.scheduledHours, + scheduledMinutes: scheduledMinutes ?? this.scheduledMinutes, + isTimerActive: isTimerActive ?? this.isTimerActive, + ); + } @override List get props => [status, timestamp]; @@ -40,3 +63,14 @@ class AcsFailedState extends AcsState { @override List get props => [error]; } + +class TimerRunInProgress extends AcsState { + final int remainingTime; + + const TimerRunInProgress(this.remainingTime); + + @override + List get props => [remainingTime]; +} + + diff --git a/lib/pages/device_managment/ac/model/ac_model.dart b/lib/pages/device_managment/ac/model/ac_model.dart index 1eb2145f..c67006b2 100644 --- a/lib/pages/device_managment/ac/model/ac_model.dart +++ b/lib/pages/device_managment/ac/model/ac_model.dart @@ -11,6 +11,7 @@ class AcStatusModel { final bool childLock; final TempModes acMode; final FanSpeeds acFanSpeed; + late final int countdown1; AcStatusModel({ required this.uuid, @@ -18,6 +19,7 @@ class AcStatusModel { required this.modeString, required this.tempSet, required this.currentTemp, + required this.countdown1, required this.fanSpeedsString, required this.childLock, }) : acMode = getACMode(modeString), @@ -30,6 +32,7 @@ class AcStatusModel { late int currentTemp; late String fanSpeeds; late bool childLock; + late int _countdown1 = 0; for (var status in jsonList) { switch (status.code) { @@ -51,6 +54,9 @@ class AcStatusModel { case 'child_lock': childLock = status.value ?? false; break; + case 'countdown_time': + _countdown1 = status.value ?? 0; + break; } } @@ -62,6 +68,7 @@ class AcStatusModel { currentTemp: currentTemp, fanSpeedsString: fanSpeeds, childLock: childLock, + countdown1: _countdown1, ); } @@ -73,6 +80,7 @@ class AcStatusModel { int? currentTemp, String? fanSpeedsString, bool? childLock, + int? countdown1, }) { return AcStatusModel( uuid: uuid ?? this.uuid, @@ -82,6 +90,7 @@ class AcStatusModel { currentTemp: currentTemp ?? this.currentTemp, fanSpeedsString: fanSpeedsString ?? this.fanSpeedsString, childLock: childLock ?? this.childLock, + countdown1: countdown1 ?? this.countdown1, ); } diff --git a/lib/pages/device_managment/ac/view/ac_device_control.dart b/lib/pages/device_managment/ac/view/ac_device_control.dart index 071344d7..e5c9bbbd 100644 --- a/lib/pages/device_managment/ac/view/ac_device_control.dart +++ b/lib/pages/device_managment/ac/view/ac_device_control.dart @@ -10,11 +10,10 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_mo import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; -import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; class AcDeviceControlsView extends StatelessWidget with HelperResponsiveLayout { - const AcDeviceControlsView({super.key, required this.device}); + AcDeviceControlsView({super.key, required this.device}); final AllDevicesModel device; @@ -23,11 +22,15 @@ class AcDeviceControlsView extends StatelessWidget with HelperResponsiveLayout { final isExtraLarge = isExtraLargeScreenSize(context); final isLarge = isLargeScreenSize(context); final isMedium = isMediumScreenSize(context); + return BlocProvider( create: (context) => AcBloc(deviceId: device.uuid!) ..add(AcFetchDeviceStatusEvent(device.uuid!)), child: BlocBuilder( builder: (context, state) { + final acBloc = BlocProvider.of(context); + + print(state); if (state is ACStatusLoaded) { return GridView( padding: const EdgeInsets.symmetric(horizontal: 50), @@ -78,56 +81,101 @@ class AcDeviceControlsView extends StatelessWidget with HelperResponsiveLayout { ), ToggleWidget( label: '', - labelWidget: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, + labelWidget: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, children: [ - IconButton( - padding: const EdgeInsets.all(0), - onPressed: () {}, - icon: const Icon( - Icons.remove, - size: 28, - color: ColorsManager.greyColor, + Container( + width: MediaQuery.of(context).size.width, + decoration: const ShapeDecoration( + color: ColorsManager.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(30)), + ), ), ), - Text( - '06', - style: context.textTheme.titleLarge!.copyWith( - color: ColorsManager.dialogBlueTitle, - fontWeight: FontWeight.bold, - ), - ), - Text( - 'h', - style: context.textTheme.bodySmall! - .copyWith(color: ColorsManager.blackColor), - ), - Text( - '30', - style: context.textTheme.titleLarge!.copyWith( - color: ColorsManager.dialogBlueTitle, - fontWeight: FontWeight.bold, - ), - ), - Text('m', - style: context.textTheme.bodySmall! - .copyWith(color: ColorsManager.blackColor)), - IconButton( - padding: const EdgeInsets.all(0), - onPressed: () {}, - icon: const Icon( - Icons.add, - size: 28, - color: ColorsManager.greyColor, + Center( + child: SizedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + IconButton( + onPressed: () { + if (acBloc.timerActive == false) { + context + .read() + .add(DecreaseTimeEvent()); + } + }, + icon: const Icon(Icons.remove, + color: ColorsManager.greyColor), + ), + Text( + acBloc.scheduledHours + .toString() + .padLeft(2, '0'), + style: Theme.of(context) + .textTheme + .titleLarge! + .copyWith( + color: ColorsManager.dialogBlueTitle, + fontWeight: FontWeight.bold, + ), + ), + Text( + 'h', + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + color: ColorsManager.blackColor, + ), + ), + Text( + acBloc.scheduledMinutes + .toString() + .padLeft(2, '0'), + style: Theme.of(context) + .textTheme + .titleLarge! + .copyWith( + color: ColorsManager.dialogBlueTitle, + fontWeight: FontWeight.bold, + ), + ), + Text( + 'm', + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + color: ColorsManager.blackColor, + ), + ), + IconButton( + onPressed: () { + if (acBloc.timerActive == false) { + context + .read() + .add(IncreaseTimeEvent()); + } + }, + icon: const Icon(Icons.add, + color: ColorsManager.greyColor), + ), + ], + ), ), ), ], ), - value: false, + value: acBloc.timerActive, code: 'ac_schedule', deviceId: device.uuid!, icon: Assets.acSchedule, - onChange: (value) {}, + onChange: (value) { + context.read().add(ToggleScheduleEvent()); + }, ), ToggleWidget( deviceId: device.uuid!,