Merged with dev

This commit is contained in:
Abdullah Alassaf
2025-04-15 11:18:43 +03:00
13 changed files with 557 additions and 234 deletions

View File

@ -201,20 +201,17 @@ class ForgetPasswordWebPage extends StatelessWidget {
!state.isButtonEnabled && !state.isButtonEnabled &&
state.remainingTime != 1 state.remainingTime != 1
? null ? null
: () { :
() {
if (forgetBloc if (forgetBloc
.forgetEmailKey.currentState! .forgetEmailKey
.validate() || .currentState!
forgetBloc .validate()) {
.forgetRegionKey.currentState! forgetBloc.add(
.validate()) { StartTimerEvent());
if (forgetBloc
.forgetRegionKey.currentState!
.validate()) {
forgetBloc.add(StartTimerEvent());
}
} }
}, },
child: Text( child: Text(
'Get Code ${state is TimerState && !state.isButtonEnabled && state.remainingTime != 1 ? "(${forgetBloc.formattedTime(state.remainingTime)}) " : ""}', 'Get Code ${state is TimerState && !state.isButtonEnabled && state.remainingTime != 1 ? "(${forgetBloc.formattedTime(state.remainingTime)}) " : ""}',
style: TextStyle( style: TextStyle(

View File

@ -13,6 +13,7 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
late AcStatusModel deviceStatus; late AcStatusModel deviceStatus;
final String deviceId; final String deviceId;
Timer? _timer; Timer? _timer;
Timer? _countdownTimer;
AcBloc({required this.deviceId}) : super(AcsInitialState()) { AcBloc({required this.deviceId}) : super(AcsInitialState()) {
on<AcFetchDeviceStatusEvent>(_onFetchAcStatus); on<AcFetchDeviceStatusEvent>(_onFetchAcStatus);
@ -21,7 +22,16 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
on<AcBatchControlEvent>(_onAcBatchControl); on<AcBatchControlEvent>(_onAcBatchControl);
on<AcFactoryResetEvent>(_onFactoryReset); on<AcFactoryResetEvent>(_onFactoryReset);
on<AcStatusUpdated>(_onAcStatusUpdated); on<AcStatusUpdated>(_onAcStatusUpdated);
on<OnClose>(_onClose);
on<IncreaseTimeEvent>(_handleIncreaseTime);
on<DecreaseTimeEvent>(_handleDecreaseTime);
on<UpdateTimerEvent>(_handleUpdateTimer);
on<ToggleScheduleEvent>(_handleToggleTimer);
on<ApiCountdownValueEvent>(_handleApiCountdownValue);
} }
bool timerActive = false;
int scheduledHours = 0;
int scheduledMinutes = 0;
FutureOr<void> _onFetchAcStatus( FutureOr<void> _onFetchAcStatus(
AcFetchDeviceStatusEvent event, Emitter<AcsState> emit) async { AcFetchDeviceStatusEvent event, Emitter<AcsState> emit) async {
@ -30,8 +40,23 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
final status = final status =
await DevicesManagementApi().getDeviceStatus(event.deviceId); await DevicesManagementApi().getDeviceStatus(event.deviceId);
deviceStatus = AcStatusModel.fromJson(event.deviceId, status.status); 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); _listenToChanges(event.deviceId);
emit(ACStatusLoaded(deviceStatus));
} catch (e) { } catch (e) {
emit(AcsFailedState(error: e.toString())); emit(AcsFailedState(error: e.toString()));
} }
@ -70,31 +95,16 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
void _onAcStatusUpdated(AcStatusUpdated event, Emitter<AcsState> emit) { void _onAcStatusUpdated(AcStatusUpdated event, Emitter<AcsState> emit) {
deviceStatus = event.deviceStatus; deviceStatus = event.deviceStatus;
emit(ACStatusLoaded(deviceStatus)); emit(ACStatusLoaded(status: deviceStatus));
} }
// Future<void> 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<void> _onAcControl( FutureOr<void> _onAcControl(
AcControlEvent event, Emitter<AcsState> emit) async { AcControlEvent event, Emitter<AcsState> emit) async {
final oldValue = _getValueByCode(event.code); final oldValue = _getValueByCode(event.code);
_updateLocalValue(event.code, event.value, emit); _updateLocalValue(event.code, event.value, emit);
emit(ACStatusLoaded(deviceStatus)); emit(ACStatusLoaded(status: deviceStatus));
await _runDebounce( await _runDebounce(
isBatch: false, isBatch: false,
@ -151,7 +161,7 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
void _revertValueAndEmit( void _revertValueAndEmit(
String deviceId, String code, dynamic oldValue, Emitter<AcsState> emit) { String deviceId, String code, dynamic oldValue, Emitter<AcsState> emit) {
_updateLocalValue(code, oldValue, emit); _updateLocalValue(code, oldValue, emit);
emit(ACStatusLoaded(deviceStatus)); emit(ACStatusLoaded(status: deviceStatus));
} }
void _updateLocalValue(String code, dynamic value, Emitter<AcsState> emit) { void _updateLocalValue(String code, dynamic value, Emitter<AcsState> emit) {
@ -184,11 +194,16 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
if (value is bool) { if (value is bool) {
deviceStatus = deviceStatus.copyWith(childLock: value); deviceStatus = deviceStatus.copyWith(childLock: value);
} }
case 'countdown_time':
if (value is int) {
deviceStatus = deviceStatus.copyWith(countdown1: value);
}
break; break;
default: default:
break; break;
} }
emit(ACStatusLoaded(deviceStatus)); emit(ACStatusLoaded(status: deviceStatus));
} }
dynamic _getValueByCode(String code) { dynamic _getValueByCode(String code) {
@ -203,6 +218,8 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
return deviceStatus.fanSpeedsString; return deviceStatus.fanSpeedsString;
case 'child_lock': case 'child_lock':
return deviceStatus.childLock; return deviceStatus.childLock;
case 'countdown_time':
return deviceStatus.countdown1;
default: default:
return null; return null;
} }
@ -216,7 +233,7 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
await DevicesManagementApi().getBatchStatus(event.devicesIds); await DevicesManagementApi().getBatchStatus(event.devicesIds);
deviceStatus = deviceStatus =
AcStatusModel.fromJson(event.devicesIds.first, status.status); AcStatusModel.fromJson(event.devicesIds.first, status.status);
emit(ACStatusLoaded(deviceStatus)); emit(ACStatusLoaded(status: deviceStatus));
} catch (e) { } catch (e) {
emit(AcsFailedState(error: e.toString())); emit(AcsFailedState(error: e.toString()));
} }
@ -228,7 +245,7 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
_updateLocalValue(event.code, event.value, emit); _updateLocalValue(event.code, event.value, emit);
emit(ACStatusLoaded(deviceStatus)); emit(ACStatusLoaded(status: deviceStatus));
await _runDebounce( await _runDebounce(
isBatch: true, isBatch: true,
@ -257,4 +274,144 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
emit(AcsFailedState(error: e.toString())); emit(AcsFailedState(error: e.toString()));
} }
} }
void _onClose(OnClose event, Emitter<AcsState> emit) {
_countdownTimer?.cancel();
_timer?.cancel();
}
void _handleIncreaseTime(IncreaseTimeEvent event, Emitter<AcsState> 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<AcsState> 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<void> _handleToggleTimer(
ToggleScheduleEvent event, Emitter<AcsState> 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 _runDebounce(
isBatch: false,
deviceId: deviceId,
code: 'countdown_time',
value: scaledValue,
oldValue: scaledValue,
emit: emit,
);
_startCountdownTimer(emit);
emit(currentState.copyWith(isTimerActive: timerActive));
} catch (e) {
timerActive = false;
emit(AcsFailedState(error: e.toString()));
}
} else {
await _runDebounce(
isBatch: false,
deviceId: deviceId,
code: 'countdown_time',
value: 0,
oldValue: 0,
emit: emit,
);
_countdownTimer?.cancel();
scheduledHours = 0;
scheduledMinutes = 0;
emit(currentState.copyWith(
isTimerActive: timerActive,
scheduledHours: 0,
scheduledMinutes: 0,
));
}
}
void _startCountdownTimer(Emitter<AcsState> 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<AcsState> emit) {
if (state is ACStatusLoaded) {
final currentState = state as ACStatusLoaded;
emit(currentState.copyWith(
scheduledHours: scheduledHours,
scheduledMinutes: scheduledMinutes,
isTimerActive: timerActive,
));
}
}
void _handleApiCountdownValue(
ApiCountdownValueEvent event, Emitter<AcsState> emit) {
if (state is ACStatusLoaded) {
final totalMinutes = event.apiValue * 6;
final scheduledHours = totalMinutes ~/ 60;
scheduledMinutes = totalMinutes % 60;
_startCountdownTimer(
emit,
);
add(UpdateTimerEvent());
}
}
@override
Future<void> close() {
add(OnClose());
return super.close();
}
} }

View File

@ -8,6 +8,7 @@ sealed class AcsEvent extends Equatable {
@override @override
List<Object> get props => []; List<Object> get props => [];
} }
class AcUpdated extends AcsEvent {} class AcUpdated extends AcsEvent {}
class AcFetchDeviceStatusEvent extends AcsEvent { class AcFetchDeviceStatusEvent extends AcsEvent {
@ -18,10 +19,12 @@ class AcFetchDeviceStatusEvent extends AcsEvent {
@override @override
List<Object> get props => [deviceId]; List<Object> get props => [deviceId];
} }
class AcStatusUpdated extends AcsEvent { class AcStatusUpdated extends AcsEvent {
final AcStatusModel deviceStatus; final AcStatusModel deviceStatus;
AcStatusUpdated(this.deviceStatus); AcStatusUpdated(this.deviceStatus);
} }
class AcFetchBatchStatusEvent extends AcsEvent { class AcFetchBatchStatusEvent extends AcsEvent {
final List<String> devicesIds; final List<String> devicesIds;
@ -73,3 +76,30 @@ class AcFactoryResetEvent extends AcsEvent {
@override @override
List<Object> get props => [deviceId, factoryResetModel]; List<Object> get props => [deviceId, factoryResetModel];
} }
class OnClose extends AcsEvent {}
class IncreaseTimeEvent extends AcsEvent {
@override
List<Object> get props => [];
}
class DecreaseTimeEvent extends AcsEvent {
@override
List<Object> 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);
}

View File

@ -2,8 +2,9 @@ import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/device_managment/ac/model/ac_model.dart'; import 'package:syncrow_web/pages/device_managment/ac/model/ac_model.dart';
abstract class AcsState extends Equatable { abstract class AcsState extends Equatable {
const AcsState(); final bool isTimerActive;
const AcsState({this.isTimerActive = false});
@override @override
List<Object> get props => []; List<Object> get props => [];
} }
@ -15,8 +16,30 @@ class AcsLoadingState extends AcsState {}
class ACStatusLoaded extends AcsState { class ACStatusLoaded extends AcsState {
final AcStatusModel status; final AcStatusModel status;
final DateTime timestamp; 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 @override
List<Object> get props => [status, timestamp]; List<Object> get props => [status, timestamp];
@ -40,3 +63,14 @@ class AcsFailedState extends AcsState {
@override @override
List<Object> get props => [error]; List<Object> get props => [error];
} }
class TimerRunInProgress extends AcsState {
final int remainingTime;
const TimerRunInProgress(this.remainingTime);
@override
List<Object> get props => [remainingTime];
}

View File

@ -11,6 +11,7 @@ class AcStatusModel {
final bool childLock; final bool childLock;
final TempModes acMode; final TempModes acMode;
final FanSpeeds acFanSpeed; final FanSpeeds acFanSpeed;
final int countdown1;
AcStatusModel({ AcStatusModel({
required this.uuid, required this.uuid,
@ -18,6 +19,7 @@ class AcStatusModel {
required this.modeString, required this.modeString,
required this.tempSet, required this.tempSet,
required this.currentTemp, required this.currentTemp,
required this.countdown1,
required this.fanSpeedsString, required this.fanSpeedsString,
required this.childLock, required this.childLock,
}) : acMode = getACMode(modeString), }) : acMode = getACMode(modeString),
@ -30,6 +32,7 @@ class AcStatusModel {
late int currentTemp; late int currentTemp;
late String fanSpeeds; late String fanSpeeds;
late bool childLock; late bool childLock;
late int _countdown1 = 0;
for (var status in jsonList) { for (var status in jsonList) {
switch (status.code) { switch (status.code) {
@ -51,6 +54,9 @@ class AcStatusModel {
case 'child_lock': case 'child_lock':
childLock = status.value ?? false; childLock = status.value ?? false;
break; break;
case 'countdown_time':
_countdown1 = status.value ?? 0;
break;
} }
} }
@ -62,6 +68,7 @@ class AcStatusModel {
currentTemp: currentTemp, currentTemp: currentTemp,
fanSpeedsString: fanSpeeds, fanSpeedsString: fanSpeeds,
childLock: childLock, childLock: childLock,
countdown1: _countdown1,
); );
} }
@ -73,6 +80,7 @@ class AcStatusModel {
int? currentTemp, int? currentTemp,
String? fanSpeedsString, String? fanSpeedsString,
bool? childLock, bool? childLock,
int? countdown1,
}) { }) {
return AcStatusModel( return AcStatusModel(
uuid: uuid ?? this.uuid, uuid: uuid ?? this.uuid,
@ -82,6 +90,7 @@ class AcStatusModel {
currentTemp: currentTemp ?? this.currentTemp, currentTemp: currentTemp ?? this.currentTemp,
fanSpeedsString: fanSpeedsString ?? this.fanSpeedsString, fanSpeedsString: fanSpeedsString ?? this.fanSpeedsString,
childLock: childLock ?? this.childLock, childLock: childLock ?? this.childLock,
countdown1: countdown1 ?? this.countdown1,
); );
} }

View File

@ -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/pages/device_managment/shared/toggle_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.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'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
class AcDeviceControlsView extends StatelessWidget with HelperResponsiveLayout { class AcDeviceControlsView extends StatelessWidget with HelperResponsiveLayout {
const AcDeviceControlsView({super.key, required this.device}); const AcDeviceControlsView({super.key, required this.device});
final AllDevicesModel device; final AllDevicesModel device;
@ -23,11 +22,13 @@ class AcDeviceControlsView extends StatelessWidget with HelperResponsiveLayout {
final isExtraLarge = isExtraLargeScreenSize(context); final isExtraLarge = isExtraLargeScreenSize(context);
final isLarge = isLargeScreenSize(context); final isLarge = isLargeScreenSize(context);
final isMedium = isMediumScreenSize(context); final isMedium = isMediumScreenSize(context);
return BlocProvider( return BlocProvider(
create: (context) => AcBloc(deviceId: device.uuid!) create: (context) => AcBloc(deviceId: device.uuid!)
..add(AcFetchDeviceStatusEvent(device.uuid!)), ..add(AcFetchDeviceStatusEvent(device.uuid!)),
child: BlocBuilder<AcBloc, AcsState>( child: BlocBuilder<AcBloc, AcsState>(
builder: (context, state) { builder: (context, state) {
final acBloc = BlocProvider.of<AcBloc>(context);
if (state is ACStatusLoaded) { if (state is ACStatusLoaded) {
return GridView( return GridView(
padding: const EdgeInsets.symmetric(horizontal: 50), padding: const EdgeInsets.symmetric(horizontal: 50),
@ -78,56 +79,101 @@ class AcDeviceControlsView extends StatelessWidget with HelperResponsiveLayout {
), ),
ToggleWidget( ToggleWidget(
label: '', label: '',
labelWidget: Row( labelWidget: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
IconButton( Container(
padding: const EdgeInsets.all(0), width: MediaQuery.of(context).size.width,
onPressed: () {}, decoration: const ShapeDecoration(
icon: const Icon( color: ColorsManager.primaryColor,
Icons.remove, shape: RoundedRectangleBorder(
size: 28, borderRadius: BorderRadius.all(Radius.circular(30)),
color: ColorsManager.greyColor, ),
), ),
), ),
Text( Center(
'06', child: SizedBox(
style: context.textTheme.titleLarge!.copyWith( child: Row(
color: ColorsManager.dialogBlueTitle, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
fontWeight: FontWeight.bold, children: [
), IconButton(
), onPressed: () {
Text( if (acBloc.timerActive == false) {
'h', context
style: context.textTheme.bodySmall! .read<AcBloc>()
.copyWith(color: ColorsManager.blackColor), .add(DecreaseTimeEvent());
), }
Text( },
'30', icon: const Icon(Icons.remove,
style: context.textTheme.titleLarge!.copyWith( color: ColorsManager.greyColor),
color: ColorsManager.dialogBlueTitle, ),
fontWeight: FontWeight.bold, Text(
), acBloc.scheduledHours
), .toString()
Text('m', .padLeft(2, '0'),
style: context.textTheme.bodySmall! style: Theme.of(context)
.copyWith(color: ColorsManager.blackColor)), .textTheme
IconButton( .titleLarge!
padding: const EdgeInsets.all(0), .copyWith(
onPressed: () {}, color: ColorsManager.dialogBlueTitle,
icon: const Icon( fontWeight: FontWeight.bold,
Icons.add, ),
size: 28, ),
color: ColorsManager.greyColor, 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<AcBloc>()
.add(IncreaseTimeEvent());
}
},
icon: const Icon(Icons.add,
color: ColorsManager.greyColor),
),
],
),
), ),
), ),
], ],
), ),
value: false, value: acBloc.timerActive,
code: 'ac_schedule', code: 'ac_schedule',
deviceId: device.uuid!, deviceId: device.uuid!,
icon: Assets.acSchedule, icon: Assets.acSchedule,
onChange: (value) {}, onChange: (value) {
context.read<AcBloc>().add(ToggleScheduleEvent());
},
), ),
ToggleWidget( ToggleWidget(
deviceId: device.uuid!, deviceId: device.uuid!,

View File

@ -62,9 +62,6 @@ class ToggleWidget extends StatelessWidget {
)), )),
if (showToggle) if (showToggle)
Container( Container(
height: 20,
width: 35,
padding: const EdgeInsets.only(right: 16, top: 10),
child: CupertinoSwitch( child: CupertinoSwitch(
value: value, value: value,
activeColor: ColorsManager.dialogBlueTitle, activeColor: ColorsManager.dialogBlueTitle,

View File

@ -13,7 +13,8 @@ import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_la
import '../models/sos_status_model.dart'; import '../models/sos_status_model.dart';
class SosDeviceControlsView extends StatelessWidget with HelperResponsiveLayout { class SosDeviceControlsView extends StatelessWidget
with HelperResponsiveLayout {
const SosDeviceControlsView({ const SosDeviceControlsView({
super.key, super.key,
required this.device, required this.device,
@ -24,7 +25,8 @@ class SosDeviceControlsView extends StatelessWidget with HelperResponsiveLayout
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => SosDeviceBloc()..add(GetDeviceStatus(device.uuid!)), create: (context) =>
SosDeviceBloc()..add(GetDeviceStatus(device.uuid!)),
child: BlocBuilder<SosDeviceBloc, SosDeviceState>( child: BlocBuilder<SosDeviceBloc, SosDeviceState>(
builder: (context, state) { builder: (context, state) {
if (state is SosDeviceLoadingState) { if (state is SosDeviceLoadingState) {
@ -63,7 +65,8 @@ class SosDeviceControlsView extends StatelessWidget with HelperResponsiveLayout
)); ));
} }
Widget _buildStatusControls(BuildContext context, SosStatusModel deviceStatus) { Widget _buildStatusControls(
BuildContext context, SosStatusModel deviceStatus) {
final isExtraLarge = isExtraLargeScreenSize(context); final isExtraLarge = isExtraLargeScreenSize(context);
final isLarge = isLargeScreenSize(context); final isLarge = isLargeScreenSize(context);
final isMedium = isMediumScreenSize(context); final isMedium = isMediumScreenSize(context);
@ -85,7 +88,7 @@ class SosDeviceControlsView extends StatelessWidget with HelperResponsiveLayout
IconNameStatusContainer( IconNameStatusContainer(
isFullIcon: false, isFullIcon: false,
name: deviceStatus.sosStatus == 'sos' ? 'SOS' : 'Normal', name: deviceStatus.sosStatus == 'sos' ? 'SOS' : 'Normal',
icon: deviceStatus.sosStatus == 'sos' ? Assets.sos : Assets.sosNormal, icon: deviceStatus.sosStatus == 'sos' ? Assets.sosNormal : Assets.sos,
onTap: () {}, onTap: () {},
status: false, status: false,
textColor: ColorsManager.blackColor, textColor: ColorsManager.blackColor,

View File

@ -63,7 +63,11 @@ class _CreateNewRoutinesDialogState extends State<CreateNewRoutinesDialog> {
Padding( Padding(
padding: const EdgeInsets.only(left: 15, right: 15), padding: const EdgeInsets.only(left: 15, right: 15),
child: CommunityDropdown( child: CommunityDropdown(
communities: _bloc.communities, communities: _bloc.communities..sort(
(a, b) => a.name.toLowerCase().compareTo(
b.name.toLowerCase(),
),
),
selectedValue: _selectedCommunity, selectedValue: _selectedCommunity,
onChanged: (String? newValue) { onChanged: (String? newValue) {
setState(() { setState(() {

View File

@ -1,4 +1,3 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
@ -28,9 +27,12 @@ class _TimeWheelPickerState extends State<TimeWheelPicker> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_hoursController = FixedExtentScrollController(initialItem: widget.initialHours); _hoursController =
_minutesController = FixedExtentScrollController(initialItem: widget.initialMinutes); FixedExtentScrollController(initialItem: widget.initialHours);
_secondsController = FixedExtentScrollController(initialItem: widget.initialSeconds); _minutesController =
FixedExtentScrollController(initialItem: widget.initialMinutes);
_secondsController =
FixedExtentScrollController(initialItem: widget.initialSeconds);
} }
@override @override
@ -47,6 +49,8 @@ class _TimeWheelPickerState extends State<TimeWheelPicker> {
} }
} }
@override @override
void dispose() { void dispose() {
_hoursController.dispose(); _hoursController.dispose();
@ -61,26 +65,28 @@ class _TimeWheelPickerState extends State<TimeWheelPicker> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
_buildPickerColumn( _buildPickerColumn(
label: 'h', label: 'h',
controller: _hoursController, controller: _hoursController,
itemCount: 24, itemCount: 3,
onChanged: (value) => _handleTimeChange( onChanged: (value) {
value, _handleTimeChange(
_minutesController.selectedItem, value,
_secondsController.selectedItem, _minutesController.selectedItem,
), _secondsController.selectedItem,
), );
}),
const SizedBox(width: 5), const SizedBox(width: 5),
_buildPickerColumn( _buildPickerColumn(
label: 'm', label: 'm',
controller: _minutesController, controller: _minutesController,
itemCount: 60, itemCount: 60,
onChanged: (value) => _handleTimeChange( onChanged: (value) {
_hoursController.selectedItem, _handleTimeChange(
value, _hoursController.selectedItem,
_secondsController.selectedItem, value,
), _secondsController.selectedItem,
), );
}),
const SizedBox(width: 5), const SizedBox(width: 5),
_buildPickerColumn( _buildPickerColumn(
label: 's', label: 's',
@ -97,6 +103,19 @@ class _TimeWheelPickerState extends State<TimeWheelPicker> {
} }
void _handleTimeChange(int hours, int minutes, int seconds) { void _handleTimeChange(int hours, int minutes, int seconds) {
int total = hours * 3600 + minutes * 60 + seconds;
if (total > 10000) {
hours = 2;
minutes = 46;
seconds = 40;
total = 10000;
WidgetsBinding.instance.addPostFrameCallback((_) {
_hoursController.jumpToItem(hours);
_minutesController.jumpToItem(minutes);
_secondsController.jumpToItem(seconds);
});
}
widget.onTimeChanged(hours, minutes, seconds); widget.onTimeChanged(hours, minutes, seconds);
} }
@ -147,4 +166,4 @@ class _TimeWheelPickerState extends State<TimeWheelPicker> {
], ],
); );
} }
} }

View File

@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart';
import 'package:syncrow_web/pages/spaces_management/create_community/view/create_community_dialog.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class SidebarAddCommunityButton extends StatelessWidget {
const SidebarAddCommunityButton({
required this.existingCommunityNames,
super.key,
});
final List<String> existingCommunityNames;
@override
Widget build(BuildContext context) {
return SizedBox.square(
dimension: 30,
child: IconButton(
style: IconButton.styleFrom(
iconSize: 20,
backgroundColor: ColorsManager.circleImageBackground,
shape: const CircleBorder(
side: BorderSide(
color: ColorsManager.lightGrayBorderColor,
width: 3,
),
),
),
onPressed: () => _showCreateCommunityDialog(context),
icon: SvgPicture.asset(Assets.addIcon),
),
);
}
void _showCreateCommunityDialog(BuildContext context) => showDialog<void>(
context: context,
builder: (context) => CreateCommunityDialog(
isEditMode: false,
existingCommunityNames: existingCommunityNames,
onCreateCommunity: (name, description) {
context.read<SpaceManagementBloc>().add(
CreateCommunityEvent(name, description, context),
);
},
),
);
}

View File

@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/sidebar_add_community_button.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/utils/style.dart';
class SidebarHeader extends StatelessWidget {
const SidebarHeader({required this.existingCommunityNames, super.key});
final List<String> existingCommunityNames;
@override
Widget build(BuildContext context) {
return Container(
decoration: subSectionContainerDecoration,
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Communities',
style: context.textTheme.titleMedium?.copyWith(
color: ColorsManager.blackColor,
),
),
SidebarAddCommunityButton(existingCommunityNames: existingCommunityNames),
],
),
);
}
}

View File

@ -1,17 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/common/widgets/search_bar.dart'; import 'package:syncrow_web/common/widgets/search_bar.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/community_tile.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/community_tile.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/sidebar_header.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/space_tile_widget.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/space_tile_widget.dart';
import 'package:syncrow_web/pages/spaces_management/structure_selector/bloc/center_body_bloc.dart'; import 'package:syncrow_web/pages/spaces_management/structure_selector/bloc/center_body_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/structure_selector/bloc/center_body_event.dart'; import 'package:syncrow_web/pages/spaces_management/structure_selector/bloc/center_body_event.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/style.dart'; import 'package:syncrow_web/utils/style.dart';
class SidebarWidget extends StatefulWidget { class SidebarWidget extends StatefulWidget {
@ -19,62 +17,59 @@ class SidebarWidget extends StatefulWidget {
final String? selectedSpaceUuid; final String? selectedSpaceUuid;
const SidebarWidget({ const SidebarWidget({
super.key,
required this.communities, required this.communities,
this.selectedSpaceUuid, this.selectedSpaceUuid,
super.key,
}); });
@override @override
_SidebarWidgetState createState() => _SidebarWidgetState(); State<SidebarWidget> createState() => _SidebarWidgetState();
} }
class _SidebarWidgetState extends State<SidebarWidget> { class _SidebarWidgetState extends State<SidebarWidget> {
String _searchQuery = ''; // Track search query String _searchQuery = '';
String? _selectedSpaceUuid; String? _selectedSpaceUuid;
String? _selectedId; String? _selectedId;
@override @override
void initState() { void initState() {
_selectedId = widget.selectedSpaceUuid;
super.initState(); super.initState();
_selectedId = widget.selectedSpaceUuid; // Initialize with the passed selected space UUID
} }
@override @override
void didUpdateWidget(covariant SidebarWidget oldWidget) { void didUpdateWidget(covariant SidebarWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.selectedSpaceUuid != oldWidget.selectedSpaceUuid) { if (widget.selectedSpaceUuid != oldWidget.selectedSpaceUuid) {
setState(() { setState(() => _selectedId = widget.selectedSpaceUuid);
_selectedId = widget.selectedSpaceUuid;
});
} }
super.didUpdateWidget(oldWidget);
} }
// Function to filter communities based on the search query List<CommunityModel> _filteredCommunities() {
List<CommunityModel> _filterCommunities() {
if (_searchQuery.isEmpty) { if (_searchQuery.isEmpty) {
// Reset the selected community and space UUIDs if there's no query
_selectedSpaceUuid = null; _selectedSpaceUuid = null;
return widget.communities; return widget.communities;
} }
// Filter communities and expand only those that match the query
return widget.communities.where((community) { return widget.communities.where((community) {
final containsQueryInCommunity = final containsQueryInCommunity =
community.name.toLowerCase().contains(_searchQuery.toLowerCase()); community.name.toLowerCase().contains(_searchQuery.toLowerCase());
final containsQueryInSpaces = final containsQueryInSpaces = community.spaces.any((space) =>
community.spaces.any((space) => _containsQuery(space, _searchQuery.toLowerCase())); _containsQuery(space: space, query: _searchQuery.toLowerCase()));
return containsQueryInCommunity || containsQueryInSpaces; return containsQueryInCommunity || containsQueryInSpaces;
}).toList(); }).toList();
} }
// Helper function to determine if any space or its children match the search query bool _containsQuery({
bool _containsQuery(SpaceModel space, String query) { required SpaceModel space,
required String query,
}) {
final matchesSpace = space.name.toLowerCase().contains(query); final matchesSpace = space.name.toLowerCase().contains(query);
final matchesChildren = final matchesChildren = space.children.any(
space.children.any((child) => _containsQuery(child, query)); // Recursive check for children (child) => _containsQuery(space: child, query: query),
);
// If the space or any of its children match the query, expand this space
if (matchesSpace || matchesChildren) { if (matchesSpace || matchesChildren) {
_selectedSpaceUuid = space.uuid; _selectedSpaceUuid = space.uuid;
} }
@ -83,79 +78,35 @@ class _SidebarWidgetState extends State<SidebarWidget> {
} }
bool _isSpaceOrChildSelected(SpaceModel space) { bool _isSpaceOrChildSelected(SpaceModel space) {
// Return true if the current space or any of its child spaces is selected final isSpaceSelected = _selectedSpaceUuid == space.uuid;
if (_selectedSpaceUuid == space.uuid) { final anySubSpaceIsSelected = space.children.any(_isSpaceOrChildSelected);
return true; return isSpaceSelected || anySubSpaceIsSelected;
}
// Recursively check if any child spaces match the query
for (var child in space.children) {
if (_isSpaceOrChildSelected(child)) {
return true;
}
}
return false;
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final filteredCommunities = _filterCommunities(); final filteredCommunities = _filteredCommunities();
return Container( return Container(
width: 300, width: 300,
decoration: subSectionContainerDecoration, decoration: subSectionContainerDecoration,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, // Ensures the Column only takes necessary height mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Communities title with the add button SidebarHeader(
Container( existingCommunityNames:
decoration: subSectionContainerDecoration, widget.communities.map((community) => community.name).toList(),
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Communities',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: ColorsManager.blackColor,
)),
GestureDetector(
onTap: () => _navigateToBlank(context),
child: Container(
width: 30,
height: 30,
decoration: const BoxDecoration(
color: ColorsManager.whiteColors,
shape: BoxShape.circle,
),
child: Center(
child: SvgPicture.asset(
Assets.roundedAddIcon,
width: 24,
height: 24,
),
),
),
),
],
),
), ),
// Search bar
CustomSearchBar( CustomSearchBar(
onSearchChanged: (query) { onSearchChanged: (query) => setState(() => _searchQuery = query),
setState(() {
_searchQuery = query;
});
},
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Community list
Expanded( Expanded(
child: ListView( child: ListView(
children: filteredCommunities.map((community) { children: filteredCommunities
return _buildCommunityTile(context, community); .map((community) => _buildCommunityTile(context, community))
}).toList(), .toList(),
), ),
), ),
], ],
@ -163,18 +114,7 @@ class _SidebarWidgetState extends State<SidebarWidget> {
); );
} }
void _navigateToBlank(BuildContext context) {
setState(() {
_selectedId = '';
});
context.read<SpaceManagementBloc>().add(
NewCommunityEvent(communities: widget.communities),
);
}
Widget _buildCommunityTile(BuildContext context, CommunityModel community) { Widget _buildCommunityTile(BuildContext context, CommunityModel community) {
bool hasChildren = community.spaces.isNotEmpty;
return CommunityTile( return CommunityTile(
title: community.name, title: community.name,
key: ValueKey(community.uuid), key: ValueKey(community.uuid),
@ -183,7 +123,7 @@ class _SidebarWidgetState extends State<SidebarWidget> {
onItemSelected: () { onItemSelected: () {
setState(() { setState(() {
_selectedId = community.uuid; _selectedId = community.uuid;
_selectedSpaceUuid = null; // Update the selected community _selectedSpaceUuid = null;
}); });
context.read<CenterBodyBloc>().add(CommunitySelectedEvent()); context.read<CenterBodyBloc>().add(CommunitySelectedEvent());
@ -192,46 +132,51 @@ class _SidebarWidgetState extends State<SidebarWidget> {
SelectCommunityEvent(selectedCommunity: community), SelectCommunityEvent(selectedCommunity: community),
); );
}, },
onExpansionChanged: (String title, bool expanded) { onExpansionChanged: (title, expanded) {},
_handleExpansionChange(community.uuid, expanded); children: community.spaces
}, .where((space) {
children: hasChildren final isDeleted = space.status != SpaceStatus.deleted;
? community.spaces final isParentDeleted = space.status != SpaceStatus.parentDeleted;
.where((space) => (space.status != SpaceStatus.deleted || return (isDeleted || isParentDeleted);
space.status != SpaceStatus.parentDeleted)) })
.map((space) => _buildSpaceTile(space, community)) .map((space) => _buildSpaceTile(space: space, community: community))
.toList() .toList(),
: null,
); );
} }
Widget _buildSpaceTile(SpaceModel space, CommunityModel community, {int depth = 1}) { Widget _buildSpaceTile({
bool isExpandedSpace = _isSpaceOrChildSelected(space); required SpaceModel space,
required CommunityModel community,
}) {
final spaceIsExpanded = _isSpaceOrChildSelected(space);
final isSelected = _selectedId == space.uuid;
return Padding( return Padding(
padding: EdgeInsets.only(left: depth * 16.0), padding: const EdgeInsetsDirectional.only(start: 16.0),
child: SpaceTile( child: SpaceTile(
title: space.name, title: space.name,
key: ValueKey(space.uuid), key: ValueKey(space.uuid),
isSelected: _selectedId == space.uuid, isSelected: isSelected,
initiallyExpanded: isExpandedSpace, initiallyExpanded: spaceIsExpanded,
onExpansionChanged: (bool expanded) { onExpansionChanged: (expanded) {},
_handleExpansionChange(space.uuid ?? '', expanded); onItemSelected: () {
}, setState(() {
onItemSelected: () { _selectedId = space.uuid;
setState(() { _selectedSpaceUuid = space.uuid;
_selectedId = space.uuid; });
_selectedSpaceUuid = space.uuid;
});
context.read<SpaceManagementBloc>().add( context.read<SpaceManagementBloc>().add(
SelectSpaceEvent(selectedCommunity: community, selectedSpace: space), SelectSpaceEvent(selectedCommunity: community, selectedSpace: space),
); );
}, },
children: space.children.isNotEmpty children: space.children
? space.children.map((childSpace) => _buildSpaceTile(childSpace, community)).toList() .map(
: [], // Recursively render child spaces if available (childSpace) => _buildSpaceTile(
)); space: childSpace,
community: community,
),
)
.toList(),
),
);
} }
void _handleExpansionChange(String uuid, bool expanded) {}
} }