Make search screen local-first

This commit is contained in:
franzap
2024-05-02 13:24:24 -03:00
parent d881560d33
commit e6bbe11a3c
6 changed files with 127 additions and 51 deletions
+1 -1
View File
@@ -24,7 +24,7 @@ void main() {
print('initializing local storage at $path');
return path;
},
clear: LocalStorageClearStrategy.always, // TODO whenError
clear: LocalStorageClearStrategy.whenError,
),
)
],
+2 -1
View File
@@ -70,8 +70,9 @@ mixin NostrAdapter<T extends ZapstoreEvent<T>> on Adapter<T> {
return findAllLocal();
}
final additionalKinds = params?.remove('kinds');
final req = RelayRequest(
kinds: {kind, ...?params?.remove('kinds')},
kinds: {kind, ...?additionalKinds},
tags: params ?? {},
);
print(req);
+3
View File
@@ -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: [
+103 -42
View File
@@ -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<List<App>>>((_) => 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<String?>((ref) => null);
final searchStateProvider = StateNotifierProvider.autoDispose<
DataStateNotifier<List<App>>, DataState<List<App>>>((ref) {
final n = DataStateNotifier(
data: DataState<List<App>>(
[],
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: <App>[], 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;
});
+17 -6
View File
@@ -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,
),
],
);
}
}
+1 -1
View File
@@ -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),