diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 8e09a30..165c002 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -30,7 +30,7 @@ android { applicationId = "com.example.the_wallpaper_company" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. - minSdk = 23 + minSdk = flutter.minSdkVersion targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName @@ -60,4 +60,4 @@ dependencies { coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5") // Other dependencies for your project... -} \ No newline at end of file +} diff --git a/lib/features/home/presentation/screen/home_screen.dart b/lib/features/home/presentation/screen/home_screen.dart index 6c87e6e..0a9e1b9 100644 --- a/lib/features/home/presentation/screen/home_screen.dart +++ b/lib/features/home/presentation/screen/home_screen.dart @@ -12,6 +12,8 @@ import 'package:the_wallpaper_company/features/home/presentation/widgets/wallpap import 'package:the_wallpaper_company/features/home/provider/wallpaper_provider.dart'; import 'package:the_wallpaper_company/firebase_message.dart'; import '../widgets/category_carousel.dart'; +import '../widgets/search_bar_widget.dart'; +import '../widgets/no_results_widget.dart'; import 'package:the_wallpaper_company/core/constants.dart'; import 'package:the_wallpaper_company/features/home/presentation/widgets/wallpaper_tile.dart'; import 'package:pull_to_refresh_flutter3/pull_to_refresh_flutter3.dart'; @@ -121,60 +123,76 @@ class _HomeScreenState extends State { sigmaX: 12, sigmaY: 12, ), - child: CategoryCarousel( - categories: AppConstants.categories, - selectedCategory: - provider.selectedCategory, - onCategorySelected: - provider.selectCategory, + child: Column( + children: [ + const SearchBarWidget(), + CategoryCarousel( + categories: + AppConstants.categories, + selectedCategory: + provider.selectedCategory, + onCategorySelected: + provider.selectCategory, + ), + ], ), ), ), ), // expandedHeight: 72, - toolbarHeight: 150, + toolbarHeight: 220, ), - SliverPadding( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 8, - ), - sliver: SliverMasonryGrid.count( - crossAxisCount: 2, - mainAxisSpacing: 8, - crossAxisSpacing: 8, - childCount: - visibleWallpapers.length + - (provider.isPaginating ? 1 : 0), - itemBuilder: (context, index) { - if (provider.isPaginating && - index == visibleWallpapers.length) { - return const WallpaperShimmer(); - } - final wallpaper = - visibleWallpapers[index]; - return InkWell( - onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => - WallpaperScreen( - wallpapers: - visibleWallpapers, - initialIndex: index, + visibleWallpapers.isEmpty && + provider.searchQuery.isNotEmpty + ? SliverFillRemaining( + child: NoResultsWidget( + searchQuery: provider.searchQuery, + onClearSearch: provider.clearSearch, + ), + ) + : SliverPadding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 8, + ), + sliver: SliverMasonryGrid.count( + crossAxisCount: 2, + mainAxisSpacing: 8, + crossAxisSpacing: 8, + childCount: + visibleWallpapers.length + + (provider.isPaginating ? 1 : 0), + itemBuilder: (context, index) { + if (provider.isPaginating && + index == + visibleWallpapers + .length) { + return const WallpaperShimmer(); + } + final wallpaper = + visibleWallpapers[index]; + return InkWell( + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => + WallpaperScreen( + wallpapers: + visibleWallpapers, + initialIndex: index, + ), ), - ), - ); - }, - child: WallpaperTile( - imageUrl: wallpaper.imageUrl, - title: wallpaper.title, - id: wallpaper.id, + ); + }, + child: WallpaperTile( + imageUrl: wallpaper.imageUrl, + title: wallpaper.title, + id: wallpaper.id, + ), + ); + }, ), - ); - }, - ), - ), + ), ], ), ), diff --git a/lib/features/home/presentation/widgets/no_results_widget.dart b/lib/features/home/presentation/widgets/no_results_widget.dart new file mode 100644 index 0000000..f053e4d --- /dev/null +++ b/lib/features/home/presentation/widgets/no_results_widget.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; + +class NoResultsWidget extends StatelessWidget { + final String searchQuery; + final VoidCallback onClearSearch; + + const NoResultsWidget({ + super.key, + required this.searchQuery, + required this.onClearSearch, + }); + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsets.all(32.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.search_off, + size: 64, + color: Theme.of(context).brightness == Brightness.dark + ? Colors.grey[600] + : Colors.grey[400], + ), + const SizedBox(height: 16), + Text( + 'No wallpapers found', + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + color: Theme.of(context).brightness == Brightness.dark + ? Colors.grey[400] + : Colors.grey[600], + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + Text( + 'No results for "${searchQuery.trim()}"', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).brightness == Brightness.dark + ? Colors.grey[500] + : Colors.grey[500], + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + ElevatedButton.icon( + onPressed: onClearSearch, + icon: const Icon(Icons.clear), + label: const Text('Clear Search'), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(25), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/home/presentation/widgets/search_bar_widget.dart b/lib/features/home/presentation/widgets/search_bar_widget.dart new file mode 100644 index 0000000..fdd1d02 --- /dev/null +++ b/lib/features/home/presentation/widgets/search_bar_widget.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../../provider/wallpaper_provider.dart'; + +class SearchBarWidget extends StatefulWidget { + const SearchBarWidget({super.key}); + + @override + State createState() => _SearchBarWidgetState(); +} + +class _SearchBarWidgetState extends State { + final TextEditingController _searchController = TextEditingController(); + final FocusNode _focusNode = FocusNode(); + + @override + void initState() { + super.initState(); + final provider = Provider.of(context, listen: false); + _searchController.text = provider.searchQuery; + } + + @override + void dispose() { + _searchController.dispose(); + _focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, provider, child) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Theme.of(context).brightness == Brightness.dark + ? Colors.grey[800]?.withValues(alpha: 0.8) + : Colors.white.withValues(alpha: 0.8), + borderRadius: BorderRadius.circular(25), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: TextField( + controller: _searchController, + focusNode: _focusNode, + onChanged: (value) { + provider.updateSearchQuery(value); + }, + decoration: InputDecoration( + hintText: 'Search wallpapers by title or category...', + hintStyle: TextStyle( + color: Theme.of(context).brightness == Brightness.dark + ? Colors.grey[400] + : Colors.grey[600], + fontSize: 16, + ), + prefixIcon: Icon( + provider.searchQuery.isNotEmpty ? Icons.search : Icons.search, + color: provider.searchQuery.isNotEmpty + ? Colors.pinkAccent + : (Theme.of(context).brightness == Brightness.dark + ? Colors.grey[400] + : Colors.grey[600]), + size: 24, + ), + suffixIcon: provider.searchQuery.isNotEmpty + ? IconButton( + icon: Icon( + Icons.clear, + color: Theme.of(context).brightness == Brightness.dark + ? Colors.grey[400] + : Colors.grey[600], + size: 20, + ), + onPressed: () { + _searchController.clear(); + provider.clearSearch(); + _focusNode.unfocus(); + }, + ) + : null, + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 16, + ), + ), + style: TextStyle( + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white + : Colors.black87, + fontSize: 16, + ), + ), + ); + }, + ); + } +} diff --git a/lib/features/home/provider/wallpaper_provider.dart b/lib/features/home/provider/wallpaper_provider.dart index 67cdd90..01bd521 100644 --- a/lib/features/home/provider/wallpaper_provider.dart +++ b/lib/features/home/provider/wallpaper_provider.dart @@ -8,19 +8,38 @@ class WallpaperProvider extends ChangeNotifier { List _wallpapers = []; int _visibleCount = 10; bool _isPaginating = false; + String _searchQuery = ''; - List get wallpapers => _selectedCategory == 'All' - ? _wallpapers.take(_visibleCount).toList() - : _wallpapers - .where((w) => w.category == _selectedCategory) - .toList() - .take(_visibleCount) - .toList(); + List get wallpapers { + List filteredWallpapers = _wallpapers; + + // Apply search filter + if (_searchQuery.isNotEmpty) { + filteredWallpapers = _wallpapers.where((wallpaper) { + return wallpaper.title.toLowerCase().contains( + _searchQuery.toLowerCase(), + ) || + wallpaper.category.toLowerCase().contains( + _searchQuery.toLowerCase(), + ); + }).toList(); + } + + // Apply category filter + if (_selectedCategory != 'All') { + filteredWallpapers = filteredWallpapers + .where((w) => w.category == _selectedCategory) + .toList(); + } + + return filteredWallpapers.take(_visibleCount).toList(); + } void addWallpaper(Wallpaper wallpaper) { _wallpapers.add(wallpaper); notifyListeners(); } + bool _isLoading = false; bool get isLoading => _isLoading; bool get isPaginating => _isPaginating; @@ -29,6 +48,8 @@ class WallpaperProvider extends ChangeNotifier { String _selectedCategory = 'All'; String get selectedCategory => _selectedCategory; + String get searchQuery => _searchQuery; + Future fetchWallpapers() async { _isLoading = true; notifyListeners(); @@ -54,12 +75,49 @@ class WallpaperProvider extends ChangeNotifier { notifyListeners(); } + void updateSearchQuery(String query) { + _searchQuery = query; + _visibleCount = 10; // Reset visible count when searching + notifyListeners(); + } + + void clearSearch() { + _searchQuery = ''; + _visibleCount = 10; + notifyListeners(); + } + void loadMore() { - if (_visibleCount < _wallpapers.length && !_isPaginating) { + // Get the total count of filtered wallpapers + List filteredWallpapers = _wallpapers; + + // Apply search filter + if (_searchQuery.isNotEmpty) { + filteredWallpapers = _wallpapers.where((wallpaper) { + return wallpaper.title.toLowerCase().contains( + _searchQuery.toLowerCase(), + ) || + wallpaper.category.toLowerCase().contains( + _searchQuery.toLowerCase(), + ); + }).toList(); + } + + // Apply category filter + if (_selectedCategory != 'All') { + filteredWallpapers = filteredWallpapers + .where((w) => w.category == _selectedCategory) + .toList(); + } + + if (_visibleCount < filteredWallpapers.length && !_isPaginating) { _isPaginating = true; notifyListeners(); Future.delayed(const Duration(milliseconds: 500), () { - _visibleCount = (_visibleCount + 10).clamp(0, _wallpapers.length); + _visibleCount = (_visibleCount + 10).clamp( + 0, + filteredWallpapers.length, + ); _isPaginating = false; notifyListeners(); }); diff --git a/pubspec.lock b/pubspec.lock index a361883..2aba802 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -380,26 +380,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -745,10 +745,10 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.6" timezone: dependency: transitive description: @@ -785,10 +785,10 @@ packages: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: