Refactor HomeBloc and GarageDoorBloc event handling; update CreateNewRoutinesDialog to use SpaceTreeDropdown; add settings button SVG.

This commit is contained in:
mohammad
2025-06-09 22:55:00 +03:00
parent ad8e06ac40
commit 662fe211eb
7 changed files with 313 additions and 171 deletions

View File

@ -1,156 +1,157 @@
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/routines/create_new_routines/dropdown_menu_content.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_state.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class CommunityDropdown extends StatelessWidget {
final String? selectedValue;
final List<CommunityModel> communities;
final Function(String?) onChanged;
final TextEditingController _searchController = TextEditingController();
class SpaceTreeDropdown extends StatefulWidget {
final String? selectedSpaceId;
final Function(String?)? onChanged;
CommunityDropdown({
Key? key,
required this.selectedValue,
required this.onChanged,
required this.communities,
}) : super(key: key);
const SpaceTreeDropdown({
super.key,
this.selectedSpaceId,
this.onChanged,
});
@override
State<SpaceTreeDropdown> createState() => _SpaceTreeDropdownState();
}
class _SpaceTreeDropdownState extends State<SpaceTreeDropdown> {
late String? _selectedSpaceId;
final LayerLink _layerLink = LayerLink();
OverlayEntry? _overlayEntry;
@override
void initState() {
super.initState();
_selectedSpaceId = widget.selectedSpaceId;
}
@override
void dispose() {
_removeOverlay();
super.dispose();
}
void _removeOverlay() {
_overlayEntry?.remove();
_overlayEntry = null;
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(10.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Community",
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.w400,
fontSize: 13,
color: ColorsManager.blackColor,
),
),
const SizedBox(height: 8),
SizedBox(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
),
child: DropdownButton2<String>(
underline: const SizedBox(),
value: selectedValue,
items: communities.map((community) {
return DropdownMenuItem<String>(
value: community.uuid,
child: Text(
' ${community.name}',
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
);
}).toList(),
onChanged: onChanged,
style: const TextStyle(color: Colors.black),
hint: Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(
" Please Select",
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: ColorsManager.textGray,
),
),
return BlocBuilder<SpaceTreeBloc, SpaceTreeState>(
builder: (context, state) {
final communities = state.searchQuery.isNotEmpty
? state.filteredCommunity
: state.communityList;
final selectedCommunity = _findCommunity(communities, _selectedSpaceId);
return CompositedTransformTarget(
link: _layerLink,
child: GestureDetector(
onTap: _toggleDropdown,
child: Container(
height: 46,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(12),
),
customButton: Container(
height: 45,
decoration: BoxDecoration(
border: Border.all(color: ColorsManager.textGray, width: 1.0),
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
flex: 5,
child: Text(
selectedValue != null
? " ${communities.firstWhere((element) => element.uuid == selectedValue).name}"
: ' Please Select',
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: selectedValue != null
? Colors.black
: ColorsManager.textGray,
),
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Text(
selectedCommunity?.name ?? 'Select a space',
style: TextStyle(
color: selectedCommunity != null
? Colors.black
: Colors.grey,
overflow: TextOverflow.ellipsis,
),
),
Expanded(
child: Container(
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: const BorderRadius.only(
topRight: Radius.circular(10),
bottomRight: Radius.circular(10),
),
),
height: 45,
child: const Icon(
Icons.keyboard_arrow_down,
color: ColorsManager.textGray,
),
),
),
],
),
),
dropdownStyleData: DropdownStyleData(
maxHeight: MediaQuery.of(context).size.height * 0.4,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
),
),
dropdownSearchData: DropdownSearchData(
searchController: _searchController,
searchInnerWidgetHeight: 50,
searchInnerWidget: Container(
height: 50,
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: TextFormField(
style: const TextStyle(color: Colors.black),
controller: _searchController,
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 12,
),
hintText: 'Search for community...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
fontWeight: FontWeight.w400,
fontSize: 15,
),
),
),
),
searchMatchFn: (item, searchValue) {
final communityName =
(item.child as Text).data?.toLowerCase() ?? '';
return communityName
.contains(searchValue.toLowerCase().trim());
},
),
onMenuStateChange: (isOpen) {
if (!isOpen) {
_searchController.clear();
}
},
menuItemStyleData: const MenuItemStyleData(
height: 40,
Container(
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: const BorderRadius.only(
topRight: Radius.circular(10),
bottomRight: Radius.circular(10),
),
),
height: 45,
width: 35,
child: const Icon(
Icons.keyboard_arrow_down,
color: ColorsManager.textGray,
),
),
],
),
),
))
],
),
),
);
},
);
}
void _toggleDropdown() {
if (_overlayEntry != null) {
_removeOverlay();
return;
}
_overlayEntry = OverlayEntry(
builder: (context) => Positioned(
width: 300,
child: CompositedTransformFollower(
link: _layerLink,
showWhenUnlinked: false,
offset: const Offset(0, 48),
child: Material(
elevation: 8,
borderRadius: BorderRadius.circular(12),
child: DropdownMenuContent(
selectedSpaceId: _selectedSpaceId,
onChanged: (id) {
if (id != null && mounted) {
setState(() => _selectedSpaceId = id);
widget.onChanged?.call(id);
_removeOverlay();
}
},
onClose: _removeOverlay,
),
),
),
),
);
Overlay.of(context).insert(_overlayEntry!);
}
CommunityModel? _findCommunity(
List<CommunityModel> communities, String? communityId) {
if (communityId == null) return null;
try {
return communities.firstWhere((c) => c.uuid == communityId);
} catch (e) {
return CommunityModel(
uuid: '',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
name: '',
description: '',
spaces: []);
}
}
}

View File

@ -18,6 +18,7 @@ class CreateNewRoutinesDialog extends StatefulWidget {
class _CreateNewRoutinesDialogState extends State<CreateNewRoutinesDialog> {
String? _selectedCommunity;
String? _selectedSpace;
String? _selectedId;
@override
Widget build(BuildContext context) {
@ -40,7 +41,10 @@ class _CreateNewRoutinesDialogState extends State<CreateNewRoutinesDialog> {
spaceHint = 'Select Space';
}
}
if (_selectedId != null && _selectedCommunity != _selectedId) {
_selectedSpace = null;
_selectedCommunity = _selectedId;
}
return AlertDialog(
backgroundColor: Colors.white,
insetPadding: EdgeInsets.zero,
@ -61,25 +65,17 @@ class _CreateNewRoutinesDialogState extends State<CreateNewRoutinesDialog> {
children: [
const Divider(),
Padding(
padding: const EdgeInsets.only(left: 15, right: 15),
child: CommunityDropdown(
communities: _bloc.communities..sort(
(a, b) => a.name.toLowerCase().compareTo(
b.name.toLowerCase(),
),
),
selectedValue: _selectedCommunity,
onChanged: (String? newValue) {
setState(() {
_selectedCommunity = newValue;
_selectedSpace = null;
});
if (newValue != null) {
_bloc.add(SpaceOnlyWithDevicesEvent(newValue));
}
},
),
),
padding: const EdgeInsets.only(left: 15, right: 15),
child: SpaceTreeDropdown(
selectedSpaceId: _selectedId,
onChanged: (String? newValue) {
setState(() => _selectedId = newValue!);
if (_selectedId != null) {
_bloc.add(
SpaceOnlyWithDevicesEvent(_selectedId!));
}
},
)),
const SizedBox(height: 5),
Padding(
padding: const EdgeInsets.only(left: 15, right: 15),

View File

@ -0,0 +1,148 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_state.dart';
class DropdownMenuContent extends StatefulWidget {
final String? selectedSpaceId;
final ValueChanged<String?> onChanged;
final VoidCallback onClose;
const DropdownMenuContent({
required this.selectedSpaceId,
required this.onChanged,
required this.onClose,
});
@override
State<DropdownMenuContent> createState() => _DropdownMenuContentState();
}
class _DropdownMenuContentState extends State<DropdownMenuContent> {
final ScrollController _scrollController = ScrollController();
final TextEditingController _searchController = TextEditingController();
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
}
@override
void dispose() {
_scrollController.dispose();
_searchController.dispose();
super.dispose();
}
void _onScroll() {
final bloc = context.read<SpaceTreeBloc>();
final state = bloc.state;
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 30) {
if (state is SpaceTreeState && !state.paginationIsLoading) {
bloc.add(PaginationEvent(state.paginationModel, state.communityList));
}
}
}
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 300),
child: BlocBuilder<SpaceTreeBloc, SpaceTreeState>(
builder: (context, state) {
final communities = state.searchQuery.isNotEmpty
? state.filteredCommunity
: state.communityList;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// Search bar
Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
controller: _searchController,
onChanged: (query) {
context.read<SpaceTreeBloc>().add(SearchQueryEvent(query));
},
style: const TextStyle(fontSize: 14, color: Colors.black),
decoration: InputDecoration(
hintText: 'Search for space...',
prefixIcon: const Icon(Icons.search, size: 20),
contentPadding:
const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
isDense: true,
),
),
),
// Community list
Expanded(
child: ListView.builder(
controller: _scrollController,
itemCount:
communities.length + (state.paginationIsLoading ? 1 : 0),
itemBuilder: (context, index) {
if (index >= communities.length) {
return state.paginationIsLoading
? const Padding(
padding: EdgeInsets.all(8.0),
child: Center(
child: SizedBox(
width: 20,
height: 20,
child:
CircularProgressIndicator(strokeWidth: 2),
),
),
)
: const SizedBox.shrink();
}
final community = communities[index];
final isSelected = community.uuid == widget.selectedSpaceId;
return ListTile(
title: Text(
community.name,
style: TextStyle(
color: isSelected ? Colors.blue : Colors.black,
fontWeight:
isSelected ? FontWeight.bold : FontWeight.normal,
),
),
onTap: () {
setState(() {
_searchController.clear();
_searchController.text.isEmpty
? context
.read<SpaceTreeBloc>()
.add(SearchQueryEvent(''))
: context.read<SpaceTreeBloc>().add(
SearchQueryEvent(_searchController.text));
});
// Future.delayed(const Duration(seconds: 1), () {
widget.onChanged(community.uuid);
widget.onClose();
// });
},
);
},
),
),
],
);
},
),
);
}
}