mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 20:48:24 +00:00
Make search screen local-first
This commit is contained in:
+1
-1
@@ -24,7 +24,7 @@ void main() {
|
||||
print('initializing local storage at $path');
|
||||
return path;
|
||||
},
|
||||
clear: LocalStorageClearStrategy.always, // TODO whenError
|
||||
clear: LocalStorageClearStrategy.whenError,
|
||||
),
|
||||
)
|
||||
],
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user