This commit is contained in:
franzap
2024-02-07 21:55:39 +00:00
parent b48a3095a6
commit b74e54f40a
19 changed files with 507 additions and 531 deletions
+1 -1
View File
@@ -1 +1 @@
/home/admin/fvm/versions/3.16.0
/home/admin/fvm/versions/3.16.9
+1 -1
View File
@@ -1,4 +1,4 @@
{
"flutterSdkVersion": "3.16.0",
"flutterSdkVersion": "3.16.9",
"flavors": {}
}
+7 -14
View File
@@ -1,16 +1,9 @@
# zapstore
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
- test on macos
- drop file
- compute sha256 and query nostr by `x`
- install button
- downloads file (to whwatever the downloads provider is)
- checks sha256 with crypto and updates statenotifier bool state
- display if matches (green) or not (red)
-78
View File
@@ -1,78 +0,0 @@
import 'dart:async';
import 'package:dart_nostr/dart_nostr.dart';
import 'package:flutter_data/flutter_data.dart';
import 'package:zapstore/models/base_event.dart';
mixin NostrAdapter<T extends BaseEvent<T>> on RemoteAdapter<T> {
late final Nostr nostr;
@override
Future<void> onInitialized() async {
nostr = await ref.read(nostrProvider.future);
super.onInitialized();
}
void update(List<NostrFilter> filters) async {
final req = NostrRequest(filters: filters);
await Nostr.instance.relaysService.startEventsSubscriptionAsync(
request: req,
timeout: Duration(seconds: 10),
);
}
@override
DataStateNotifier<List<T>> watchAllNotifier(
{bool? remote,
Map<String, dynamic>? params,
Map<String, String>? headers,
bool? syncLocal,
String? finder,
DataRequestLabel? label}) {
late final StreamSubscription _sub;
_sub = Nostr.instance.relaysService.streamsController.events
.listen((NostrEvent event) {
final baseEvent = localAdapter.deserialize({
'id': event.id,
'kind': event.kind,
'content': event.content,
'sig': event.sig,
'pubkey': event.pubkey,
'created_at': event.createdAt.toIso8601String(),
'tags': event.tags,
});
baseEvent.saveLocal();
});
ref.onDispose(() {
print('disposing nostr sub');
_sub.cancel();
});
return super
.watchAllNotifier(
remote: remote,
params: params,
headers: headers,
syncLocal: syncLocal,
finder: finder,
label: label)
.where((e) {
final search = params?['search'];
if (search != null && search.isNotEmpty) {
return e.content.contains(search);
}
return true;
});
}
}
final nostrProvider = FutureProvider<Nostr>((Ref ref) async {
final relays = ['wss://relay.nostr.band'];
print('***** initing nostr ********');
await Nostr.instance.relaysService.init(
relaysUrl: relays,
);
Nostr.instance.utilsService.utils.disableLogs();
return Nostr.instance;
});
+11 -2
View File
@@ -3,6 +3,7 @@ import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:flutter_data/flutter_data.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:ndk/ndk.dart';
import 'package:zapstore/main.data.dart';
import 'package:zapstore/screens/profile_screen.dart';
import 'package:zapstore/screens/search_screen.dart';
@@ -12,7 +13,8 @@ void main() {
runApp(
ProviderScope(
overrides: [
configureRepositoryLocalStorage(clear: LocalStorageClearStrategy.never),
configureRepositoryLocalStorage(
clear: LocalStorageClearStrategy.always),
],
child: const ZapStoreApp(),
),
@@ -31,12 +33,19 @@ void main() {
const borderColor = Color(0xFF805306);
final newInitializer = FutureProvider<void>((ref) async {
await ref.read(repositoryInitializerProvider.future);
await ref
.read(frameProvider.notifier)
.initialize({'wss://relay.nostr.band', 'wss://relay.damus.io'});
});
class ZapStoreApp extends HookConsumerWidget {
const ZapStoreApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final initializer = ref.watch(repositoryInitializerProvider);
final initializer = ref.watch(newInitializer);
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData.dark(),
+3 -13
View File
@@ -8,8 +8,6 @@ import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:path_provider/path_provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:zapstore/models/metadata.dart';
import 'package:zapstore/models/note.dart';
import 'package:zapstore/models/user.dart';
// ignore: prefer_function_declarations_over_variables
@@ -31,18 +29,14 @@ ConfigureRepositoryLocalStorage configureRepositoryLocalStorage = ({FutureFn<Str
};
final repositoryProviders = <String, Provider<Repository<DataModelMixin>>>{
'fileMetadata': fileMetadataRepositoryProvider,
'notes': notesRepositoryProvider,
'users': usersRepositoryProvider
'users': usersRepositoryProvider
};
final repositoryInitializerProvider =
FutureProvider<RepositoryInitializer>((ref) async {
DataHelpers.setInternalType<FileMetadata>('fileMetadata');
DataHelpers.setInternalType<Note>('notes');
DataHelpers.setInternalType<User>('users');
final adapters = <String, RemoteAdapter>{'fileMetadata': ref.watch(internalFileMetadataRemoteAdapterProvider), 'notes': ref.watch(internalNotesRemoteAdapterProvider), 'users': ref.watch(internalUsersRemoteAdapterProvider)};
final remotes = <String, bool>{'fileMetadata': true, 'notes': true, 'users': true};
final adapters = <String, RemoteAdapter>{'users': ref.watch(internalUsersRemoteAdapterProvider)};
final remotes = <String, bool>{'users': true};
await ref.watch(graphNotifierProvider).initialize();
@@ -60,14 +54,10 @@ final repositoryInitializerProvider =
return RepositoryInitializer();
});
extension RepositoryWidgetRefX on WidgetRef {
Repository<FileMetadata> get fileMetadata => watch(fileMetadataRepositoryProvider)..remoteAdapter.internalWatch = watch;
Repository<Note> get notes => watch(notesRepositoryProvider)..remoteAdapter.internalWatch = watch;
Repository<User> get users => watch(usersRepositoryProvider)..remoteAdapter.internalWatch = watch;
}
extension RepositoryRefX on Ref {
Repository<FileMetadata> get fileMetadata => watch(fileMetadataRepositoryProvider)..remoteAdapter.internalWatch = watch as Watcher;
Repository<Note> get notes => watch(notesRepositoryProvider)..remoteAdapter.internalWatch = watch as Watcher;
Repository<User> get users => watch(usersRepositoryProvider)..remoteAdapter.internalWatch = watch as Watcher;
}
-43
View File
@@ -1,43 +0,0 @@
import 'package:collection/collection.dart';
import 'package:dart_nostr/dart_nostr.dart';
import 'package:flutter_data/flutter_data.dart';
import 'package:json_annotation/json_annotation.dart';
abstract class BaseEvent<T extends BaseEvent<T>> extends DataModel<T> {
@override
final String id;
final NostrEvent _event;
String get pubkey => _event.pubkey;
int get kind => _event.kind;
String get content => _event.content;
String get sig => _event.sig;
@JsonKey(name: 'created_at')
DateTime get createdAt => _event.createdAt;
List<List<String>> get tags => _event.tags;
Map<String, List<String>> get tagMap {
final f2 = _event.tags.groupFoldBy<String, List<String>>(
(e) => e.first, (acc, e) => [...?acc, e[1]]);
return f2;
}
BaseEvent(
{required this.id,
required String pubkey,
required DateTime createdAt,
required int kind,
List<List<String>> tags = const [],
required String content,
required String sig})
: _event = NostrEvent(
id: id,
pubkey: pubkey,
createdAt: createdAt,
kind: kind,
tags: tags,
content: content,
sig: sig,
ots: null,
);
}
-44
View File
@@ -1,44 +0,0 @@
import 'package:dart_nostr/dart_nostr.dart';
import 'package:flutter_data/flutter_data.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:zapstore/adapters/nostr_adapter.dart';
import 'package:zapstore/models/base_event.dart';
part 'metadata.g.dart';
@JsonSerializable()
@DataRepository([NostrAdapter])
class FileMetadata extends BaseEvent<FileMetadata> {
String get url => tagMap['url']!.first;
String get mimeType => tagMap['m']!.first;
String get sha256 => tagMap['x']!.first;
FileMetadata(
{required super.id,
required super.pubkey,
required super.createdAt,
required super.content,
required super.sig,
super.tags})
: super(kind: 1063);
}
class FileMetadataFilter extends NostrFilter {
final List<String>? m;
FileMetadataFilter(
{super.ids,
super.authors,
super.e,
super.p,
this.m,
super.since,
super.until,
super.limit,
super.search})
: super(kinds: [1063]);
@override
Map<String, dynamic> toMap() {
return {'#m': m, ...super.toMap()};
}
}
-82
View File
@@ -1,82 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'metadata.dart';
// **************************************************************************
// RepositoryGenerator
// **************************************************************************
// ignore_for_file: non_constant_identifier_names, duplicate_ignore
mixin $FileMetadataLocalAdapter on LocalAdapter<FileMetadata> {
static final Map<String, RelationshipMeta> _kFileMetadataRelationshipMetas =
{};
@override
Map<String, RelationshipMeta> get relationshipMetas =>
_kFileMetadataRelationshipMetas;
@override
FileMetadata deserialize(map) {
map = transformDeserialize(map);
return _$FileMetadataFromJson(map);
}
@override
Map<String, dynamic> serialize(model, {bool withRelationships = true}) {
final map = _$FileMetadataToJson(model);
return transformSerialize(map, withRelationships: withRelationships);
}
}
final _fileMetadataFinders = <String, dynamic>{};
// ignore: must_be_immutable
class $FileMetadataHiveLocalAdapter = HiveLocalAdapter<FileMetadata>
with $FileMetadataLocalAdapter;
class $FileMetadataRemoteAdapter = RemoteAdapter<FileMetadata>
with NostrAdapter<FileMetadata>;
final internalFileMetadataRemoteAdapterProvider =
Provider<RemoteAdapter<FileMetadata>>((ref) => $FileMetadataRemoteAdapter(
$FileMetadataHiveLocalAdapter(ref),
InternalHolder(_fileMetadataFinders)));
final fileMetadataRepositoryProvider =
Provider<Repository<FileMetadata>>((ref) => Repository<FileMetadata>(ref));
extension FileMetadataDataRepositoryX on Repository<FileMetadata> {
NostrAdapter<FileMetadata> get nostrAdapter =>
remoteAdapter as NostrAdapter<FileMetadata>;
}
extension FileMetadataRelationshipGraphNodeX
on RelationshipGraphNode<FileMetadata> {}
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
FileMetadata _$FileMetadataFromJson(Map<String, dynamic> json) => FileMetadata(
id: json['id'] as String,
pubkey: json['pubkey'] as String,
createdAt: DateTime.parse(json['created_at'] as String),
content: json['content'] as String,
sig: json['sig'] as String,
tags: (json['tags'] as List<dynamic>?)
?.map(
(e) => (e as List<dynamic>).map((e) => e as String).toList())
.toList() ??
const [],
);
Map<String, dynamic> _$FileMetadataToJson(FileMetadata instance) =>
<String, dynamic>{
'id': instance.id,
'pubkey': instance.pubkey,
'content': instance.content,
'sig': instance.sig,
'created_at': instance.createdAt.toIso8601String(),
'tags': instance.tags,
};
-19
View File
@@ -1,19 +0,0 @@
import 'package:flutter_data/flutter_data.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:zapstore/adapters/nostr_adapter.dart';
import 'package:zapstore/models/base_event.dart';
part 'note.g.dart';
@JsonSerializable()
@DataRepository([NostrAdapter])
class Note extends BaseEvent<Note> {
Note(
{required super.id,
required super.pubkey,
required super.createdAt,
required super.content,
required super.sig,
super.tags})
: super(kind: 1);
}
-75
View File
@@ -1,75 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'note.dart';
// **************************************************************************
// RepositoryGenerator
// **************************************************************************
// ignore_for_file: non_constant_identifier_names, duplicate_ignore
mixin $NoteLocalAdapter on LocalAdapter<Note> {
static final Map<String, RelationshipMeta> _kNoteRelationshipMetas = {};
@override
Map<String, RelationshipMeta> get relationshipMetas =>
_kNoteRelationshipMetas;
@override
Note deserialize(map) {
map = transformDeserialize(map);
return _$NoteFromJson(map);
}
@override
Map<String, dynamic> serialize(model, {bool withRelationships = true}) {
final map = _$NoteToJson(model);
return transformSerialize(map, withRelationships: withRelationships);
}
}
final _notesFinders = <String, dynamic>{};
// ignore: must_be_immutable
class $NoteHiveLocalAdapter = HiveLocalAdapter<Note> with $NoteLocalAdapter;
class $NoteRemoteAdapter = RemoteAdapter<Note> with NostrAdapter<Note>;
final internalNotesRemoteAdapterProvider = Provider<RemoteAdapter<Note>>(
(ref) => $NoteRemoteAdapter(
$NoteHiveLocalAdapter(ref), InternalHolder(_notesFinders)));
final notesRepositoryProvider =
Provider<Repository<Note>>((ref) => Repository<Note>(ref));
extension NoteDataRepositoryX on Repository<Note> {
NostrAdapter<Note> get nostrAdapter => remoteAdapter as NostrAdapter<Note>;
}
extension NoteRelationshipGraphNodeX on RelationshipGraphNode<Note> {}
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Note _$NoteFromJson(Map<String, dynamic> json) => Note(
id: json['id'] as String,
pubkey: json['pubkey'] as String,
createdAt: DateTime.parse(json['created_at'] as String),
content: json['content'] as String,
sig: json['sig'] as String,
tags: (json['tags'] as List<dynamic>?)
?.map(
(e) => (e as List<dynamic>).map((e) => e as String).toList())
.toList() ??
const [],
);
Map<String, dynamic> _$NoteToJson(Note instance) => <String, dynamic>{
'id': instance.id,
'pubkey': instance.pubkey,
'content': instance.content,
'sig': instance.sig,
'created_at': instance.createdAt.toIso8601String(),
'tags': instance.tags,
};
+35 -4
View File
@@ -1,13 +1,44 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_data/flutter_data.dart';
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart';
@JsonSerializable()
@DataRepository([])
class User extends DataModel<User> {
@DataRepository([UserAdapter])
class User extends DataModel<User> with EquatableMixin {
final String id;
final String name;
String? name;
String? pictureUrl;
String? nip05;
@DataRelationship(inverse: 'followers')
final HasMany<User> following;
@DataRelationship(inverse: 'following')
final HasMany<User> followers;
User({required this.id, required this.name});
User({required this.id, HasMany<User>? following, HasMany<User>? followers})
: this.following = following ?? HasMany(),
this.followers = followers ?? HasMany();
@override
List<Object?> get props => [id];
}
mixin UserAdapter on RemoteAdapter<User> {
@override
DataStateNotifier<List<User>> watchAllNotifier(
{bool? remote,
Map<String, dynamic>? params,
Map<String, String>? headers,
bool? syncLocal,
String? finder,
DataRequestLabel? label}) {
return super.watchAllNotifier(
remote: remote,
params: params,
headers: headers,
syncLocal: syncLocal,
finder: finder,
label: label);
}
}
+49 -6
View File
@@ -9,7 +9,22 @@ part of 'user.dart';
// ignore_for_file: non_constant_identifier_names, duplicate_ignore
mixin $UserLocalAdapter on LocalAdapter<User> {
static final Map<String, RelationshipMeta> _kUserRelationshipMetas = {};
static final Map<String, RelationshipMeta> _kUserRelationshipMetas = {
'following': RelationshipMeta<User>(
name: 'following',
inverseName: 'followers',
type: 'users',
kind: 'HasMany',
instance: (_) => (_ as User).following,
),
'followers': RelationshipMeta<User>(
name: 'followers',
inverseName: 'following',
type: 'users',
kind: 'HasMany',
instance: (_) => (_ as User).followers,
)
};
@override
Map<String, RelationshipMeta> get relationshipMetas =>
@@ -33,7 +48,7 @@ final _usersFinders = <String, dynamic>{};
// ignore: must_be_immutable
class $UserHiveLocalAdapter = HiveLocalAdapter<User> with $UserLocalAdapter;
class $UserRemoteAdapter = RemoteAdapter<User> with NothingMixin;
class $UserRemoteAdapter = RemoteAdapter<User> with UserAdapter;
final internalUsersRemoteAdapterProvider = Provider<RemoteAdapter<User>>(
(ref) => $UserRemoteAdapter(
@@ -42,9 +57,25 @@ final internalUsersRemoteAdapterProvider = Provider<RemoteAdapter<User>>(
final usersRepositoryProvider =
Provider<Repository<User>>((ref) => Repository<User>(ref));
extension UserDataRepositoryX on Repository<User> {}
extension UserDataRepositoryX on Repository<User> {
UserAdapter get userAdapter => remoteAdapter as UserAdapter;
}
extension UserRelationshipGraphNodeX on RelationshipGraphNode<User> {}
extension UserRelationshipGraphNodeX on RelationshipGraphNode<User> {
RelationshipGraphNode<User> get following {
final meta = $UserLocalAdapter._kUserRelationshipMetas['following']
as RelationshipMeta<User>;
return meta.clone(
parent: this is RelationshipMeta ? this as RelationshipMeta : null);
}
RelationshipGraphNode<User> get followers {
final meta = $UserLocalAdapter._kUserRelationshipMetas['followers']
as RelationshipMeta<User>;
return meta.clone(
parent: this is RelationshipMeta ? this as RelationshipMeta : null);
}
}
// **************************************************************************
// JsonSerializableGenerator
@@ -52,10 +83,22 @@ extension UserRelationshipGraphNodeX on RelationshipGraphNode<User> {}
User _$UserFromJson(Map<String, dynamic> json) => User(
id: json['id'] as String,
name: json['name'] as String,
);
following: json['following'] == null
? null
: HasMany<User>.fromJson(json['following'] as Map<String, dynamic>),
followers: json['followers'] == null
? null
: HasMany<User>.fromJson(json['followers'] as Map<String, dynamic>),
)
..name = json['name'] as String?
..pictureUrl = json['pictureUrl'] as String?
..nip05 = json['nip05'] as String?;
Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
'pictureUrl': instance.pictureUrl,
'nip05': instance.nip05,
'following': instance.following,
'followers': instance.followers,
};
+154 -21
View File
@@ -1,34 +1,167 @@
import 'dart:async';
import 'dart:convert';
import 'package:bech32/bech32.dart';
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:ndk/ndk.dart';
import 'package:zapstore/main.data.dart';
import 'package:zapstore/models/user.dart';
import 'package:convert/convert.dart';
final profileProvider =
FutureProvider.family<User, String>((ref, pubkey) async {
final completer = Completer<User>();
final notifier = ref.read(frameProvider.notifier);
notifier.send(jsonEncode([
"REQ",
pubkey,
{
'authors': [pubkey],
'kinds': [0, 3],
}
]));
late Function _sub;
User? u;
List<String>? contacts;
_sub = ref.watch(frameProvider.notifier).addListener((frame) async {
// print('listener: ${frame.event}');
final event = frame.event;
if (event is Metadata) {
u = (await ref.users.findOne(event.pubkey)) ?? User(id: event.pubkey);
final map = jsonDecode(event.content);
u!.name = map['displayName'] ?? map['display_name'] ?? map['name'];
u!.nip05 = map['nip05'];
}
if (event is ContactList) {
contacts = [...?contacts, ...event.tagMap['p']!];
}
if (u != null && contacts != null) {
if (completer.isCompleted == false) {
print('completing with $u');
contacts!.forEach((c) {
final uc = User(id: c);
u!.following.add(uc);
uc.saveLocal();
});
u!.saveLocal();
completer.complete(u);
}
}
});
ref.onDispose(() {
print('disposing');
_sub();
});
return completer.future;
});
final loggedInUser = StateProvider<User?>((ref) => null);
final downloadsPath = StateProvider<String>((ref) => '/tmp');
class ProfileScreen extends HookConsumerWidget {
const ProfileScreen({super.key});
ProfileScreen({super.key});
final codec = Bech32Codec();
@override
Widget build(BuildContext context, WidgetRef ref) {
final watcher = ref.notes.watchAll(remote: false);
final u2 = ref.watch(loggedInUser);
final controller = useTextEditingController();
final controller2 = useTextEditingController(text: ref.read(downloadsPath));
return Expanded(
child: Column(
children: [
TextButton(
onPressed: () => {},
child: Text('press me'),
),
switch (watcher) {
DataState(isLoading: false, exception: null, model: final value) =>
Text(value.length.toString()),
_ => Text('loading')
}
],
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
children: [
TextField(controller: controller),
SizedBox(height: 10),
ElevatedButton(
onPressed: () async {
final pubkey = bech32Decode(controller.text);
final uid = await ref.read(profileProvider(pubkey).future);
ref.read(loggedInUser.notifier).state =
await ref.users.findOne(uid, remote: false);
},
child: Text('Log in with npub')),
if (u2 != null)
Expanded(
child: Column(
children: [
Text('Logged in as ${u2.name}'),
Text('(following: ${u2.following.length})'),
],
),
),
SizedBox(height: 20),
// ListView.builder(
// shrinkWrap: true,
// itemCount: contacts.length,
// itemBuilder: (context, i) {
// return Text(' - ${contacts[i].id}');
// }),
TextField(controller: controller2),
SizedBox(height: 10),
ElevatedButton(
onPressed: () async {
ref.read(downloadsPath.notifier).state = controller2.text;
},
child: Text('Save downloads path'),
),
],
),
),
);
// return Column(
// children: [
// Text(
// 'profile, ${Nip19.decodePubkey('npub1wf4pufsucer5va8g9p0rj5dnhvfeh6d8w0g6eayaep5dhps6rsgs43dgh9')}'),
// Text(value.isLoading ? 'loading' : value.value!.join('\n')),
// ],
// );
}
}
String bech32Encode(String prefix, String hexData) {
final data = hex.decode(hexData);
final convertedData = convertBits(data, 8, 5, true);
final bech32Data = Bech32(prefix, convertedData);
return bech32.encode(bech32Data);
}
String bech32Decode(String bech32Data) {
final decodedData = bech32.decode(bech32Data);
final convertedData = convertBits(decodedData.data, 5, 8, false);
final hexData = hex.encode(convertedData);
return hexData;
}
List<int> convertBits(List<int> data, int fromBits, int toBits, bool pad) {
var acc = 0;
var bits = 0;
final maxv = (1 << toBits) - 1;
final result = <int>[];
for (final value in data) {
if (value < 0 || value >> fromBits != 0) {
throw Exception('Invalid value: $value');
}
acc = (acc << fromBits) | value;
bits += fromBits;
while (bits >= toBits) {
bits -= toBits;
result.add((acc >> bits) & maxv);
}
}
if (pad) {
if (bits > 0) {
result.add((acc << (toBits - bits)) & maxv);
}
} else if (bits >= fromBits || ((acc << (toBits - bits)) & maxv) != 0) {
throw Exception('Invalid data');
}
return result;
}
+185 -52
View File
@@ -1,74 +1,207 @@
import 'package:dart_nostr/dart_nostr.dart';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:crypto/crypto.dart';
import 'package:desktop_drop/desktop_drop.dart';
import 'package:expansion_tile_card/expansion_tile_card.dart';
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:ndk/ndk.dart';
import 'package:zapstore/main.data.dart';
import 'package:zapstore/models/note.dart';
import 'package:zapstore/models/user.dart';
import 'package:zapstore/screens/profile_screen.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as path;
class SearchScreen extends HookConsumerWidget {
const SearchScreen({super.key});
SearchScreen({super.key});
final r = Random();
@override
Widget build(BuildContext context, WidgetRef ref) {
final searchState = useState<String?>(null);
final watcher = ref.notes
.watchAll(remote: false, params: {'search': searchState.value});
final subscriptionIdState = useState<String?>(null);
final value =
ref.watch(subscriptionFrameProvider(subscriptionIdState.value));
return Expanded(
child: switch (watcher) {
DataState(isLoading: false, exception: null, model: final value) =>
Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
children: [
SearchBar(
elevation: MaterialStatePropertyAll(2.2),
onSubmitted: (query) {
searchState.value = query;
if (query.isNotEmpty) {
ref.notes.nostrAdapter.update(
[
NostrFilter(
search: query,
since: DateTime.now().subtract(Duration(days: 1)),
limit: 10,
)
],
);
child: DropTarget(
onDragDone: (detail) async {
if (detail.files.isNotEmpty) {
final bytes = await detail.files.first.readAsBytes();
final digest = sha256.convert(bytes).toString();
subscriptionIdState.value = 'sub-${digest}';
final notifier = ref.read(frameProvider.notifier);
notifier.send(jsonEncode([
"REQ",
subscriptionIdState.value!,
{
'kinds': [1063],
'#x': [digest],
}
]));
}
// print(detail.files.map((e) => e.path));
},
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
children: [
SearchBar(
elevation: MaterialStatePropertyAll(2.2),
onSubmitted: (query) {
searchState.value = query;
subscriptionIdState.value = 'sub-${r.nextInt(9999999)}';
final notifier = ref.read(frameProvider.notifier);
notifier.send(jsonEncode([
"REQ",
subscriptionIdState.value!,
{
'kinds': [1063],
'limit': 20,
'search': query,
// '#m': ['video/mp4'],
// 'since': DateTime.now()
// .subtract(Duration(days: 1))
// .millisecondsSinceEpoch ~/
// 1000,
}
]));
},
),
Padding(
padding: const EdgeInsets.all(6.0),
child: Text('Total results: ${value.length}'),
),
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: value.length,
itemBuilder: (context, index) {
final event = value[index];
return CardWidget(note: event as FileMetadata);
},
),
Padding(
padding: const EdgeInsets.all(6.0),
child: Text(
'Total results: ${value.length}, search term: ${searchState.value}'),
),
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: value.length,
itemBuilder: (context, index) {
final note = value[index];
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Text(note.pubkey),
Text(note.content),
// Image.network(note.),
],
),
);
},
),
],
),
),
),
);
}
}
class CardWidget extends HookConsumerWidget {
final FileMetadata note;
const CardWidget({super.key, required this.note});
@override
Widget build(BuildContext context, WidgetRef ref) {
final authorUser = useState<User?>(null);
final currentUser = ref.read(loggedInUser);
final isWebOfTrust =
currentUser?.following.contains(authorUser.value) ?? false;
final isSha256Ok = useState<bool?>(null);
return ExpansionTileCard(
// key: cardA,
leading: ClipRRect(
borderRadius: BorderRadius.circular(15.0),
child: Image.network(
note.tagMap['url']!.first,
width: 80,
height: 80,
fit: BoxFit.cover,
errorBuilder: (context, err, _) => Text(err.toString()),
),
),
title: Text(
note.content,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Type: ${note.tagMap['m']?.first}'),
],
),
onExpansionChanged: (isOpen) async {
if (isOpen && currentUser != null) {
final existingUser =
await ref.users.findOne(note.pubkey, remote: false);
if (existingUser?.name != null) {
print('fd has ${note.pubkey}! (${existingUser!.name}) ');
authorUser.value = existingUser;
} else {
authorUser.value =
await ref.read(profileProvider(note.pubkey).future);
}
}
},
children: [
Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (authorUser.value != null)
Text(
'Author: ${authorUser.value!.name} (${authorUser.value!.nip05})'),
if (isWebOfTrust)
Text(
'Author is in your web of trust',
style: TextStyle(color: Colors.green),
),
if (!isWebOfTrust)
Text('Author is not in your web of trust',
style: TextStyle(color: Colors.red)),
Text(
'SHA-256: ${note.tagMap['x']?.first}',
style: TextStyle(
color: (isSha256Ok.value != null)
? (isSha256Ok.value! ? Colors.green : Colors.red)
: Colors.white),
),
TextButton(
child: const Column(
children: <Widget>[
Text('Install'),
Icon(Icons.arrow_downward),
],
),
onPressed: () async {
final uri = Uri.parse(note.tagMap['url']!.first);
final response = await http.get(uri);
final dir = ref.read(downloadsPath);
File file = File(path.join(dir, uri.pathSegments.last));
await file.writeAsBytes(response.bodyBytes);
final digest = sha256.convert(response.bodyBytes);
isSha256Ok.value =
digest.toString() == note.tagMap['x']!.first;
if (isSha256Ok.value == false) {
print('actual digest $digest');
} else {
print('digest ok $digest');
}
},
),
],
),
),
DataState(isLoading: true, exception: null) =>
CircularProgressIndicator(),
DataState(:final exception) => Text(exception.toString()),
},
),
],
);
}
}
@@ -7,6 +7,7 @@
#include "generated_plugin_registrant.h"
#include <bitsdojo_window_linux/bitsdojo_window_plugin.h>
#include <desktop_drop/desktop_drop_plugin.h>
#include <handy_window/handy_window_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <window_manager/window_manager_plugin.h>
@@ -15,6 +16,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) bitsdojo_window_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "BitsdojoWindowPlugin");
bitsdojo_window_plugin_register_with_registrar(bitsdojo_window_linux_registrar);
g_autoptr(FlPluginRegistrar) desktop_drop_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin");
desktop_drop_plugin_register_with_registrar(desktop_drop_registrar);
g_autoptr(FlPluginRegistrar) handy_window_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "HandyWindowPlugin");
handy_window_plugin_register_with_registrar(handy_window_registrar);
+1
View File
@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
bitsdojo_window_linux
desktop_drop
handy_window
screen_retriever
window_manager
+46 -74
View File
@@ -33,46 +33,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.11.0"
base58check:
dependency: transitive
description:
name: base58check
sha256: "6c300dfc33e598d2fe26319e13f6243fea81eaf8204cb4c6b69ef20a625319a5"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
bech32:
dependency: transitive
dependency: "direct main"
description:
name: bech32
sha256: "156cbace936f7720c79a79d16a03efad343b1ef17106716e04b8b8e39f99f7f7"
url: "https://pub.dev"
source: hosted
version: "0.2.2"
bip32_bip44:
dependency: transitive
description:
name: bip32_bip44
sha256: "8e28e6bde00da1ed207f2c0a5361792375799196176742c0d36c71e89a5485d3"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
bip340:
dependency: transitive
description:
name: bip340
sha256: "9a2f454d3f5e2d67a1b21494203132786d95bde8784e7ada74adac00df3ddc11"
url: "https://pub.dev"
source: hosted
version: "0.1.0"
bip39:
dependency: transitive
description:
name: bip39
sha256: de1ee27ebe7d96b84bb3a04a4132a0a3007dcdd5ad27dd14aa87a29d97c45edc
url: "https://pub.dev"
source: hosted
version: "1.0.6"
bitsdojo_window:
dependency: "direct main"
description:
@@ -233,8 +201,16 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.1"
crypto:
cross_file:
dependency: transitive
description:
name: cross_file
sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e
url: "https://pub.dev"
source: hosted
version: "0.3.3+8"
crypto:
dependency: "direct main"
description:
name: crypto
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
@@ -249,14 +225,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.6"
dart_nostr:
dependency: "direct main"
description:
name: dart_nostr
sha256: "9d75157b4fe9ade7a186765efe3200a40f8b6673f88d617e96de2bc69671cb8c"
url: "https://pub.dev"
source: hosted
version: "8.0.0"
dart_style:
dependency: transitive
description:
@@ -265,14 +233,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.2"
desktop_drop:
dependency: "direct main"
description:
name: desktop_drop
sha256: d55a010fe46c8e8fcff4ea4b451a9ff84a162217bdb3b2a0aa1479776205e15d
url: "https://pub.dev"
source: hosted
version: "0.4.4"
equatable:
dependency: transitive
dependency: "direct main"
description:
name: equatable
sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2
url: "https://pub.dev"
source: hosted
version: "2.0.5"
expansion_tile_card:
dependency: "direct main"
description:
name: expansion_tile_card
sha256: "27ce4cb518f00e21d0f2309aaa6462b26b148e93cee2029a73088cecf42b1eb0"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
fake_async:
dependency: transitive
description:
@@ -338,15 +322,20 @@ packages:
dependency: transitive
description:
name: flutter_riverpod
sha256: da9591d1f8d5881628ccd5c25c40e74fc3eef50ba45e40c3905a06e1712412d5
sha256: "4bce556b7ecbfea26109638d5237684538d4abc509d253e6c5c4c5733b360098"
url: "https://pub.dev"
source: hosted
version: "2.4.9"
version: "2.4.10"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
frontend_server_client:
dependency: transitive
description:
@@ -387,14 +376,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.1"
hex:
dependency: transitive
description:
name: hex
sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
hive:
dependency: transitive
description:
@@ -407,10 +388,10 @@ packages:
dependency: "direct main"
description:
name: hooks_riverpod
sha256: c12a456e03ef9be65b0be66963596650ad7a3220e96c7e7b0a048562ea32d6ae
sha256: "758b07eba336e3cbacbd81dba481f2228a14102083fdde07045e8514e8054c49"
url: "https://pub.dev"
source: hosted
version: "2.4.9"
version: "2.4.10"
http:
dependency: transitive
description:
@@ -523,6 +504,13 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.5"
ndk:
dependency: "direct main"
description:
path: "../ndk"
relative: true
source: path
version: "0.1.0"
package_config:
dependency: transitive
description:
@@ -603,14 +591,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pointycastle:
dependency: transitive
description:
name: pointycastle
sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29"
url: "https://pub.dev"
source: hosted
version: "3.7.4"
pool:
dependency: transitive
description:
@@ -655,10 +635,10 @@ packages:
dependency: transitive
description:
name: riverpod
sha256: "942999ee48b899f8a46a860f1e13cee36f2f77609eb54c5b7a669bb20d550b11"
sha256: "548e2192eb7aeb826eb89387f814edb76594f3363e2c0bb99dd733d795ba3589"
url: "https://pub.dev"
source: hosted
version: "2.4.9"
version: "2.5.0"
screen_retriever:
dependency: transitive
description:
@@ -824,14 +804,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.4.0"
web_socket_client:
dependency: "direct main"
description:
name: web_socket_client
sha256: de52c9b3acf1f9fafb2951aa2764dd67c7d99d70be70c7c1fd791fae8f632f98
url: "https://pub.dev"
source: hosted
version: "0.1.0"
win32:
dependency: transitive
description:
@@ -865,5 +837,5 @@ packages:
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.2.0 <=3.9.9"
dart: ">=3.2.0 <4.0.0"
flutter: ">=3.10.0"
+10 -2
View File
@@ -40,8 +40,16 @@ dependencies:
build_runner: ^2.4.8
collection: ^1.18.0
path_provider: ^2.1.2
web_socket_client: ^0.1.0
dart_nostr: ^8.0.0
ndk: ^0.0.1
bech32: ^0.2.2
expansion_tile_card: ^3.0.0
desktop_drop: ^0.4.4
equatable: ^2.0.5
crypto: ^3.0.3
dependency_overrides:
ndk:
path: ../ndk
dev_dependencies:
flutter_test: