diff --git a/lib/screens/stickers/sticker_marketplace.dart b/lib/screens/stickers/sticker_marketplace.dart index 35796791..313ffc2e 100644 --- a/lib/screens/stickers/sticker_marketplace.dart +++ b/lib/screens/stickers/sticker_marketplace.dart @@ -8,6 +8,8 @@ import 'package:island/models/sticker.dart'; import 'package:island/pods/network.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:material_symbols_icons/symbols.dart'; +import 'dart:async'; + import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; @@ -17,7 +19,10 @@ part 'sticker_marketplace.g.dart'; class MarketplaceStickerPacksNotifier extends _$MarketplaceStickerPacksNotifier with CursorPagingNotifierMixin { @override - Future> build({required bool byUsage}) { + Future> build({ + required String? query, + required bool byUsage, + }) { return fetch(cursor: null); } @@ -34,6 +39,7 @@ class MarketplaceStickerPacksNotifier extends _$MarketplaceStickerPacksNotifier 'offset': offset, 'take': 20, 'order': byUsage ? 'usage' : 'date', + if (query != null && query!.isNotEmpty) 'query': query, }, ); @@ -60,6 +66,25 @@ class MarketplaceStickersScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final byUsage = useState(true); + final query = useState(null); + final searchController = useTextEditingController(); + final focusNode = useFocusNode(); + final debounceTimer = useState(null); + + // Clear search when query is cleared + useEffect(() { + if (query.value == null || query.value!.isEmpty) { + searchController.clear(); + } + return null; + }, [query.value]); + + // Clean up timer on dispose + useEffect(() { + return () { + debounceTimer.value?.cancel(); + }; + }, []); return AppScaffold( appBar: AppBar( @@ -84,39 +109,89 @@ class MarketplaceStickersScreen extends HookConsumerWidget { body: PagingHelperView( provider: marketplaceStickerPacksNotifierProvider( byUsage: byUsage.value, + query: query.value, ), futureRefreshable: marketplaceStickerPacksNotifierProvider( byUsage: byUsage.value, + query: query.value, ).future, notifierRefreshable: marketplaceStickerPacksNotifierProvider( byUsage: byUsage.value, + query: query.value, ).notifier, contentBuilder: - (data, widgetCount, endItemView) => ListView.builder( - padding: EdgeInsets.zero, - itemCount: widgetCount, - itemBuilder: (context, index) { - if (index == widgetCount - 1) { - return endItemView; - } + (data, widgetCount, endItemView) => Column( + children: [ + // Search bar above the list + Padding( + padding: const EdgeInsets.all(16), + child: SearchBar( + elevation: WidgetStateProperty.all(4), + controller: searchController, + focusNode: focusNode, + hintText: 'search'.tr(), + leading: const Icon(Symbols.search), + padding: WidgetStateProperty.all( + const EdgeInsets.symmetric(horizontal: 24), + ), + onTapOutside: + (_) => FocusManager.instance.primaryFocus?.unfocus(), + trailing: [ + if (query.value != null && query.value!.isNotEmpty) + IconButton( + icon: const Icon(Symbols.close), + onPressed: () { + query.value = null; + searchController.clear(); + focusNode.unfocus(); + }, + ), + ], + onChanged: (value) { + // Debounce search to avoid excessive API calls + debounceTimer.value?.cancel(); + debounceTimer.value = Timer( + const Duration(milliseconds: 500), + () { + query.value = value.isEmpty ? null : value; + }, + ); + }, + onSubmitted: (value) { + query.value = value.isEmpty ? null : value; + focusNode.unfocus(); + }, + ), + ), + Expanded( + child: ListView.builder( + padding: EdgeInsets.zero, + itemCount: widgetCount, + itemBuilder: (context, index) { + if (index == widgetCount - 1) { + return endItemView; + } - final pack = data.items[index]; - return ListTile( - title: Text(pack.name), - subtitle: Text(pack.description), - trailing: const Icon(Symbols.chevron_right), - onTap: () { - // Navigate to user-facing sticker pack detail page. - // Adjust the route name/parameters if your app uses different ones. - context.pushNamed( - 'stickerPackDetail', - pathParameters: {'packId': pack.id}, - ); - }, - ); - }, + final pack = data.items[index]; + return ListTile( + title: Text(pack.name), + subtitle: Text(pack.description), + trailing: const Icon(Symbols.chevron_right), + onTap: () { + // Navigate to user-facing sticker pack detail page. + // Adjust the route name/parameters if your app uses different ones. + context.pushNamed( + 'stickerPackDetail', + pathParameters: {'packId': pack.id}, + ); + }, + ); + }, + ), + ), + ], ), ), ); diff --git a/lib/screens/stickers/sticker_marketplace.g.dart b/lib/screens/stickers/sticker_marketplace.g.dart index 9b06ac89..a569ae23 100644 --- a/lib/screens/stickers/sticker_marketplace.g.dart +++ b/lib/screens/stickers/sticker_marketplace.g.dart @@ -7,7 +7,7 @@ part of 'sticker_marketplace.dart'; // ************************************************************************** String _$marketplaceStickerPacksNotifierHash() => - r'7e985cdee651a2ae868c0acb15da2b7a10525ae3'; + r'711eafeadf488485521563d0831676c51772d13c'; /// Copied from Dart SDK class _SystemHash { @@ -32,9 +32,13 @@ class _SystemHash { abstract class _$MarketplaceStickerPacksNotifier extends BuildlessAutoDisposeAsyncNotifier> { + late final String? query; late final bool byUsage; - FutureOr> build({required bool byUsage}); + FutureOr> build({ + required String? query, + required bool byUsage, + }); } /// See also [MarketplaceStickerPacksNotifier]. @@ -49,15 +53,21 @@ class MarketplaceStickerPacksNotifierFamily const MarketplaceStickerPacksNotifierFamily(); /// See also [MarketplaceStickerPacksNotifier]. - MarketplaceStickerPacksNotifierProvider call({required bool byUsage}) { - return MarketplaceStickerPacksNotifierProvider(byUsage: byUsage); + MarketplaceStickerPacksNotifierProvider call({ + required String? query, + required bool byUsage, + }) { + return MarketplaceStickerPacksNotifierProvider( + query: query, + byUsage: byUsage, + ); } @override MarketplaceStickerPacksNotifierProvider getProviderOverride( covariant MarketplaceStickerPacksNotifierProvider provider, ) { - return call(byUsage: provider.byUsage); + return call(query: provider.query, byUsage: provider.byUsage); } static const Iterable? _dependencies = null; @@ -83,20 +93,26 @@ class MarketplaceStickerPacksNotifierProvider CursorPagingData > { /// See also [MarketplaceStickerPacksNotifier]. - MarketplaceStickerPacksNotifierProvider({required bool byUsage}) - : this._internal( - () => MarketplaceStickerPacksNotifier()..byUsage = byUsage, - from: marketplaceStickerPacksNotifierProvider, - name: r'marketplaceStickerPacksNotifierProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$marketplaceStickerPacksNotifierHash, - dependencies: MarketplaceStickerPacksNotifierFamily._dependencies, - allTransitiveDependencies: - MarketplaceStickerPacksNotifierFamily._allTransitiveDependencies, - byUsage: byUsage, - ); + MarketplaceStickerPacksNotifierProvider({ + required String? query, + required bool byUsage, + }) : this._internal( + () => + MarketplaceStickerPacksNotifier() + ..query = query + ..byUsage = byUsage, + from: marketplaceStickerPacksNotifierProvider, + name: r'marketplaceStickerPacksNotifierProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$marketplaceStickerPacksNotifierHash, + dependencies: MarketplaceStickerPacksNotifierFamily._dependencies, + allTransitiveDependencies: + MarketplaceStickerPacksNotifierFamily._allTransitiveDependencies, + query: query, + byUsage: byUsage, + ); MarketplaceStickerPacksNotifierProvider._internal( super._createNotifier, { @@ -105,16 +121,18 @@ class MarketplaceStickerPacksNotifierProvider required super.allTransitiveDependencies, required super.debugGetCreateSourceHash, required super.from, + required this.query, required this.byUsage, }) : super.internal(); + final String? query; final bool byUsage; @override FutureOr> runNotifierBuild( covariant MarketplaceStickerPacksNotifier notifier, ) { - return notifier.build(byUsage: byUsage); + return notifier.build(query: query, byUsage: byUsage); } @override @@ -122,12 +140,16 @@ class MarketplaceStickerPacksNotifierProvider return ProviderOverride( origin: this, override: MarketplaceStickerPacksNotifierProvider._internal( - () => create()..byUsage = byUsage, + () => + create() + ..query = query + ..byUsage = byUsage, from: from, name: null, dependencies: null, allTransitiveDependencies: null, debugGetCreateSourceHash: null, + query: query, byUsage: byUsage, ), ); @@ -145,12 +167,14 @@ class MarketplaceStickerPacksNotifierProvider @override bool operator ==(Object other) { return other is MarketplaceStickerPacksNotifierProvider && + other.query == query && other.byUsage == byUsage; } @override int get hashCode { var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, query.hashCode); hash = _SystemHash.combine(hash, byUsage.hashCode); return _SystemHash.finish(hash); @@ -161,6 +185,9 @@ class MarketplaceStickerPacksNotifierProvider // ignore: unused_element mixin MarketplaceStickerPacksNotifierRef on AutoDisposeAsyncNotifierProviderRef> { + /// The parameter `query` of this provider. + String? get query; + /// The parameter `byUsage` of this provider. bool get byUsage; } @@ -174,6 +201,9 @@ class _MarketplaceStickerPacksNotifierProviderElement with MarketplaceStickerPacksNotifierRef { _MarketplaceStickerPacksNotifierProviderElement(super.provider); + @override + String? get query => + (origin as MarketplaceStickerPacksNotifierProvider).query; @override bool get byUsage => (origin as MarketplaceStickerPacksNotifierProvider).byUsage;