mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 20:48:24 +00:00
data loading wip
This commit is contained in:
+6
-4
@@ -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<void>((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 {
|
||||
|
||||
+5
-27
@@ -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<App> with BaseApp, EquatableMixin {
|
||||
@DataAdapter([NostrAdapter])
|
||||
class App extends ZapstoreEvent<App> with BaseApp {
|
||||
late final HasMany<Release> releases;
|
||||
App();
|
||||
factory App.fromJson(Map<String, dynamic> json) => _$AppFromJson(json);
|
||||
@override
|
||||
List<Object?> get props => [id];
|
||||
}
|
||||
|
||||
mixin CoolAdapter on Adapter<App> {
|
||||
@override
|
||||
Future<void> onInitialized() async {
|
||||
// db.execute('''
|
||||
// CREATE TABLE IF NOT EXISTS $internalType (
|
||||
// key INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
// data TEXT
|
||||
// );
|
||||
// ''');
|
||||
super.onInitialized();
|
||||
}
|
||||
|
||||
Future<List<App>> query(int kind) async {
|
||||
final result = db.select(
|
||||
"SELECT data FROM $internalType WHERE json_extract(data, '\$.kind') = ?",
|
||||
[kind]);
|
||||
return deserializeFromResult(result);
|
||||
}
|
||||
late final BelongsTo<User> signer;
|
||||
late final BelongsTo<User> developer;
|
||||
}
|
||||
|
||||
+35
-6
@@ -16,6 +16,18 @@ mixin _$AppAdapter on Adapter<App> {
|
||||
type: 'releases',
|
||||
kind: 'HasMany',
|
||||
instance: (_) => (_ as App).releases,
|
||||
),
|
||||
'signer': RelationshipMeta<User>(
|
||||
name: 'signer',
|
||||
type: 'users',
|
||||
kind: 'BelongsTo',
|
||||
instance: (_) => (_ as App).signer,
|
||||
),
|
||||
'developer': RelationshipMeta<User>(
|
||||
name: 'developer',
|
||||
type: 'users',
|
||||
kind: 'BelongsTo',
|
||||
instance: (_) => (_ as App).developer,
|
||||
)
|
||||
};
|
||||
|
||||
@@ -25,7 +37,7 @@ mixin _$AppAdapter on Adapter<App> {
|
||||
@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<App> {
|
||||
|
||||
final _appsFinders = <String, dynamic>{};
|
||||
|
||||
class $AppAdapter = Adapter<App>
|
||||
with _$AppAdapter, NostrAdapter<App>, CoolAdapter;
|
||||
class $AppAdapter = Adapter<App> with _$AppAdapter, NostrAdapter<App>;
|
||||
|
||||
final appsAdapterProvider = Provider<Adapter<App>>(
|
||||
(ref) => $AppAdapter(ref, InternalHolder(_appsFinders)));
|
||||
|
||||
extension AppAdapterX on Adapter<App> {
|
||||
NostrAdapter<App> get nostrAdapter => this as NostrAdapter<App>;
|
||||
CoolAdapter get coolAdapter => this as CoolAdapter;
|
||||
}
|
||||
|
||||
extension AppRelationshipGraphNodeX on RelationshipGraphNode<App> {
|
||||
@@ -55,6 +65,20 @@ extension AppRelationshipGraphNodeX on RelationshipGraphNode<App> {
|
||||
return meta.clone(
|
||||
parent: this is RelationshipMeta ? this as RelationshipMeta : null);
|
||||
}
|
||||
|
||||
RelationshipGraphNode<User> get signer {
|
||||
final meta =
|
||||
_$AppAdapter._kAppRelationshipMetas['signer'] as RelationshipMeta<User>;
|
||||
return meta.clone(
|
||||
parent: this is RelationshipMeta ? this as RelationshipMeta : null);
|
||||
}
|
||||
|
||||
RelationshipGraphNode<User> get developer {
|
||||
final meta = _$AppAdapter._kAppRelationshipMetas['developer']
|
||||
as RelationshipMeta<User>;
|
||||
return meta.clone(
|
||||
parent: this is RelationshipMeta ? this as RelationshipMeta : null);
|
||||
}
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
@@ -66,13 +90,16 @@ App _$AppFromJson(Map<String, dynamic> 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<dynamic>)
|
||||
.map((e) => (e as List<dynamic>).map((e) => e as String).toList())
|
||||
.toList()
|
||||
..signature = json['signature'] as String?
|
||||
..releases =
|
||||
HasMany<Release>.fromJson(json['releases'] as Map<String, dynamic>);
|
||||
HasMany<Release>.fromJson(json['releases'] as Map<String, dynamic>)
|
||||
..signer = BelongsTo<User>.fromJson(json['signer'] as Map<String, dynamic>)
|
||||
..developer =
|
||||
BelongsTo<User>.fromJson(json['developer'] as Map<String, dynamic>);
|
||||
|
||||
Map<String, dynamic> _$AppToJson(App instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
@@ -83,4 +110,6 @@ Map<String, dynamic> _$AppToJson(App instance) => <String, dynamic>{
|
||||
'tags': instance.tags,
|
||||
'signature': instance.signature,
|
||||
'releases': instance.releases,
|
||||
'signer': instance.signer,
|
||||
'developer': instance.developer,
|
||||
};
|
||||
|
||||
@@ -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<FileMetadata> with BaseFileMetadata {
|
||||
late final BelongsTo<User> author;
|
||||
late final BelongsTo<Release> release = BelongsTo();
|
||||
late final BelongsTo<User> signer;
|
||||
|
||||
Future<void> install() async {
|
||||
// throw Exception('something nasty');
|
||||
|
||||
@@ -22,6 +22,12 @@ mixin _$FileMetadataAdapter on Adapter<FileMetadata> {
|
||||
type: 'releases',
|
||||
kind: 'BelongsTo',
|
||||
instance: (_) => (_ as FileMetadata).release,
|
||||
),
|
||||
'signer': RelationshipMeta<User>(
|
||||
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<User> get signer {
|
||||
final meta = _$FileMetadataAdapter._kFileMetadataRelationshipMetas['signer']
|
||||
as RelationshipMeta<User>;
|
||||
return meta.clone(
|
||||
parent: this is RelationshipMeta ? this as RelationshipMeta : null);
|
||||
}
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
@@ -83,12 +96,13 @@ FileMetadata _$FileMetadataFromJson(Map<String, dynamic> 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<dynamic>)
|
||||
.map((e) => (e as List<dynamic>).map((e) => e as String).toList())
|
||||
.toList()
|
||||
..signature = json['signature'] as String?
|
||||
..author = BelongsTo<User>.fromJson(json['author'] as Map<String, dynamic>);
|
||||
..author = BelongsTo<User>.fromJson(json['author'] as Map<String, dynamic>)
|
||||
..signer = BelongsTo<User>.fromJson(json['signer'] as Map<String, dynamic>);
|
||||
|
||||
Map<String, dynamic> _$FileMetadataToJson(FileMetadata instance) =>
|
||||
<String, dynamic>{
|
||||
@@ -100,4 +114,5 @@ Map<String, dynamic> _$FileMetadataToJson(FileMetadata instance) =>
|
||||
'tags': instance.tags,
|
||||
'signature': instance.signature,
|
||||
'author': instance.author,
|
||||
'signer': instance.signer,
|
||||
};
|
||||
|
||||
@@ -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<T extends ZapstoreEvent<T>> = BaseEvent
|
||||
with DataModelMixin<T>;
|
||||
|
||||
mixin NostrAdapter<T extends ZapstoreEvent<T>> on Adapter<T> {
|
||||
late final RelayMessageNotifier notifier =
|
||||
ref.read(relayMessageNotifierProvider.notifier);
|
||||
|
||||
Map<int, String> kindType = {
|
||||
0: 'users',
|
||||
3: 'users',
|
||||
1063: 'fileMetadata',
|
||||
30063: 'releases',
|
||||
32267: 'apps'
|
||||
};
|
||||
|
||||
@override
|
||||
DeserializedData<T> deserialize(Object? data, {String? key}) {
|
||||
final list = data is Iterable ? data : [data as Map];
|
||||
final models = <T>[];
|
||||
final included = <DataModelMixin>[];
|
||||
|
||||
for (final e in list) {
|
||||
final map = e as Map<String, dynamic>;
|
||||
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<T>);
|
||||
} else {
|
||||
final newData = adapters[xType]!.deserialize(map);
|
||||
included.addAll(newData.models as Iterable<DataModelMixin>);
|
||||
}
|
||||
}
|
||||
}
|
||||
return DeserializedData<T>(models, included: included);
|
||||
}
|
||||
|
||||
int get kind =>
|
||||
kindType.entries.firstWhere((e) => e.value == internalType).key;
|
||||
|
||||
@override
|
||||
Future<List<T>> findAll(
|
||||
{bool? remote,
|
||||
bool? background,
|
||||
Map<String, dynamic>? params,
|
||||
Map<String, String>? headers,
|
||||
bool? syncLocal,
|
||||
OnSuccessAll<T>? onSuccess,
|
||||
OnErrorAll<T>? 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;
|
||||
}
|
||||
}
|
||||
@@ -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<Release> with BaseRelease {
|
||||
late final HasMany<FileMetadata> artifacts;
|
||||
late final BelongsTo<App> app;
|
||||
late final BelongsTo<User> signer;
|
||||
}
|
||||
|
||||
mixin ReleaseAdapter on Adapter<Release> {
|
||||
@@ -26,12 +28,7 @@ mixin ReleaseAdapter on Adapter<Release> {
|
||||
map['artifacts'] = eTags.toList();
|
||||
final appIdentifier =
|
||||
map['tags'].firstWhere((t) => t.first == 'd')[1].split('@').first;
|
||||
map['app'] = (adapters['apps'] as Adapter<App>)
|
||||
.findAllLocal()
|
||||
.where((a) => a.identifier == appIdentifier)
|
||||
// ignore: invalid_use_of_visible_for_testing_member
|
||||
.safeFirst
|
||||
?.id;
|
||||
map['app'] = appIdentifier;
|
||||
}
|
||||
return super.deserialize(data);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,12 @@ mixin _$ReleaseAdapter on Adapter<Release> {
|
||||
type: 'apps',
|
||||
kind: 'BelongsTo',
|
||||
instance: (_) => (_ as Release).app,
|
||||
),
|
||||
'signer': RelationshipMeta<User>(
|
||||
name: 'signer',
|
||||
type: 'users',
|
||||
kind: 'BelongsTo',
|
||||
instance: (_) => (_ as Release).signer,
|
||||
)
|
||||
};
|
||||
|
||||
@@ -70,6 +76,13 @@ extension ReleaseRelationshipGraphNodeX on RelationshipGraphNode<Release> {
|
||||
return meta.clone(
|
||||
parent: this is RelationshipMeta ? this as RelationshipMeta : null);
|
||||
}
|
||||
|
||||
RelationshipGraphNode<User> get signer {
|
||||
final meta = _$ReleaseAdapter._kReleaseRelationshipMetas['signer']
|
||||
as RelationshipMeta<User>;
|
||||
return meta.clone(
|
||||
parent: this is RelationshipMeta ? this as RelationshipMeta : null);
|
||||
}
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
@@ -81,14 +94,15 @@ Release _$ReleaseFromJson(Map<String, dynamic> 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<dynamic>)
|
||||
.map((e) => (e as List<dynamic>).map((e) => e as String).toList())
|
||||
.toList()
|
||||
..signature = json['signature'] as String?
|
||||
..artifacts =
|
||||
HasMany<FileMetadata>.fromJson(json['artifacts'] as Map<String, dynamic>)
|
||||
..app = BelongsTo<App>.fromJson(json['app'] as Map<String, dynamic>);
|
||||
..app = BelongsTo<App>.fromJson(json['app'] as Map<String, dynamic>)
|
||||
..signer = BelongsTo<User>.fromJson(json['signer'] as Map<String, dynamic>);
|
||||
|
||||
Map<String, dynamic> _$ReleaseToJson(Release instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
@@ -100,4 +114,5 @@ Map<String, dynamic> _$ReleaseToJson(Release instance) => <String, dynamic>{
|
||||
'signature': instance.signature,
|
||||
'artifacts': instance.artifacts,
|
||||
'app': instance.app,
|
||||
'signer': instance.signer,
|
||||
};
|
||||
|
||||
+30
-95
@@ -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<T extends ZapstoreEvent<T>> = BaseEvent
|
||||
with DataModelMixin<T>;
|
||||
|
||||
@JsonSerializable()
|
||||
@DataAdapter([NostrAdapter, UserAdapter])
|
||||
class User extends ZapstoreEvent<User> with BaseUser, EquatableMixin {
|
||||
User();
|
||||
User.fromMap(super.map) : super.fromMap();
|
||||
|
||||
class User extends ZapstoreEvent<User> with BaseUser {
|
||||
@DataRelationship(inverse: 'followers')
|
||||
late final HasMany<User> following;
|
||||
@DataRelationship(inverse: 'following')
|
||||
late final HasMany<User> followers = HasMany();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id];
|
||||
}
|
||||
|
||||
mixin UserAdapter on NostrAdapter<User> {
|
||||
@@ -32,15 +23,14 @@ mixin UserAdapter on NostrAdapter<User> {
|
||||
final map = e as Map<String, dynamic>;
|
||||
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<User> {
|
||||
return DeserializedData(users);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<User>> findAll(
|
||||
{bool? remote,
|
||||
bool? background,
|
||||
Map<String, dynamic>? params,
|
||||
Map<String, String>? headers,
|
||||
bool? syncLocal,
|
||||
OnSuccessAll<User>? onSuccess,
|
||||
OnErrorAll<User>? onError,
|
||||
DataRequestLabel? label}) async {
|
||||
final req =
|
||||
RelayRequest(authors: Set<String>.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<User?> findOne(Object id,
|
||||
{bool remote = true,
|
||||
@@ -73,17 +83,12 @@ mixin UserAdapter on NostrAdapter<User> {
|
||||
OnSuccessOne<User>? onSuccess,
|
||||
OnErrorOne<User>? 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<dynamic>(
|
||||
Uri.parse('https://$domain/.well-known/nostr.json?name=$username'),
|
||||
onSuccess: (response, label) {
|
||||
@@ -92,10 +97,6 @@ mixin UserAdapter on NostrAdapter<User> {
|
||||
);
|
||||
}
|
||||
|
||||
if (publicKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final req = RelayRequest(
|
||||
authors: {publicKey},
|
||||
kinds: {kind, ...?params?.remove('kinds')},
|
||||
@@ -108,69 +109,3 @@ mixin UserAdapter on NostrAdapter<User> {
|
||||
return data.model?..saveLocal();
|
||||
}
|
||||
}
|
||||
|
||||
mixin NostrAdapter<T extends ZapstoreEvent<T>> on Adapter<T> {
|
||||
late final RelayMessageNotifier notifier =
|
||||
ref.read(relayMessageNotifierProvider.notifier);
|
||||
|
||||
Map<int, String> kindType = {
|
||||
0: 'users',
|
||||
3: 'users',
|
||||
1063: 'fileMetadata',
|
||||
30063: 'releases',
|
||||
32267: 'apps'
|
||||
};
|
||||
|
||||
@override
|
||||
DeserializedData<T> deserialize(Object? data, {String? key}) {
|
||||
final list = data is Iterable ? data : [data as Map];
|
||||
final models = <T>[];
|
||||
final included = <DataModelMixin>[];
|
||||
|
||||
for (final e in list) {
|
||||
final map = e as Map<String, dynamic>;
|
||||
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<T>);
|
||||
} else {
|
||||
final newData = adapters[xType]!.deserialize(map);
|
||||
included.addAll(newData.models as Iterable<DataModelMixin>);
|
||||
}
|
||||
}
|
||||
}
|
||||
return DeserializedData<T>(models, included: included);
|
||||
}
|
||||
|
||||
int get kind =>
|
||||
kindType.entries.firstWhere((e) => e.value == internalType).key;
|
||||
|
||||
@override
|
||||
Future<List<T>> findAll(
|
||||
{bool? remote,
|
||||
bool? background,
|
||||
Map<String, dynamic>? params,
|
||||
Map<String, String>? headers,
|
||||
bool? syncLocal,
|
||||
OnSuccessAll<T>? onSuccess,
|
||||
OnErrorAll<T>? 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ User _$UserFromJson(Map<String, dynamic> 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<dynamic>)
|
||||
.map((e) => (e as List<dynamic>).map((e) => e as String).toList())
|
||||
.toList()
|
||||
|
||||
@@ -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<String> 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)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
),
|
||||
|
||||
+13
-13
@@ -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(
|
||||
|
||||
+31
-31
@@ -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<String> 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user