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.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(

View File

@ -13,6 +13,7 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
late AcStatusModel deviceStatus;
final String deviceId;
Timer? _timer;
Timer? _countdownTimer;
AcBloc({required this.deviceId}) : super(AcsInitialState()) {
on<AcFetchDeviceStatusEvent>(_onFetchAcStatus);
@ -21,7 +22,16 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
on<AcBatchControlEvent>(_onAcBatchControl);
on<AcFactoryResetEvent>(_onFactoryReset);
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(
AcFetchDeviceStatusEvent event, Emitter<AcsState> emit) async {
@ -30,8 +40,23 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
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<AcsEvent, AcsState> {
void _onAcStatusUpdated(AcStatusUpdated event, Emitter<AcsState> emit) {
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(
AcControlEvent event, Emitter<AcsState> 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<AcsEvent, AcsState> {
void _revertValueAndEmit(
String deviceId, String code, dynamic oldValue, Emitter<AcsState> emit) {
_updateLocalValue(code, oldValue, emit);
emit(ACStatusLoaded(deviceStatus));
emit(ACStatusLoaded(status: deviceStatus));
}
void _updateLocalValue(String code, dynamic value, Emitter<AcsState> emit) {
@ -184,11 +194,16 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
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<AcsEvent, AcsState> {
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<AcsEvent, AcsState> {
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<AcsEvent, AcsState> {
_updateLocalValue(event.code, event.value, emit);
emit(ACStatusLoaded(deviceStatus));
emit(ACStatusLoaded(status: deviceStatus));
await _runDebounce(
isBatch: true,
@ -257,4 +274,144 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
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
List<Object> get props => [];
}
class AcUpdated extends AcsEvent {}
class AcFetchDeviceStatusEvent extends AcsEvent {
@ -18,10 +19,12 @@ class AcFetchDeviceStatusEvent extends AcsEvent {
@override
List<Object> get props => [deviceId];
}
class AcStatusUpdated extends AcsEvent {
final AcStatusModel deviceStatus;
AcStatusUpdated(this.deviceStatus);
}
class AcFetchBatchStatusEvent extends AcsEvent {
final List<String> devicesIds;
@ -73,3 +76,30 @@ class AcFactoryResetEvent extends AcsEvent {
@override
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';
abstract class AcsState extends Equatable {
const AcsState();
final bool isTimerActive;
const AcsState({this.isTimerActive = false});
@override
List<Object> 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<Object> get props => [status, timestamp];
@ -40,3 +63,14 @@ class AcsFailedState extends AcsState {
@override
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 TempModes acMode;
final FanSpeeds acFanSpeed;
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,
);
}

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/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});
const AcDeviceControlsView({super.key, required this.device});
final AllDevicesModel device;
@ -23,11 +22,13 @@ 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<AcBloc, AcsState>(
builder: (context, state) {
final acBloc = BlocProvider.of<AcBloc>(context);
if (state is ACStatusLoaded) {
return GridView(
padding: const EdgeInsets.symmetric(horizontal: 50),
@ -78,56 +79,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<AcBloc>()
.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<AcBloc>()
.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<AcBloc>().add(ToggleScheduleEvent());
},
),
ToggleWidget(
deviceId: device.uuid!,

View File

@ -62,9 +62,6 @@ class ToggleWidget extends StatelessWidget {
)),
if (showToggle)
Container(
height: 20,
width: 35,
padding: const EdgeInsets.only(right: 16, top: 10),
child: CupertinoSwitch(
value: value,
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';
class SosDeviceControlsView extends StatelessWidget with HelperResponsiveLayout {
class SosDeviceControlsView extends StatelessWidget
with HelperResponsiveLayout {
const SosDeviceControlsView({
super.key,
required this.device,
@ -24,7 +25,8 @@ class SosDeviceControlsView extends StatelessWidget with HelperResponsiveLayout
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => SosDeviceBloc()..add(GetDeviceStatus(device.uuid!)),
create: (context) =>
SosDeviceBloc()..add(GetDeviceStatus(device.uuid!)),
child: BlocBuilder<SosDeviceBloc, SosDeviceState>(
builder: (context, state) {
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 isLarge = isLargeScreenSize(context);
final isMedium = isMediumScreenSize(context);
@ -85,7 +88,7 @@ class SosDeviceControlsView extends StatelessWidget with HelperResponsiveLayout
IconNameStatusContainer(
isFullIcon: false,
name: deviceStatus.sosStatus == 'sos' ? 'SOS' : 'Normal',
icon: deviceStatus.sosStatus == 'sos' ? Assets.sos : Assets.sosNormal,
icon: deviceStatus.sosStatus == 'sos' ? Assets.sosNormal : Assets.sos,
onTap: () {},
status: false,
textColor: ColorsManager.blackColor,

View File

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

View File

@ -1,4 +1,3 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
@ -28,9 +27,12 @@ class _TimeWheelPickerState extends State<TimeWheelPicker> {
@override
void initState() {
super.initState();
_hoursController = FixedExtentScrollController(initialItem: widget.initialHours);
_minutesController = FixedExtentScrollController(initialItem: widget.initialMinutes);
_secondsController = FixedExtentScrollController(initialItem: widget.initialSeconds);
_hoursController =
FixedExtentScrollController(initialItem: widget.initialHours);
_minutesController =
FixedExtentScrollController(initialItem: widget.initialMinutes);
_secondsController =
FixedExtentScrollController(initialItem: widget.initialSeconds);
}
@override
@ -47,6 +49,8 @@ class _TimeWheelPickerState extends State<TimeWheelPicker> {
}
}
@override
void dispose() {
_hoursController.dispose();
@ -61,26 +65,28 @@ class _TimeWheelPickerState extends State<TimeWheelPicker> {
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildPickerColumn(
label: 'h',
controller: _hoursController,
itemCount: 24,
onChanged: (value) => _handleTimeChange(
value,
_minutesController.selectedItem,
_secondsController.selectedItem,
),
),
label: 'h',
controller: _hoursController,
itemCount: 3,
onChanged: (value) {
_handleTimeChange(
value,
_minutesController.selectedItem,
_secondsController.selectedItem,
);
}),
const SizedBox(width: 5),
_buildPickerColumn(
label: 'm',
controller: _minutesController,
itemCount: 60,
onChanged: (value) => _handleTimeChange(
_hoursController.selectedItem,
value,
_secondsController.selectedItem,
),
),
label: 'm',
controller: _minutesController,
itemCount: 60,
onChanged: (value) {
_handleTimeChange(
_hoursController.selectedItem,
value,
_secondsController.selectedItem,
);
}),
const SizedBox(width: 5),
_buildPickerColumn(
label: 's',
@ -97,6 +103,19 @@ class _TimeWheelPickerState extends State<TimeWheelPicker> {
}
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);
}
@ -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_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.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_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/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/sidebar_header.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_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';
class SidebarWidget extends StatefulWidget {
@ -19,62 +17,59 @@ class SidebarWidget extends StatefulWidget {
final String? selectedSpaceUuid;
const SidebarWidget({
super.key,
required this.communities,
this.selectedSpaceUuid,
super.key,
});
@override
_SidebarWidgetState createState() => _SidebarWidgetState();
State<SidebarWidget> createState() => _SidebarWidgetState();
}
class _SidebarWidgetState extends State<SidebarWidget> {
String _searchQuery = ''; // Track search query
String _searchQuery = '';
String? _selectedSpaceUuid;
String? _selectedId;
@override
void initState() {
_selectedId = widget.selectedSpaceUuid;
super.initState();
_selectedId = widget.selectedSpaceUuid; // Initialize with the passed selected space UUID
}
@override
void didUpdateWidget(covariant SidebarWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.selectedSpaceUuid != oldWidget.selectedSpaceUuid) {
setState(() {
_selectedId = widget.selectedSpaceUuid;
});
setState(() => _selectedId = widget.selectedSpaceUuid);
}
super.didUpdateWidget(oldWidget);
}
// Function to filter communities based on the search query
List<CommunityModel> _filterCommunities() {
List<CommunityModel> _filteredCommunities() {
if (_searchQuery.isEmpty) {
// Reset the selected community and space UUIDs if there's no query
_selectedSpaceUuid = null;
return widget.communities;
}
// Filter communities and expand only those that match the query
return widget.communities.where((community) {
final containsQueryInCommunity =
community.name.toLowerCase().contains(_searchQuery.toLowerCase());
final containsQueryInSpaces =
community.spaces.any((space) => _containsQuery(space, _searchQuery.toLowerCase()));
final containsQueryInSpaces = community.spaces.any((space) =>
_containsQuery(space: space, query: _searchQuery.toLowerCase()));
return containsQueryInCommunity || containsQueryInSpaces;
}).toList();
}
// Helper function to determine if any space or its children match the search query
bool _containsQuery(SpaceModel space, String query) {
bool _containsQuery({
required SpaceModel space,
required String query,
}) {
final matchesSpace = space.name.toLowerCase().contains(query);
final matchesChildren =
space.children.any((child) => _containsQuery(child, query)); // Recursive check for children
final matchesChildren = space.children.any(
(child) => _containsQuery(space: child, query: query),
);
// If the space or any of its children match the query, expand this space
if (matchesSpace || matchesChildren) {
_selectedSpaceUuid = space.uuid;
}
@ -83,79 +78,35 @@ class _SidebarWidgetState extends State<SidebarWidget> {
}
bool _isSpaceOrChildSelected(SpaceModel space) {
// Return true if the current space or any of its child spaces is selected
if (_selectedSpaceUuid == space.uuid) {
return true;
}
// Recursively check if any child spaces match the query
for (var child in space.children) {
if (_isSpaceOrChildSelected(child)) {
return true;
}
}
return false;
final isSpaceSelected = _selectedSpaceUuid == space.uuid;
final anySubSpaceIsSelected = space.children.any(_isSpaceOrChildSelected);
return isSpaceSelected || anySubSpaceIsSelected;
}
@override
Widget build(BuildContext context) {
final filteredCommunities = _filterCommunities();
final filteredCommunities = _filteredCommunities();
return Container(
width: 300,
decoration: subSectionContainerDecoration,
child: Column(
mainAxisSize: MainAxisSize.min, // Ensures the Column only takes necessary height
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Communities title with the add button
Container(
decoration: subSectionContainerDecoration,
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,
),
),
),
),
],
),
SidebarHeader(
existingCommunityNames:
widget.communities.map((community) => community.name).toList(),
),
// Search bar
CustomSearchBar(
onSearchChanged: (query) {
setState(() {
_searchQuery = query;
});
},
onSearchChanged: (query) => setState(() => _searchQuery = query),
),
const SizedBox(height: 16),
// Community list
Expanded(
child: ListView(
children: filteredCommunities.map((community) {
return _buildCommunityTile(context, community);
}).toList(),
children: filteredCommunities
.map((community) => _buildCommunityTile(context, community))
.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) {
bool hasChildren = community.spaces.isNotEmpty;
return CommunityTile(
title: community.name,
key: ValueKey(community.uuid),
@ -183,7 +123,7 @@ class _SidebarWidgetState extends State<SidebarWidget> {
onItemSelected: () {
setState(() {
_selectedId = community.uuid;
_selectedSpaceUuid = null; // Update the selected community
_selectedSpaceUuid = null;
});
context.read<CenterBodyBloc>().add(CommunitySelectedEvent());
@ -192,46 +132,51 @@ class _SidebarWidgetState extends State<SidebarWidget> {
SelectCommunityEvent(selectedCommunity: community),
);
},
onExpansionChanged: (String title, bool expanded) {
_handleExpansionChange(community.uuid, expanded);
},
children: hasChildren
? community.spaces
.where((space) => (space.status != SpaceStatus.deleted ||
space.status != SpaceStatus.parentDeleted))
.map((space) => _buildSpaceTile(space, community))
.toList()
: null,
onExpansionChanged: (title, expanded) {},
children: community.spaces
.where((space) {
final isDeleted = space.status != SpaceStatus.deleted;
final isParentDeleted = space.status != SpaceStatus.parentDeleted;
return (isDeleted || isParentDeleted);
})
.map((space) => _buildSpaceTile(space: space, community: community))
.toList(),
);
}
Widget _buildSpaceTile(SpaceModel space, CommunityModel community, {int depth = 1}) {
bool isExpandedSpace = _isSpaceOrChildSelected(space);
Widget _buildSpaceTile({
required SpaceModel space,
required CommunityModel community,
}) {
final spaceIsExpanded = _isSpaceOrChildSelected(space);
final isSelected = _selectedId == space.uuid;
return Padding(
padding: EdgeInsets.only(left: depth * 16.0),
child: SpaceTile(
title: space.name,
key: ValueKey(space.uuid),
isSelected: _selectedId == space.uuid,
initiallyExpanded: isExpandedSpace,
onExpansionChanged: (bool expanded) {
_handleExpansionChange(space.uuid ?? '', expanded);
},
onItemSelected: () {
setState(() {
_selectedId = space.uuid;
_selectedSpaceUuid = space.uuid;
});
padding: const EdgeInsetsDirectional.only(start: 16.0),
child: SpaceTile(
title: space.name,
key: ValueKey(space.uuid),
isSelected: isSelected,
initiallyExpanded: spaceIsExpanded,
onExpansionChanged: (expanded) {},
onItemSelected: () {
setState(() {
_selectedId = space.uuid;
_selectedSpaceUuid = space.uuid;
});
context.read<SpaceManagementBloc>().add(
SelectSpaceEvent(selectedCommunity: community, selectedSpace: space),
);
},
children: space.children.isNotEmpty
? space.children.map((childSpace) => _buildSpaceTile(childSpace, community)).toList()
: [], // Recursively render child spaces if available
));
context.read<SpaceManagementBloc>().add(
SelectSpaceEvent(selectedCommunity: community, selectedSpace: space),
);
},
children: space.children
.map(
(childSpace) => _buildSpaceTile(
space: childSpace,
community: community,
),
)
.toList(),
),
);
}
void _handleExpansionChange(String uuid, bool expanded) {}
}