diff --git a/lib/controllers/rooms_controller.dart b/lib/controllers/rooms_controller.dart index f7dd29cf..cd793a41 100644 --- a/lib/controllers/rooms_controller.dart +++ b/lib/controllers/rooms_controller.dart @@ -23,11 +23,15 @@ class RoomsController extends GetxController { final Databases databases = AppwriteService.getDatabases(); RxList rooms = [].obs; final ThemeController themeController = Get.find(); + RxBool isSearching = false.obs; + RxBool searchBarIsEmpty = true.obs; + RxList filteredRooms = [].obs; @override void onInit() async { super.onInit(); await getRooms(); + filteredRooms.value = rooms; } Future createRoomObject(Document room, String userUid) async { @@ -82,6 +86,7 @@ class RoomsController extends GetxController { } } update(); + updateFilteredRooms(); } catch (e) { log(e.toString()); } finally { @@ -142,4 +147,37 @@ class RoomsController extends GetxController { Get.back(); } } + + Future searchLiveRooms(String query) async { + if (query.isEmpty) { + filteredRooms.value = rooms; + searchBarIsEmpty.value = true; + return; + } + isSearching.value = true; + searchBarIsEmpty.value = false; + try { + final lowerQuery = query.toLowerCase(); + filteredRooms.value = rooms.where((room) { + return room.name.toLowerCase().contains(lowerQuery) || + room.description.toLowerCase().contains(lowerQuery); + }).toList(); + } catch (e) { + log('Error searching rooms: $e'); + filteredRooms.value = []; + } finally { + isSearching.value = false; + } + } + + void clearLiveSearch() { + filteredRooms.value = rooms; + searchBarIsEmpty.value = true; + } + + void updateFilteredRooms() { + if (searchBarIsEmpty.value) { + filteredRooms.value = rooms; + } + } } diff --git a/lib/controllers/upcomming_rooms_controller.dart b/lib/controllers/upcomming_rooms_controller.dart index 6350aae7..7da4ddea 100644 --- a/lib/controllers/upcomming_rooms_controller.dart +++ b/lib/controllers/upcomming_rooms_controller.dart @@ -36,6 +36,9 @@ class UpcomingRoomsController extends GetxController { late String localTimeZoneName; late bool isOffsetNegetive; Rx isLoading = false.obs; + RxBool searchBarIsEmpty = true.obs; + RxList filteredUpcomingRooms = + [].obs; late DateTime currentTimeInstance; final Map monthMap = { "1": "Jan", @@ -185,6 +188,9 @@ class UpcomingRoomsController extends GetxController { await fetchUpcomingRoomDetails(upcomingRoom); upcomingRooms.add(appwriteUpcomingRoom); } + if (searchBarIsEmpty.value) { + filteredUpcomingRooms.value = upcomingRooms; + } } catch (e) { log(e.toString()); } finally { @@ -205,8 +211,6 @@ class UpcomingRoomsController extends GetxController { // Delete UpcomingRoom as it is now a room await deleteUpcomingRoom(upcomingRoomId); - - await getUpcomingRooms(); } Future createUpcomingRoom() async { @@ -346,4 +350,30 @@ class UpcomingRoomsController extends GetxController { isDismissible: false, ); } + + Future searchUpcomingRooms(String query) async { + if (query.isEmpty) { + filteredUpcomingRooms.value = upcomingRooms; + searchBarIsEmpty.value = true; + return; + } + + searchBarIsEmpty.value = false; + + try { + final lowerQuery = query.toLowerCase(); + filteredUpcomingRooms.value = upcomingRooms.where((room) { + return room.name.toLowerCase().contains(lowerQuery) || + room.description.toLowerCase().contains(lowerQuery); + }).toList(); + } catch (e) { + log('Error searching upcoming rooms: $e'); + filteredUpcomingRooms.value = []; + } + } + + void clearUpcomingSearch() { + filteredUpcomingRooms.value = upcomingRooms; + searchBarIsEmpty.value = true; + } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 78fbbe9b..591e7829 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,4 +1,5 @@ { + "@@locale": "en", "title": "Resonate", "@title": { "description": "The title of the application." @@ -859,6 +860,38 @@ "@noSearchResults": { "description": "Message displayed when a search returns no results." }, + "searchRooms": "Search rooms...", + "@searchRooms": { + "description": "Placeholder text for room search input field." + }, + "searchingRooms": "Searching rooms...", + "@searchingRooms": { + "description": "Loading message shown while searching for rooms." + }, + "clearSearch": "Clear search", + "@clearSearch": { + "description": "Text for button to clear search results." + }, + "searchError": "Search Error", + "@searchError": { + "description": "Title for search error messages." + }, + "searchRoomsError": "Failed to search rooms. Please try again.", + "@searchRoomsError": { + "description": "Error message when room search fails." + }, + "searchUpcomingRoomsError": "Failed to search upcoming rooms. Please try again.", + "@searchUpcomingRoomsError": { + "description": "Error message when upcoming room search fails." + }, + "search": "Search", + "@search": { + "description": "Tooltip text for search button." + }, + "clear": "Clear", + "@clear": { + "description": "Tooltip text for clear button." + }, "shareRoomMessage": "🚀 Check out this amazing room: {roomName}!\n\n📖 Description: {description}\n👥 Join {participants} participants now!", "@shareRoomMessage": { "description": "The default message template used when sharing a room.", diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index 21715ec3..47789d89 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -860,6 +860,38 @@ "@noSearchResults": { "description": "Message displayed when a search returns no results." }, + "searchRooms": "रूम खोजें...", + "@searchRooms": { + "description": "Placeholder text for room search input field." + }, + "searchingRooms": "रूम खोजे जा रहे हैं...", + "@searchingRooms": { + "description": "Loading message shown while searching for rooms." + }, + "clearSearch": "खोज साफ़ करें", + "@clearSearch": { + "description": "Text for button to clear search results." + }, + "searchError": "खोज त्रुटि", + "@searchError": { + "description": "Title for search error messages." + }, + "searchRoomsError": "कमरों की खोज असफल हुई। कृपया दोबारा कोशिश करें।", + "@searchRoomsError": { + "description": "Error message when room search fails." + }, + "searchUpcomingRoomsError": "आगामी कमरों की खोज असफल हुई। कृपया दोबारा कोशिश करें।", + "@searchUpcomingRoomsError": { + "description": "Error message when upcoming room search fails." + }, + "search": "खोजें", + "@search": { + "description": "Tooltip text for search button." + }, + "clear": "साफ़ करें", + "@clear": { + "description": "Tooltip text for clear button." + }, "shareRoomMessage": "🚀 इस शानदार रूम को देखें: {roomName}!\n\n📖 विवरण: {description}\n👥 अभी {participants} लोग जुड़ चुके हैं!", "@shareRoomMessage": { "description": "The default message template used when sharing a room.", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index aeae1daf..111d9f8c 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1354,6 +1354,54 @@ abstract class AppLocalizations { /// **'No Search Results'** String get noSearchResults; + /// Placeholder text for room search input field. + /// + /// In en, this message translates to: + /// **'Search rooms...'** + String get searchRooms; + + /// Loading message shown while searching for rooms. + /// + /// In en, this message translates to: + /// **'Searching rooms...'** + String get searchingRooms; + + /// Text for button to clear search results. + /// + /// In en, this message translates to: + /// **'Clear search'** + String get clearSearch; + + /// Title for search error messages. + /// + /// In en, this message translates to: + /// **'Search Error'** + String get searchError; + + /// Error message when room search fails. + /// + /// In en, this message translates to: + /// **'Failed to search rooms. Please try again.'** + String get searchRoomsError; + + /// Error message when upcoming room search fails. + /// + /// In en, this message translates to: + /// **'Failed to search upcoming rooms. Please try again.'** + String get searchUpcomingRoomsError; + + /// Tooltip text for search button. + /// + /// In en, this message translates to: + /// **'Search'** + String get search; + + /// Tooltip text for clear button. + /// + /// In en, this message translates to: + /// **'Clear'** + String get clear; + /// The default message template used when sharing a room. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 09f04eac..db335760 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -663,6 +663,31 @@ class AppLocalizationsEn extends AppLocalizations { @override String get noSearchResults => 'No Search Results'; + @override + String get searchRooms => 'Search rooms...'; + + @override + String get searchingRooms => 'Searching rooms...'; + + @override + String get clearSearch => 'Clear search'; + + @override + String get searchError => 'Search Error'; + + @override + String get searchRoomsError => 'Failed to search rooms. Please try again.'; + + @override + String get searchUpcomingRoomsError => + 'Failed to search upcoming rooms. Please try again.'; + + @override + String get search => 'Search'; + + @override + String get clear => 'Clear'; + @override String shareRoomMessage( String roomName, diff --git a/lib/l10n/app_localizations_gu.dart b/lib/l10n/app_localizations_gu.dart index 1c859dab..4d24be93 100644 --- a/lib/l10n/app_localizations_gu.dart +++ b/lib/l10n/app_localizations_gu.dart @@ -663,6 +663,31 @@ class AppLocalizationsGu extends AppLocalizations { @override String get noSearchResults => 'કોઈ શોધ પરિણામો નથી'; + @override + String get searchRooms => 'Search rooms...'; + + @override + String get searchingRooms => 'Searching rooms...'; + + @override + String get clearSearch => 'Clear search'; + + @override + String get searchError => 'Search Error'; + + @override + String get searchRoomsError => 'Failed to search rooms. Please try again.'; + + @override + String get searchUpcomingRoomsError => + 'Failed to search upcoming rooms. Please try again.'; + + @override + String get search => 'Search'; + + @override + String get clear => 'Clear'; + @override String shareRoomMessage( String roomName, diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index 3d869f7b..69273b7c 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -665,6 +665,32 @@ class AppLocalizationsHi extends AppLocalizations { @override String get noSearchResults => 'कोई रिज़ल्ट नहीं मिला'; + @override + String get searchRooms => 'रूम खोजें...'; + + @override + String get searchingRooms => 'रूम खोजे जा रहे हैं...'; + + @override + String get clearSearch => 'खोज साफ़ करें'; + + @override + String get searchError => 'खोज त्रुटि'; + + @override + String get searchRoomsError => + 'कमरों की खोज असफल हुई। कृपया दोबारा कोशिश करें।'; + + @override + String get searchUpcomingRoomsError => + 'आगामी कमरों की खोज असफल हुई। कृपया दोबारा कोशिश करें।'; + + @override + String get search => 'खोजें'; + + @override + String get clear => 'साफ़ करें'; + @override String shareRoomMessage( String roomName, diff --git a/lib/views/screens/home_screen.dart b/lib/views/screens/home_screen.dart index e48130c9..0b678b85 100644 --- a/lib/views/screens/home_screen.dart +++ b/lib/views/screens/home_screen.dart @@ -8,8 +8,10 @@ import 'package:resonate/models/appwrite_upcomming_room.dart'; import 'package:resonate/utils/enums/room_state.dart'; import 'package:resonate/views/screens/no_room_screen.dart'; import 'package:resonate/views/widgets/live_room_tile.dart'; +import 'package:resonate/views/widgets/search_rooms.dart'; import 'package:resonate/views/widgets/upcomming_room_tile.dart'; import 'package:resonate/l10n/app_localizations.dart'; +import 'package:resonate/utils/ui_sizes.dart'; bool isLiveSelected = true; @@ -25,6 +27,7 @@ class _HomeScreenState extends State { UpcomingRoomsController(), ); final RoomsController roomsController = Get.put(RoomsController()); + bool _showSearchOverlay = false; Future pullToRefreshData() async { await upcomingRoomsController.getUpcomingRooms(); @@ -35,49 +38,85 @@ class _HomeScreenState extends State { Widget build(BuildContext context) { return Scaffold( body: SafeArea( - child: Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CustomAppBarLiveRoom( - isLiveSelected: isLiveSelected, - onTabSelected: (bool selectedTab) { - setState(() { - isLiveSelected = selectedTab; - }); - }, - ), - const SizedBox(height: 5), - Expanded( - child: RefreshIndicator( - onRefresh: pullToRefreshData, - child: Obx( - () => (isLiveSelected - ? roomsController.isLoading.value - ? Center( - child: - LoadingAnimationWidget.fourRotatingDots( - color: Theme.of( - context, - ).colorScheme.primary, - size: Get.pixelRatio * 20, - ), - ) - : LiveRoomListView() - : upcomingRoomsController.isLoading.value - ? Center( - child: LoadingAnimationWidget.fourRotatingDots( - color: Theme.of(context).colorScheme.primary, - size: Get.pixelRatio * 20, - ), - ) - : UpcomingRoomsListView()), + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomAppBarLiveRoom( + isLiveSelected: isLiveSelected, + onTabSelected: (bool selectedTab) { + setState(() { + isLiveSelected = selectedTab; + _showSearchOverlay = false; + }); + roomsController.clearLiveSearch(); + upcomingRoomsController.clearUpcomingSearch(); + }, + onSearchTapped: () { + setState(() { + _showSearchOverlay = true; + }); + }, ), - ), + SizedBox(height: UiSizes.height_16), + Expanded( + child: RefreshIndicator( + onRefresh: pullToRefreshData, + child: Obx( + () => (isLiveSelected + ? roomsController.isLoading.value + ? Center( + child: + LoadingAnimationWidget.fourRotatingDots( + color: Theme.of( + context, + ).colorScheme.primary, + size: Get.pixelRatio * 20, + ), + ) + : LiveRoomListView() + : upcomingRoomsController.isLoading.value + ? Center( + child: LoadingAnimationWidget.fourRotatingDots( + color: Theme.of(context).colorScheme.primary, + size: Get.pixelRatio * 20, + ), + ) + : UpcomingRoomsListView()), + ), + ), + ), + ], ), - ], - ), + ), + // Search bar + SearchOverlay( + isVisible: _showSearchOverlay, + onSearchChanged: (query) { + if (isLiveSelected) { + roomsController.searchLiveRooms(query); + } else { + upcomingRoomsController.searchUpcomingRooms(query); + } + }, + onClose: () { + setState(() { + _showSearchOverlay = false; + }); + if (isLiveSelected) { + roomsController.clearLiveSearch(); + } else { + upcomingRoomsController.clearUpcomingSearch(); + } + }, + isSearching: isLiveSelected + ? roomsController.isSearching.value + : false, + ), + ], ), ), ); @@ -89,8 +128,10 @@ class CustomAppBarLiveRoom extends StatelessWidget { super.key, required this.isLiveSelected, required this.onTabSelected, + required this.onSearchTapped, }); final Function(bool) onTabSelected; + final VoidCallback onSearchTapped; final bool isLiveSelected; @override Widget build(BuildContext context) { @@ -109,7 +150,7 @@ class CustomAppBarLiveRoom extends StatelessWidget { ), ), ), - const SizedBox(width: 25), + SizedBox(width: UiSizes.width_25), GestureDetector( onTap: () => onTabSelected(false), child: Text( @@ -122,6 +163,16 @@ class CustomAppBarLiveRoom extends StatelessWidget { ), ), ), + const Spacer(), + IconButton( + onPressed: onSearchTapped, + icon: Icon( + Icons.search, + color: Theme.of(context).colorScheme.primary, + size: UiSizes.size_24, + ), + tooltip: AppLocalizations.of(context)!.search, + ), ], ); } @@ -132,26 +183,70 @@ class UpcomingRoomsListView extends StatelessWidget { final UpcomingRoomsController upcomingRoomsController = Get.put( UpcomingRoomsController(), ); - + final RoomsController roomsController = Get.find(); @override Widget build(BuildContext context) { - return Obx( - () => upcomingRoomsController.upcomingRooms.isNotEmpty - ? ListView.builder( - itemCount: upcomingRoomsController.upcomingRooms.length, - physics: const BouncingScrollPhysics(), - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: UpCommingListTile( - appwriteUpcommingRoom: - upcomingRoomsController.upcomingRooms[index], - ), - ); - }, - ) - : const NoRoomScreen(isRoom: false), - ); + return Obx(() { + final roomsToShow = + upcomingRoomsController.filteredUpcomingRooms.isNotEmpty || + !upcomingRoomsController.searchBarIsEmpty.value + ? upcomingRoomsController.filteredUpcomingRooms + : upcomingRoomsController.upcomingRooms; + + if (roomsToShow.isNotEmpty) { + return ListView.builder( + itemCount: roomsToShow.length, + physics: const BouncingScrollPhysics(), + itemBuilder: (context, index) { + return Padding( + padding: EdgeInsets.symmetric(vertical: UiSizes.height_8), + child: UpCommingListTile( + appwriteUpcommingRoom: roomsToShow[index], + ), + ); + }, + ); + } else { + return upcomingRoomsController.upcomingRooms.isEmpty && + upcomingRoomsController.searchBarIsEmpty.value + ? const NoRoomScreen(isRoom: false) + : Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.search_off, + size: UiSizes.size_65, + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.3), + ), + SizedBox(height: UiSizes.height_16), + Text( + AppLocalizations.of(context)!.noSearchResults, + style: TextStyle( + fontSize: UiSizes.size_16, + fontWeight: FontWeight.w500, + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.7), + ), + ), + SizedBox(height: UiSizes.height_8), + Text( + AppLocalizations.of(context)!.clearSearch, + style: TextStyle( + fontSize: UiSizes.size_14, + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.5), + ), + ), + ], + ), + ); + } + }); } } @@ -161,22 +256,85 @@ class LiveRoomListView extends StatelessWidget { @override Widget build(BuildContext context) { - return Obx( - () => roomsController.rooms.isNotEmpty - ? ListView.builder( - physics: const BouncingScrollPhysics(), - itemCount: roomsController.rooms.length, - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: CustomLiveRoomTile( - appwriteRoom: roomsController.rooms[index], - ), - ); - }, - ) - : const NoRoomScreen(isRoom: true), - ); + return Obx(() { + final roomsToShow = roomsController.searchBarIsEmpty.value + ? roomsController.rooms + : roomsController.filteredRooms; + + if (roomsController.isSearching.value) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + LoadingAnimationWidget.fourRotatingDots( + color: Theme.of(context).colorScheme.primary, + size: UiSizes.size_20, + ), + SizedBox(height: UiSizes.height_16), + Text( + AppLocalizations.of(context)!.searchingRooms, + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.7), + ), + ), + ], + ), + ); + } + + if (roomsToShow.isNotEmpty) { + return ListView.builder( + physics: const BouncingScrollPhysics(), + itemCount: roomsToShow.length, + itemBuilder: (context, index) { + return Padding( + padding: EdgeInsets.symmetric(vertical: UiSizes.height_8), + child: CustomLiveRoomTile(appwriteRoom: roomsToShow[index]), + ); + }, + ); + } else { + return roomsController.searchBarIsEmpty.value + ? const NoRoomScreen(isRoom: true) + : Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.search_off, + size: UiSizes.size_65, + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.3), + ), + SizedBox(height: UiSizes.height_16), + Text( + AppLocalizations.of(context)!.noSearchResults, + style: TextStyle( + fontSize: UiSizes.size_16, + fontWeight: FontWeight.w500, + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.7), + ), + ), + SizedBox(height: UiSizes.height_8), + Text( + AppLocalizations.of(context)!.clearSearch, + style: TextStyle( + fontSize: UiSizes.size_14, + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.5), + ), + ), + ], + ), + ); + } + }); } } diff --git a/lib/views/widgets/search_rooms.dart b/lib/views/widgets/search_rooms.dart new file mode 100644 index 00000000..846fef7d --- /dev/null +++ b/lib/views/widgets/search_rooms.dart @@ -0,0 +1,206 @@ +import 'package:flutter/material.dart'; +import 'package:resonate/l10n/app_localizations.dart'; +import 'package:resonate/utils/ui_sizes.dart'; + +class SearchOverlay extends StatefulWidget { + final Function(String) onSearchChanged; + final VoidCallback onClose; + final bool isSearching; + final bool isVisible; + + const SearchOverlay({ + super.key, + required this.onSearchChanged, + required this.onClose, + this.isSearching = false, + required this.isVisible, + }); + + @override + State createState() => _SearchOverlayState(); +} + +class _SearchOverlayState extends State + with SingleTickerProviderStateMixin { + late TextEditingController _searchController; + late AnimationController _animationController; + late Animation _slideAnimation; + FocusNode? _focusNode; + bool _hasText = false; + + @override + void initState() { + super.initState(); + _searchController = TextEditingController(); + _focusNode = FocusNode(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + + _slideAnimation = Tween(begin: -1.0, end: 0.0).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeOutCubic), + ); + + _searchController.addListener(() { + setState(() { + _hasText = _searchController.text.isNotEmpty; + }); + widget.onSearchChanged(_searchController.text); + }); + + if (widget.isVisible) { + _animationController.forward(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _focusNode?.requestFocus(); + }); + } + } + + @override + void didUpdateWidget(SearchOverlay oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.isVisible != oldWidget.isVisible) { + if (widget.isVisible) { + _animationController.forward(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _focusNode?.requestFocus(); + }); + } else { + _focusNode?.unfocus(); + _animationController.reverse(); + _searchController.clear(); + _hasText = false; + } + } + } + + @override + void dispose() { + _searchController.dispose(); + _focusNode?.dispose(); + _animationController.dispose(); + super.dispose(); + } + + void _clearSearch() { + _searchController.clear(); + } + + void _closeOverlay() { + _focusNode?.unfocus(); + _animationController.reverse().then((_) { + widget.onClose(); + }); + } + + @override + Widget build(BuildContext context) { + if (!widget.isVisible) { + return const SizedBox.shrink(); + } + + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) { + if (!didPop) { + _closeOverlay(); + } + }, + child: AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return Positioned( + top: 0, + left: 0, + right: 0, + child: Transform.translate( + offset: Offset(0, _slideAnimation.value * 100), + child: Container( + margin: EdgeInsets.all(UiSizes.size_16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(UiSizes.size_12), + color: Theme.of(context).colorScheme.surface, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + IconButton( + onPressed: _closeOverlay, + icon: Icon( + Icons.arrow_back, + color: Theme.of(context).colorScheme.primary, + ), + ), + Expanded( + child: TextField( + controller: _searchController, + focusNode: _focusNode, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + decoration: InputDecoration( + hintText: AppLocalizations.of(context)!.searchRooms, + hintStyle: TextStyle( + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.6), + ), + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric( + horizontal: UiSizes.width_8, + vertical: UiSizes.height_12, + ), + ), + onChanged: (value) { + widget.onSearchChanged(value); + }, + onSubmitted: (value) { + _focusNode?.unfocus(); + }, + ), + ), + if (widget.isSearching) + Padding( + padding: EdgeInsets.symmetric( + horizontal: UiSizes.width_10, + ), + child: SizedBox( + width: UiSizes.width_16, + height: UiSizes.height_16, + child: CircularProgressIndicator( + strokeWidth: UiSizes.width_2, + color: Theme.of(context).colorScheme.primary, + ), + ), + ) + else if (_hasText) + IconButton( + onPressed: _clearSearch, + icon: Icon( + Icons.clear, + size: UiSizes.size_20, + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.6), + ), + tooltip: AppLocalizations.of(context)!.clear, + ) + else + SizedBox(width: UiSizes.width_56), + ], + ), + ), + ), + ); + }, + ), + ); + } +}