diff --git a/lib/main.dart b/lib/main.dart index b82a32d..63fbc06 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -24,7 +24,7 @@ void main() { print('initializing local storage at $path'); return path; }, - clear: LocalStorageClearStrategy.always, // TODO whenError + clear: LocalStorageClearStrategy.whenError, ), ) ], diff --git a/lib/models/nostr_adapter.dart b/lib/models/nostr_adapter.dart index 72aa8cd..ba7d2d7 100644 --- a/lib/models/nostr_adapter.dart +++ b/lib/models/nostr_adapter.dart @@ -70,8 +70,9 @@ mixin NostrAdapter> on Adapter { return findAllLocal(); } + final additionalKinds = params?.remove('kinds'); final req = RelayRequest( - kinds: {kind, ...?params?.remove('kinds')}, + kinds: {kind, ...?additionalKinds}, tags: params ?? {}, ); print(req); diff --git a/lib/screens/app_detail_screen.dart b/lib/screens/app_detail_screen.dart index 5bdaa45..f84c2f6 100644 --- a/lib/screens/app_detail_screen.dart +++ b/lib/screens/app_detail_screen.dart @@ -17,6 +17,7 @@ class AppDetailScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final scrollController = ScrollController(); return Column( children: [ Expanded( @@ -69,9 +70,11 @@ class AppDetailScreen extends HookConsumerWidget { Gap(16), if ((app.tagMap['image'] ?? []).isNotEmpty) Scrollbar( + controller: scrollController, interactive: true, trackVisibility: true, child: SingleChildScrollView( + controller: scrollController, scrollDirection: Axis.horizontal, child: Row( children: [ diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index a2034e9..d219f68 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_data/flutter_data.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:zapstore/main.data.dart'; @@ -9,17 +10,12 @@ import 'package:zapstore/widgets/user_avatar.dart'; const kAndroidMimeType = 'application/vnd.android.package-archive'; -final searchStateProvider = - StateProvider>>((_) => DataState([])); - -final screenInfoProvider = StateProvider( - (ref) => 'Welcome to zap.store!\nUse the search bar to find apps'); - class SearchScreen extends HookConsumerWidget { const SearchScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { + final controller = useTextEditingController(); final state = ref.watch(searchStateProvider); return Column( @@ -33,6 +29,7 @@ class SearchScreen extends HookConsumerWidget { Gap(20), Expanded( child: SearchBar( + controller: controller, shape: MaterialStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular(28), @@ -46,54 +43,42 @@ class SearchScreen extends HookConsumerWidget { size: 20, ), ), + trailing: [ + if (state.isLoading) + Padding( + padding: const EdgeInsets.only(right: 10), + child: SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 3), + ), + ), + if (controller.text.isNotEmpty && !state.isLoading) + IconButton( + hoverColor: Colors.transparent, + onPressed: controller.clear, + icon: Icon(Icons.close), + ), + ], hintText: 'Search for apps', hintStyle: MaterialStateProperty.all( TextStyle(color: Colors.grey[600])), autoFocus: true, - elevation: MaterialStatePropertyAll(2.2), + elevation: MaterialStateProperty.all(2.2), onSubmitted: (query) async { - ref.read(screenInfoProvider.notifier).state = - 'No apps found for "$query"'; - ref.read(searchStateProvider.notifier).state = - DataState([], isLoading: true); - final apps = - await ref.apps.findAll(params: {'search': query}); - - // load all signers and developers - final userIds = { - for (final app in apps) app.signer.id, - for (final app in apps) app.developer.id - }; - await ref.users.findAll(params: {'ids': userIds}); - - await ref.releases.findAll( - params: {'#i': apps.map((a) => a.identifier).toSet()}); - - // await ref.fileMetadata.findAll( - // params: { - // 'ids': releases.map((r) => r.tagMap['e']!.first).toSet(), - // '#m': [kAndroidMimeType], - // }, - // ); - ref.read(searchStateProvider.notifier).state = - DataState(apps); + ref.read(searchQueryProvider.notifier).state = query; }, ), ), ], ), Gap(10), - if (state.model.isEmpty || state.isLoading) - Expanded( - child: Center( - child: state.isLoading - ? CircularProgressIndicator() - : Text( - ref.read(screenInfoProvider), - textAlign: TextAlign.center, - ), - ), + if (state.hasMessage) + Text( + state.message!, + textAlign: TextAlign.center, ), + if (state.hasException) Text(state.exception!.toString()), if (state.hasModel) Expanded( child: ListView.builder( @@ -109,3 +94,79 @@ class SearchScreen extends HookConsumerWidget { ); } } + +final searchQueryProvider = StateProvider((ref) => null); + +final searchStateProvider = StateNotifierProvider.autoDispose< + DataStateNotifier>, DataState>>((ref) { + final n = DataStateNotifier( + data: DataState>( + [], + isLoading: false, + message: 'Welcome to zap.store!\n\nUse the search bar to find apps', + )); + + update() { + final query = ref.read(searchQueryProvider); + if (query != null) { + if (query.length < 3) { + return n.updateWith( + isLoading: false, message: 'Please provide a longer search term'); + } + n.updateWith(model: [], isLoading: true, message: null); + final r = RegExp(query, caseSensitive: false); + final apps = ref.apps + .findAllLocal() + .where((app) => + (app.url ?? '').contains(r) || + app.name.contains(r) || + app.content.contains(r) || + app.tags.any((e) => e.contains(r))) + .toList(); + n.updateWith(model: apps, isLoading: false); + } + } + + final sub = ref.listen(searchQueryProvider, (_, query) async { + if (query != null) { + update(); + try { + n.updateWith(isLoading: true); + + final apps = await ref.apps.findAll(params: {'search': query}); + + if (apps.isEmpty && n.data.model.isEmpty) { + n.updateWith(isLoading: false, message: 'No apps for term: $query'); + return; + } + + // load all signers and developers + final userIds = { + for (final app in apps) app.signer.id, + for (final app in apps) app.developer.id + }; + await ref.users.findAll(params: {'ids': userIds}); + + final ids = apps.map((a) => a.identifier).toSet(); + if (ids.isNotEmpty) { + await ref.releases.findAll(params: {'#i': ids}); + } + n.updateWith(isLoading: false); + } catch (e) { + n.updateWith(isLoading: false); + } + } + }); + + final dispose = ref.apps.watchAllNotifier().addListener((state) { + update(); + }); + + ref.onDispose(() { + sub.close(); + dispose(); + print('disposing the thing'); + }); + + return n; +}); diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 125d430..9803987 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter_data/flutter_data.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:zapstore/main.data.dart'; class SettingsScreen extends HookConsumerWidget { const SettingsScreen({super.key}); @@ -8,12 +10,21 @@ class SettingsScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final controller = useTextEditingController(); - return Expanded( - child: TextField( - controller: controller, - autofocus: true, - maxLines: 10, - ), + return Column( + children: [ + ElevatedButton( + onPressed: () async { + ref.apps.logLevel = 2; + await ref.apps.clearLocal(); + }, + child: Text('clear all'), + ), + TextField( + controller: controller, + autofocus: true, + maxLines: 10, + ), + ], ); } } diff --git a/lib/widgets/card.dart b/lib/widgets/card.dart index 2fbc19c..3186064 100644 --- a/lib/widgets/card.dart +++ b/lib/widgets/card.dart @@ -16,7 +16,7 @@ class CardWidget extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return Card( - margin: EdgeInsets.only(top: 20), + margin: EdgeInsets.only(top: 8, bottom: 8), elevation: 0, child: GestureDetector( onTap: () => context.go('/details', extra: app),