From 7efb22a91f253710b70acee96f436c162ec85287 Mon Sep 17 00:00:00 2001 From: franzap <_@franzap.com> Date: Thu, 2 May 2024 00:10:27 -0300 Subject: [PATCH] data loading wip --- lib/main.dart | 10 ++- lib/models/app.dart | 32 ++------ lib/models/app.g.dart | 41 ++++++++-- lib/models/file_metadata.dart | 5 +- lib/models/file_metadata.g.dart | 19 ++++- lib/models/nostr_adapter.dart | 85 ++++++++++++++++++++ lib/models/release.dart | 9 +-- lib/models/release.g.dart | 19 ++++- lib/models/user.dart | 125 +++++++---------------------- lib/models/user.g.dart | 2 +- lib/screens/app_detail_screen.dart | 59 +++++++------- lib/screens/search_screen.dart | 84 +++++++++++++------ lib/widgets/app_drawer.dart | 26 +++--- lib/widgets/card.dart | 62 +++++++------- lib/widgets/pill_widget.dart | 7 +- pubspec.yaml | 1 - 16 files changed, 338 insertions(+), 248 deletions(-) create mode 100644 lib/models/nostr_adapter.dart diff --git a/lib/main.dart b/lib/main.dart index 7204ce0..0c60f75 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,8 +3,10 @@ import 'package:flutter_data/flutter_data.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:purplebase/purplebase.dart'; import 'package:zapstore/main.data.dart'; import 'package:zapstore/models/app.dart'; +import 'package:zapstore/models/user.dart'; import 'package:zapstore/screens/app_detail_screen.dart'; import 'package:zapstore/screens/settings_screen.dart'; import 'package:zapstore/screens/updates_screen.dart'; @@ -23,7 +25,7 @@ void main() { print('initializing local storage at $path'); return path; }, - clear: LocalStorageClearStrategy.whenError, + clear: LocalStorageClearStrategy.always, // TODO whenError ), ) ], @@ -127,9 +129,9 @@ final goRouter = GoRouter( final newInitializer = FutureProvider((ref) async { await ref.read(initializeFlutterData(adapterProvidersMap).future); - // ref - // .read(p.relayMessageNotifierProvider.notifier) - // .initialize(['wss://relay.zap.store', 'wss://relay.nostr.band']); + ref + .read(relayMessageNotifierProvider.notifier) + .initialize(['wss://relay.zap.store', 'wss://relay.nostr.band']); }); class ScaffoldWithNestedNavigation extends HookConsumerWidget { diff --git a/lib/models/app.dart b/lib/models/app.dart index c05bee3..bf7772e 100644 --- a/lib/models/app.dart +++ b/lib/models/app.dart @@ -1,38 +1,16 @@ -import 'package:equatable/equatable.dart'; import 'package:flutter_data/flutter_data.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:purplebase/purplebase.dart'; +import 'package:zapstore/models/nostr_adapter.dart'; import 'package:zapstore/models/release.dart'; import 'package:zapstore/models/user.dart'; part 'app.g.dart'; @JsonSerializable() -@DataAdapter([NostrAdapter, CoolAdapter]) -class App extends ZapstoreEvent with BaseApp, EquatableMixin { +@DataAdapter([NostrAdapter]) +class App extends ZapstoreEvent with BaseApp { late final HasMany releases; - App(); - factory App.fromJson(Map json) => _$AppFromJson(json); - @override - List get props => [id]; -} - -mixin CoolAdapter on Adapter { - @override - Future onInitialized() async { - // db.execute(''' - // CREATE TABLE IF NOT EXISTS $internalType ( - // key INTEGER PRIMARY KEY AUTOINCREMENT, - // data TEXT - // ); - // '''); - super.onInitialized(); - } - - Future> query(int kind) async { - final result = db.select( - "SELECT data FROM $internalType WHERE json_extract(data, '\$.kind') = ?", - [kind]); - return deserializeFromResult(result); - } + late final BelongsTo signer; + late final BelongsTo developer; } diff --git a/lib/models/app.g.dart b/lib/models/app.g.dart index adde420..942a222 100644 --- a/lib/models/app.g.dart +++ b/lib/models/app.g.dart @@ -16,6 +16,18 @@ mixin _$AppAdapter on Adapter { type: 'releases', kind: 'HasMany', instance: (_) => (_ as App).releases, + ), + 'signer': RelationshipMeta( + name: 'signer', + type: 'users', + kind: 'BelongsTo', + instance: (_) => (_ as App).signer, + ), + 'developer': RelationshipMeta( + name: 'developer', + type: 'users', + kind: 'BelongsTo', + instance: (_) => (_ as App).developer, ) }; @@ -25,7 +37,7 @@ mixin _$AppAdapter on Adapter { @override App deserializeLocal(map, {String? key}) { map = transformDeserialize(map); - return internalWrapStopInit(() => App.fromJson(map), key: key); + return internalWrapStopInit(() => _$AppFromJson(map), key: key); } @override @@ -37,15 +49,13 @@ mixin _$AppAdapter on Adapter { final _appsFinders = {}; -class $AppAdapter = Adapter - with _$AppAdapter, NostrAdapter, CoolAdapter; +class $AppAdapter = Adapter with _$AppAdapter, NostrAdapter; final appsAdapterProvider = Provider>( (ref) => $AppAdapter(ref, InternalHolder(_appsFinders))); extension AppAdapterX on Adapter { NostrAdapter get nostrAdapter => this as NostrAdapter; - CoolAdapter get coolAdapter => this as CoolAdapter; } extension AppRelationshipGraphNodeX on RelationshipGraphNode { @@ -55,6 +65,20 @@ extension AppRelationshipGraphNodeX on RelationshipGraphNode { return meta.clone( parent: this is RelationshipMeta ? this as RelationshipMeta : null); } + + RelationshipGraphNode get signer { + final meta = + _$AppAdapter._kAppRelationshipMetas['signer'] as RelationshipMeta; + return meta.clone( + parent: this is RelationshipMeta ? this as RelationshipMeta : null); + } + + RelationshipGraphNode get developer { + final meta = _$AppAdapter._kAppRelationshipMetas['developer'] + as RelationshipMeta; + return meta.clone( + parent: this is RelationshipMeta ? this as RelationshipMeta : null); + } } // ************************************************************************** @@ -66,13 +90,16 @@ App _$AppFromJson(Map json) => App() ..pubkey = json['pubkey'] as String ..createdAt = DateTime.parse(json['createdAt'] as String) ..content = json['content'] as String - ..kind = json['kind'] as int + ..kind = (json['kind'] as num).toInt() ..tags = (json['tags'] as List) .map((e) => (e as List).map((e) => e as String).toList()) .toList() ..signature = json['signature'] as String? ..releases = - HasMany.fromJson(json['releases'] as Map); + HasMany.fromJson(json['releases'] as Map) + ..signer = BelongsTo.fromJson(json['signer'] as Map) + ..developer = + BelongsTo.fromJson(json['developer'] as Map); Map _$AppToJson(App instance) => { 'id': instance.id, @@ -83,4 +110,6 @@ Map _$AppToJson(App instance) => { 'tags': instance.tags, 'signature': instance.signature, 'releases': instance.releases, + 'signer': instance.signer, + 'developer': instance.developer, }; diff --git a/lib/models/file_metadata.dart b/lib/models/file_metadata.dart index 8e0c1b4..7b81fb8 100644 --- a/lib/models/file_metadata.dart +++ b/lib/models/file_metadata.dart @@ -1,13 +1,13 @@ import 'dart:io'; import 'package:android_package_installer/android_package_installer.dart'; -import 'package:android_package_manager/android_package_manager.dart'; import 'package:crypto/crypto.dart'; import 'package:flutter_data/flutter_data.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:purplebase/purplebase.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:zapstore/models/nostr_adapter.dart'; import 'package:zapstore/models/release.dart'; import 'package:zapstore/models/user.dart'; import 'package:http/http.dart' as http; @@ -15,13 +15,12 @@ import 'package:path/path.dart' as path; part 'file_metadata.g.dart'; -final packageManager = AndroidPackageManager(); - @JsonSerializable() @DataAdapter([NostrAdapter, FileMetadataAdapter]) class FileMetadata extends ZapstoreEvent with BaseFileMetadata { late final BelongsTo author; late final BelongsTo release = BelongsTo(); + late final BelongsTo signer; Future install() async { // throw Exception('something nasty'); diff --git a/lib/models/file_metadata.g.dart b/lib/models/file_metadata.g.dart index 0f4be5c..42f6353 100644 --- a/lib/models/file_metadata.g.dart +++ b/lib/models/file_metadata.g.dart @@ -22,6 +22,12 @@ mixin _$FileMetadataAdapter on Adapter { type: 'releases', kind: 'BelongsTo', instance: (_) => (_ as FileMetadata).release, + ), + 'signer': RelationshipMeta( + name: 'signer', + type: 'users', + kind: 'BelongsTo', + instance: (_) => (_ as FileMetadata).signer, ) }; @@ -72,6 +78,13 @@ extension FileMetadataRelationshipGraphNodeX return meta.clone( parent: this is RelationshipMeta ? this as RelationshipMeta : null); } + + RelationshipGraphNode get signer { + final meta = _$FileMetadataAdapter._kFileMetadataRelationshipMetas['signer'] + as RelationshipMeta; + return meta.clone( + parent: this is RelationshipMeta ? this as RelationshipMeta : null); + } } // ************************************************************************** @@ -83,12 +96,13 @@ FileMetadata _$FileMetadataFromJson(Map json) => FileMetadata() ..pubkey = json['pubkey'] as String ..createdAt = DateTime.parse(json['createdAt'] as String) ..content = json['content'] as String - ..kind = json['kind'] as int + ..kind = (json['kind'] as num).toInt() ..tags = (json['tags'] as List) .map((e) => (e as List).map((e) => e as String).toList()) .toList() ..signature = json['signature'] as String? - ..author = BelongsTo.fromJson(json['author'] as Map); + ..author = BelongsTo.fromJson(json['author'] as Map) + ..signer = BelongsTo.fromJson(json['signer'] as Map); Map _$FileMetadataToJson(FileMetadata instance) => { @@ -100,4 +114,5 @@ Map _$FileMetadataToJson(FileMetadata instance) => 'tags': instance.tags, 'signature': instance.signature, 'author': instance.author, + 'signer': instance.signer, }; diff --git a/lib/models/nostr_adapter.dart b/lib/models/nostr_adapter.dart new file mode 100644 index 0000000..3d946c2 --- /dev/null +++ b/lib/models/nostr_adapter.dart @@ -0,0 +1,85 @@ +import 'package:flutter_data/flutter_data.dart'; +import 'package:purplebase/purplebase.dart'; +import 'package:zapstore/models/user.dart'; + +abstract class ZapstoreEvent> = BaseEvent + with DataModelMixin; + +mixin NostrAdapter> on Adapter { + late final RelayMessageNotifier notifier = + ref.read(relayMessageNotifierProvider.notifier); + + Map kindType = { + 0: 'users', + 3: 'users', + 1063: 'fileMetadata', + 30063: 'releases', + 32267: 'apps' + }; + + @override + DeserializedData deserialize(Object? data, {String? key}) { + final list = data is Iterable ? data : [data as Map]; + final models = []; + final included = []; + + for (final e in list) { + final map = e as Map; + map['createdAt'] = + DateTime.fromMillisecondsSinceEpoch(map['created_at'] * 1000) + .toIso8601String(); + final kind = map['kind'] as int; + + final dTags = (map['tags'] as Iterable).where((t) => t[0] == 'd'); + if (dTags.length == 1) { + map['id'] = (dTags.first as List)[1]; + } + map['signer'] = map['pubkey']; + final zapTags = (map['tags'] as Iterable).where((t) => t[0] == 'zap'); + if (zapTags.length == 1) { + map['developer'] = (zapTags.first as List)[1]; + } + + final xType = kindType[kind]; + if (xType != null) { + if (xType == internalType) { + final newData = super.deserialize(map); + models.addAll(newData.models as Iterable); + } else { + final newData = adapters[xType]!.deserialize(map); + included.addAll(newData.models as Iterable); + } + } + } + return DeserializedData(models, included: included); + } + + int get kind => + kindType.entries.firstWhere((e) => e.value == internalType).key; + + @override + Future> findAll( + {bool? remote, + bool? background, + Map? params, + Map? headers, + bool? syncLocal, + OnSuccessAll? onSuccess, + OnErrorAll? onError, + DataRequestLabel? label}) async { + if (remote == false) { + return findAllLocal(); + } + + final req = RelayRequest( + kinds: {kind, ...?params?.remove('kinds')}, + tags: params ?? {}, + ); + print(req); + + final result = + await notifier.query(req, relayUrls: ['wss://relay.zap.store']); + final deserialized = await deserializeAsync(result, save: true); + return deserialized.models; + } +} diff --git a/lib/models/release.dart b/lib/models/release.dart index 8de2e58..2a05889 100644 --- a/lib/models/release.dart +++ b/lib/models/release.dart @@ -3,6 +3,7 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:purplebase/purplebase.dart'; import 'package:zapstore/models/app.dart'; import 'package:zapstore/models/file_metadata.dart'; +import 'package:zapstore/models/nostr_adapter.dart'; import 'package:zapstore/models/user.dart'; part 'release.g.dart'; @@ -12,6 +13,7 @@ part 'release.g.dart'; class Release extends ZapstoreEvent with BaseRelease { late final HasMany artifacts; late final BelongsTo app; + late final BelongsTo signer; } mixin ReleaseAdapter on Adapter { @@ -26,12 +28,7 @@ mixin ReleaseAdapter on Adapter { map['artifacts'] = eTags.toList(); final appIdentifier = map['tags'].firstWhere((t) => t.first == 'd')[1].split('@').first; - map['app'] = (adapters['apps'] as Adapter) - .findAllLocal() - .where((a) => a.identifier == appIdentifier) - // ignore: invalid_use_of_visible_for_testing_member - .safeFirst - ?.id; + map['app'] = appIdentifier; } return super.deserialize(data); } diff --git a/lib/models/release.g.dart b/lib/models/release.g.dart index 14c7ae3..70a3766 100644 --- a/lib/models/release.g.dart +++ b/lib/models/release.g.dart @@ -23,6 +23,12 @@ mixin _$ReleaseAdapter on Adapter { type: 'apps', kind: 'BelongsTo', instance: (_) => (_ as Release).app, + ), + 'signer': RelationshipMeta( + name: 'signer', + type: 'users', + kind: 'BelongsTo', + instance: (_) => (_ as Release).signer, ) }; @@ -70,6 +76,13 @@ extension ReleaseRelationshipGraphNodeX on RelationshipGraphNode { return meta.clone( parent: this is RelationshipMeta ? this as RelationshipMeta : null); } + + RelationshipGraphNode get signer { + final meta = _$ReleaseAdapter._kReleaseRelationshipMetas['signer'] + as RelationshipMeta; + return meta.clone( + parent: this is RelationshipMeta ? this as RelationshipMeta : null); + } } // ************************************************************************** @@ -81,14 +94,15 @@ Release _$ReleaseFromJson(Map json) => Release() ..pubkey = json['pubkey'] as String ..createdAt = DateTime.parse(json['createdAt'] as String) ..content = json['content'] as String - ..kind = json['kind'] as int + ..kind = (json['kind'] as num).toInt() ..tags = (json['tags'] as List) .map((e) => (e as List).map((e) => e as String).toList()) .toList() ..signature = json['signature'] as String? ..artifacts = HasMany.fromJson(json['artifacts'] as Map) - ..app = BelongsTo.fromJson(json['app'] as Map); + ..app = BelongsTo.fromJson(json['app'] as Map) + ..signer = BelongsTo.fromJson(json['signer'] as Map); Map _$ReleaseToJson(Release instance) => { 'id': instance.id, @@ -100,4 +114,5 @@ Map _$ReleaseToJson(Release instance) => { 'signature': instance.signature, 'artifacts': instance.artifacts, 'app': instance.app, + 'signer': instance.signer, }; diff --git a/lib/models/user.dart b/lib/models/user.dart index 7868626..dd914f7 100644 --- a/lib/models/user.dart +++ b/lib/models/user.dart @@ -1,26 +1,17 @@ -import 'package:equatable/equatable.dart'; import 'package:flutter_data/flutter_data.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:purplebase/purplebase.dart'; +import 'package:zapstore/models/nostr_adapter.dart'; part 'user.g.dart'; -abstract class ZapstoreEvent> = BaseEvent - with DataModelMixin; - @JsonSerializable() @DataAdapter([NostrAdapter, UserAdapter]) -class User extends ZapstoreEvent with BaseUser, EquatableMixin { - User(); - User.fromMap(super.map) : super.fromMap(); - +class User extends ZapstoreEvent with BaseUser { @DataRelationship(inverse: 'followers') late final HasMany following; @DataRelationship(inverse: 'following') late final HasMany followers = HasMany(); - - @override - List get props => [id]; } mixin UserAdapter on NostrAdapter { @@ -32,15 +23,14 @@ mixin UserAdapter on NostrAdapter { final map = e as Map; if (map['kind'] == 3) { for (final [_, id, ..._] in map['tags'] as Iterable) { - users.add(User.fromMap({ + super.deserialize({ 'id': id, 'content': '', 'pubkey': id, 'created_at': DateTime.now().millisecondsSinceEpoch ~/ 1000, 'kind': 0, 'tags': [], - 'following': {'_': null}, - })); + }); } } } @@ -64,6 +54,26 @@ mixin UserAdapter on NostrAdapter { return DeserializedData(users); } + @override + Future> findAll( + {bool? remote, + bool? background, + Map? params, + Map? headers, + bool? syncLocal, + OnSuccessAll? onSuccess, + OnErrorAll? onError, + DataRequestLabel? label}) async { + final req = + RelayRequest(authors: Set.from(params!['ids']), kinds: {kind}); + print('in user findall $req'); + + final result = + await notifier.query(req, relayUrls: ['wss://relay.nostr.band']); + final data = await deserializeAsync(result, save: true); + return data.models; + } + @override Future findOne(Object id, {bool remote = true, @@ -73,17 +83,12 @@ mixin UserAdapter on NostrAdapter { OnSuccessOne? onSuccess, OnErrorOne? onError, DataRequestLabel? label}) async { - String? publicKey; - try { - publicKey = '$id'.hexKey; - } catch (_) { - String username; - String domain; - try { - [username, domain] = id.toString().split('@'); - } catch (_) { - return null; - } + var publicKey = id.toString(); + + if (publicKey.startsWith('npub')) { + publicKey = publicKey.hexKey; + } else if (publicKey.contains('@')) { + final [username, domain] = id.toString().split('@'); publicKey = await sendRequest( Uri.parse('https://$domain/.well-known/nostr.json?name=$username'), onSuccess: (response, label) { @@ -92,10 +97,6 @@ mixin UserAdapter on NostrAdapter { ); } - if (publicKey == null) { - return null; - } - final req = RelayRequest( authors: {publicKey}, kinds: {kind, ...?params?.remove('kinds')}, @@ -108,69 +109,3 @@ mixin UserAdapter on NostrAdapter { return data.model?..saveLocal(); } } - -mixin NostrAdapter> on Adapter { - late final RelayMessageNotifier notifier = - ref.read(relayMessageNotifierProvider.notifier); - - Map kindType = { - 0: 'users', - 3: 'users', - 1063: 'fileMetadata', - 30063: 'releases', - 32267: 'apps' - }; - - @override - DeserializedData deserialize(Object? data, {String? key}) { - final list = data is Iterable ? data : [data as Map]; - final models = []; - final included = []; - - for (final e in list) { - final map = e as Map; - map['createdAt'] = - DateTime.fromMillisecondsSinceEpoch(map['created_at'] * 1000) - .toIso8601String(); - final kind = map['kind'] as int; - final xType = kindType[kind]; - if (xType != null) { - if (xType == internalType) { - final newData = super.deserialize(map); - models.addAll(newData.models as Iterable); - } else { - final newData = adapters[xType]!.deserialize(map); - included.addAll(newData.models as Iterable); - } - } - } - return DeserializedData(models, included: included); - } - - int get kind => - kindType.entries.firstWhere((e) => e.value == internalType).key; - - @override - Future> findAll( - {bool? remote, - bool? background, - Map? params, - Map? headers, - bool? syncLocal, - OnSuccessAll? onSuccess, - OnErrorAll? onError, - DataRequestLabel? label}) async { - if (remote == false) { - return findAllLocal(); - } - - final req = RelayRequest( - kinds: {kind, ...?params?.remove('kinds')}, - tags: params ?? {}, - ); - - final result = await notifier.query(req); - final deserialized = await deserializeAsync(result, save: true); - return deserialized.models; - } -} diff --git a/lib/models/user.g.dart b/lib/models/user.g.dart index 0d1c1ea..631752e 100644 --- a/lib/models/user.g.dart +++ b/lib/models/user.g.dart @@ -81,7 +81,7 @@ User _$UserFromJson(Map json) => User() ..pubkey = json['pubkey'] as String ..createdAt = DateTime.parse(json['createdAt'] as String) ..content = json['content'] as String - ..kind = json['kind'] as int + ..kind = (json['kind'] as num).toInt() ..tags = (json['tags'] as List) .map((e) => (e as List).map((e) => e as String).toList()) .toList() diff --git a/lib/screens/app_detail_screen.dart b/lib/screens/app_detail_screen.dart index 4fa1a45..682b978 100644 --- a/lib/screens/app_detail_screen.dart +++ b/lib/screens/app_detail_screen.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'package:auto_size_text/auto_size_text.dart'; import 'package:carousel_slider/carousel_slider.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_data/flutter_data.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -13,15 +14,6 @@ import 'package:zapstore/models/user.dart'; import 'package:zapstore/widgets/card.dart'; import 'package:zapstore/widgets/pill_widget.dart'; -final List imgList = [ - 'https://images.unsplash.com/photo-1520342868574-5fa3804e551c?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=6ff92caffcdd63681a35134a6770ed3b&auto=format&fit=crop&w=1951&q=80', - 'https://images.unsplash.com/photo-1522205408450-add114ad53fe?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=368f45b0888aeb0b7b08e3a1084d3ede&auto=format&fit=crop&w=1950&q=80', - 'https://images.unsplash.com/photo-1519125323398-675f0ddb6308?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=94a1e718d89ca60a6337a6008341ca50&auto=format&fit=crop&w=1950&q=80', - 'https://images.unsplash.com/photo-1523205771623-e0faa4d2813d?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=89719a0d55dd05e2deae4120227e6efc&auto=format&fit=crop&w=1953&q=80', - 'https://images.unsplash.com/photo-1508704019882-f9cf40e475b4?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=8c6e5e3aba713b17aa1fe71ab4f0ae5b&auto=format&fit=crop&w=1352&q=80', - 'https://images.unsplash.com/photo-1519985176271-adb1088fa94c?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=a0c8d632e977f94e5d312d9893258f59&auto=format&fit=crop&w=1355&q=80' -]; - class AppDetailScreen extends HookConsumerWidget { final App app; const AppDetailScreen({ @@ -31,16 +23,6 @@ class AppDetailScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final user = User.fromMap({ - 'kind': 0, - 'created_at': 112899202, - 'pubkey': - 'a9e95a4eb32b55441b222ae5674f063949bfd0759b82deb03d7cd262e82d5626', - 'content': jsonEncode( - {'name': 'test', 'picture': 'https://picsum.photos/200/300'}), - 'tags': [], - }); - return Column( children: [ Expanded( @@ -62,7 +44,7 @@ class AppDetailScreen extends HookConsumerWidget { mainAxisAlignment: MainAxisAlignment.start, children: [ CircularImage( - url: 'https://picsum.photos/200/200', + url: app.icon, size: 80, radius: 25, ), @@ -73,7 +55,7 @@ class AppDetailScreen extends HookConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ AutoSizeText( - 'Mutiny Wallet', + app.name, minFontSize: 16, style: TextStyle( fontSize: 28, fontWeight: FontWeight.bold), @@ -81,7 +63,7 @@ class AppDetailScreen extends HookConsumerWidget { overflow: TextOverflow.ellipsis, ), Gap(8), - PillWidget(text: '0.5.6'), + PillWidget(text: '0.5.9', color: Colors.grey[800]), ], ), ), @@ -92,10 +74,10 @@ class AppDetailScreen extends HookConsumerWidget { options: CarouselOptions( enableInfiniteScroll: false, ), - items: imgList - .map((item) => Padding( + items: (app.tagMap['image'] ?? []) + .map((i) => Padding( padding: const EdgeInsets.only(right: 10), - child: Image.network(item, + child: Image.network(i, fit: BoxFit.cover, width: 1000), )) .toList(), @@ -108,11 +90,14 @@ class AppDetailScreen extends HookConsumerWidget { p: TextStyle(fontSize: 16, fontWeight: FontWeight.w300), ), selectable: false, - data: - 'Lorem ipsum dolor sit amet\n - consectetur adipisicing elit\n - sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, ', + data: app.content, ), Gap(10), - AuthorContainer(user: user, text: 'Built by', oneLine: false), + if (app.developer.isPresent) + AuthorContainer( + user: app.developer.value!, + text: 'Built by', + oneLine: false), Gap(10), Divider(height: 24), Container( @@ -127,21 +112,33 @@ class AppDetailScreen extends HookConsumerWidget { padding: const EdgeInsets.all(8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [Text('Source'), Text('github link')], + children: [ + Text('Source'), + Text( + app.repository, + style: TextStyle(fontSize: 11), + ) + ], ), ), Padding( padding: const EdgeInsets.all(8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [Text('Github stars'), Text('456')], + children: [ + Text('Github stars'), + Text(app.githubStars) + ], ), ), Padding( padding: const EdgeInsets.all(8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [Text('Github forks'), Text('3')], + children: [ + Text('Github forks'), + Text(app.githubForks) + ], ), ) ], diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index 60dc1d5..bfdb8b1 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -1,4 +1,5 @@ -// ignore_for_file: prefer_const_literals_to_create_immutables +import 'dart:developer'; +import 'dart:ffi'; import 'package:flutter/material.dart'; import 'package:flutter_data/flutter_data.dart'; @@ -6,7 +7,7 @@ import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:zapstore/main.data.dart'; import 'package:zapstore/models/app.dart'; -import 'package:zapstore/screens/app_detail_screen.dart'; +import 'package:zapstore/models/release.dart'; import 'package:zapstore/widgets/card.dart'; import 'package:zapstore/widgets/user_avatar.dart'; @@ -25,12 +26,6 @@ class SearchScreen extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final state = ref.watch(searchStateProvider); - if (state.isLoading) { - return Center(child: CircularProgressIndicator()); - } - - final apps = state.model; - return Column( children: [ Row( @@ -61,17 +56,58 @@ class SearchScreen extends HookConsumerWidget { autoFocus: true, elevation: MaterialStatePropertyAll(2.2), onSubmitted: (query) async { - ref.read(screenInfoProvider.notifier).state = 'No items'; - final apps = [App(), App(), App(), App()]; - // await ref.apps.findAll(params: {'search': query}); + // debugger; - // final releases = await ref.releases.findAll( - // params: {'#i': apps.map((a) => a.identifier).toSet()}); + // final app = App() + // ..id = '1' + // ..kind = 32267 + // ..createdAt = DateTime.now() + // ..pubkey = + // 'a9e95a4eb32b55441b222ae5674f063949bfd0759b82deb03d7cd262e82d5626' + // ..content = 'testing' + // ..tags = [ + // ['d', 'com.test'] + // ] + // ..releases = HasMany() + // ..init() + // ..saveLocal(); + + // final release = ref.releases.deserialize({ + // 'kind': 30063, + // 'created_at': 112899202, + // 'pubkey': + // 'a9e95a4eb32b55441b222ae5674f063949bfd0759b82deb03d7cd262e82d5626', + // 'content': 'mutiny', + // 'tags': [ + // ['d', 'com.test@v0.6.4'] + // ], + // }); + // debugger; + + // ref.read(searchStateProvider.notifier).state = + // DataState([app]); + // print(release.model!.app.value?.id); + + 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(), + // 'ids': releases.map((r) => r.tagMap['e']!.first).toSet(), // '#m': [kAndroidMimeType], // }, // ); @@ -83,22 +119,24 @@ class SearchScreen extends HookConsumerWidget { ], ), Gap(10), - if (apps.isEmpty) + if (state.model.isEmpty || state.isLoading) Expanded( child: Center( - child: Text( - ref.read(screenInfoProvider), - textAlign: TextAlign.center, - ), + child: state.isLoading + ? CircularProgressIndicator() + : Text( + ref.read(screenInfoProvider), + textAlign: TextAlign.center, + ), ), ), - if (apps.isNotEmpty) + if (state.hasModel) Expanded( child: ListView.builder( shrinkWrap: true, - itemCount: apps.length, + itemCount: state.model.length, itemBuilder: (context, index) { - final app = apps[index]; + final app = state.model[index]; return CardWidget(app: app); }, ), diff --git a/lib/widgets/app_drawer.dart b/lib/widgets/app_drawer.dart index ec6cc61..902f02c 100644 --- a/lib/widgets/app_drawer.dart +++ b/lib/widgets/app_drawer.dart @@ -32,19 +32,19 @@ class AppDrawer extends HookConsumerWidget { }, child: Text('Log in'), ), - ElevatedButton( - onPressed: () async { - ref.read(loggedInUser.notifier).state = null; - controller.clear(); - // final as = await packageManager.getInstalledApplications(); - // for (final a in as!) { - // if (!a.packageName!.contains('android')) { - // print(a.packageName); - // } - // } - }, - child: Text('check'), - ), + // ElevatedButton( + // onPressed: () async { + // ref.read(loggedInUser.notifier).state = null; + // controller.clear(); + // // final as = await packageManager.getInstalledApplications(); + // // for (final a in as!) { + // // if (!a.packageName!.contains('android')) { + // // print(a.packageName); + // // } + // // } + // }, + // child: Text('check'), + // ), if (u2 != null) Expanded( child: Column( diff --git a/lib/widgets/card.dart b/lib/widgets/card.dart index d9714f5..b2f7870 100644 --- a/lib/widgets/card.dart +++ b/lib/widgets/card.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:auto_size_text/auto_size_text.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; @@ -16,18 +17,9 @@ class CardWidget extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final user = User.fromMap({ - 'kind': 0, - 'created_at': 112899202, - 'pubkey': - 'a9e95a4eb32b55441b222ae5674f063949bfd0759b82deb03d7cd262e82d5626', - 'content': jsonEncode( - {'name': 'test', 'picture': 'https://picsum.photos/200/300'}), - 'tags': [], - }); - return Card( margin: EdgeInsets.only(top: 20), + elevation: 0, child: GestureDetector( onTap: () => context.go('/details', extra: app), child: Padding( @@ -37,7 +29,7 @@ class CardWidget extends HookConsumerWidget { mainAxisAlignment: MainAxisAlignment.start, children: [ CircularImage( - url: 'https://picsum.photos/200/200', + url: app.icon, size: 80, radius: 25, ), @@ -46,19 +38,19 @@ class CardWidget extends HookConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'Mutiny Wallet - Mutiny Wallet - Muti', - // '${app.name} - ${app.name}', + AutoSizeText( + app.releases.safeFirst?.identifier.split('@').last ?? + 'none', + minFontSize: 16, style: - TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + TextStyle(fontSize: 23, fontWeight: FontWeight.bold), overflow: TextOverflow.ellipsis, maxLines: 1, - softWrap: true, ), Padding( padding: const EdgeInsets.only(top: 4), child: Text( - 'Best web-based Lightning and Ecash wallet web-based Lightning and Ecash wallet', + app.content, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w300), overflow: TextOverflow.ellipsis, @@ -67,10 +59,17 @@ class CardWidget extends HookConsumerWidget { ), ), Gap(10), - AuthorContainer(user: user), - AuthorContainer(user: user), - Gap(16), - TagsContainer(), + if (app.developer.isPresent) + AuthorContainer( + user: app.developer.value!, + text: 'Built by', + ), + if (app.signer.isPresent) + AuthorContainer(user: app.signer.value!), + // Gap(16), + // TagsContainer( + // tags: app.tagMap['t'] ?? [], + // ), ], ), ), @@ -83,17 +82,21 @@ class CardWidget extends HookConsumerWidget { } class TagsContainer extends StatelessWidget { + final List tags; const TagsContainer({ super.key, + required this.tags, }); @override Widget build(BuildContext context) { return Row( - children: const [ - PillWidget(text: 'nostr'), - Gap(6), - PillWidget(text: 'wallet'), + children: [ + for (final tag in tags) + Padding( + padding: const EdgeInsets.only(right: 6), + child: PillWidget(text: tag), + ), ], ); } @@ -156,14 +159,11 @@ class CircularImage extends StatelessWidget { Widget build(BuildContext context) { return ClipRRect( borderRadius: BorderRadius.circular(radius.toDouble()), - child: Container( + child: CachedNetworkImage( + imageUrl: url ?? '', + fit: BoxFit.cover, width: size.toDouble(), height: size.toDouble(), - color: Colors.grey[850], - child: CachedNetworkImage( - imageUrl: url ?? '', - fit: BoxFit.cover, - ), ), ); } diff --git a/lib/widgets/pill_widget.dart b/lib/widgets/pill_widget.dart index 9796dbd..89097f1 100644 --- a/lib/widgets/pill_widget.dart +++ b/lib/widgets/pill_widget.dart @@ -2,21 +2,22 @@ import 'package:flutter/material.dart'; class PillWidget extends StatelessWidget { final String text; - const PillWidget({super.key, required this.text}); + final Color? color; + const PillWidget({super.key, required this.text, this.color = Colors.blue}); @override Widget build(BuildContext context) { return Container( padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( - color: Colors.blue, + color: color, borderRadius: BorderRadius.circular(12), ), child: Text( text, style: TextStyle( color: Colors.white, - fontSize: 11, + fontSize: 12, fontWeight: FontWeight.bold, ), ), diff --git a/pubspec.yaml b/pubspec.yaml index 8e1c8ec..53d681f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,7 +43,6 @@ dependencies: async_button_builder: ^3.0.0+1 android_package_manager: ^0.7.1 flutter_styled_toast: ^2.2.1 - equatable: ^2.0.5 url_launcher: ^6.2.5 device_apps: ^2.2.0 purplebase: