diff --git a/lib/main.data.dart b/lib/main.data.dart index d10799a..c02e4ed 100644 --- a/lib/main.data.dart +++ b/lib/main.data.dart @@ -9,6 +9,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:zapstore/models/app_curation_set.dart'; import 'package:zapstore/models/app.dart'; import 'package:zapstore/models/file_metadata.dart'; +import 'package:zapstore/models/local_app.dart'; import 'package:zapstore/models/release.dart'; import 'package:zapstore/models/settings.dart'; import 'package:zapstore/models/user.dart'; @@ -17,6 +18,7 @@ final adapterProvidersMap = >>{ 'appCurationSets': appCurationSetsAdapterProvider, 'apps': appsAdapterProvider, 'fileMetadata': fileMetadataAdapterProvider, +'localApps': localAppsAdapterProvider, 'releases': releasesAdapterProvider, 'settings': settingsAdapterProvider, 'users': usersAdapterProvider @@ -26,6 +28,7 @@ extension AdapterWidgetRefX on WidgetRef { Adapter get appCurationSets => watch(appCurationSetsAdapterProvider)..internalWatch = watch; Adapter get apps => watch(appsAdapterProvider)..internalWatch = watch; Adapter get fileMetadata => watch(fileMetadataAdapterProvider)..internalWatch = watch; + Adapter get localApps => watch(localAppsAdapterProvider)..internalWatch = watch; Adapter get releases => watch(releasesAdapterProvider)..internalWatch = watch; Adapter get settings => watch(settingsAdapterProvider)..internalWatch = watch; Adapter get users => watch(usersAdapterProvider)..internalWatch = watch; @@ -36,6 +39,7 @@ extension AdapterRefX on Ref { Adapter get appCurationSets => watch(appCurationSetsAdapterProvider)..internalWatch = watch as Watcher; Adapter get apps => watch(appsAdapterProvider)..internalWatch = watch as Watcher; Adapter get fileMetadata => watch(fileMetadataAdapterProvider)..internalWatch = watch as Watcher; + Adapter get localApps => watch(localAppsAdapterProvider)..internalWatch = watch as Watcher; Adapter get releases => watch(releasesAdapterProvider)..internalWatch = watch as Watcher; Adapter get settings => watch(settingsAdapterProvider)..internalWatch = watch as Watcher; Adapter get users => watch(usersAdapterProvider)..internalWatch = watch as Watcher; diff --git a/lib/models/app.dart b/lib/models/app.dart index 0f8f9f7..1593565 100644 --- a/lib/models/app.dart +++ b/lib/models/app.dart @@ -15,6 +15,7 @@ import 'package:permission_handler/permission_handler.dart'; import 'package:purplebase/purplebase.dart'; import 'package:zapstore/main.data.dart'; import 'package:zapstore/models/file_metadata.dart'; +import 'package:zapstore/models/local_app.dart'; import 'package:zapstore/models/nostr_adapter.dart'; import 'package:zapstore/models/release.dart'; import 'package:zapstore/models/user.dart'; @@ -25,31 +26,22 @@ import 'package:zapstore/utils/system_info.dart'; part 'app.g.dart'; -const kAndroidMimeType = 'application/vnd.android.package-archive'; - @DataAdapter([NostrAdapter, AppAdapter]) class App extends BaseApp with DataModelMixin { final HasMany releases; final BelongsTo signer; final BelongsTo developer; + final BelongsTo localApp; App.fromJson(super.map) : developer = belongsTo(map['developer']), releases = hasMany(map['releases']), signer = belongsTo(map['signer']), + localApp = belongsTo(map['localApp']), super.fromJson(); Map toJson() => super.toMap(); - String? get installedVersion => tagMap['installedVersion']?.firstOrNull; - int? get installedVersionCode => - int.tryParse(tagMap['installedVersionCode']?.firstOrNull ?? ''); - AppInstallStatus? get status { - final _status = tagMap['status']?.firstOrNull; - if (_status == null) return null; - return AppInstallStatus.values.firstWhereOrNull((e) => e.name == _status); - } - FileMetadata? get latestMetadata { return releases.ordered.firstOrNull?.artifacts .where((a) => @@ -58,9 +50,9 @@ class App extends BaseApp with DataModelMixin { .firstOrNull; } - bool get canInstall => status == AppInstallStatus.installable; - bool get canUpdate => status == AppInstallStatus.updatable; - bool get isUpdated => status == AppInstallStatus.updated; + bool get canInstall => localApp.value?.status == AppInstallStatus.installable; + bool get canUpdate => localApp.value?.status == AppInstallStatus.updatable; + bool get isUpdated => localApp.value?.status == AppInstallStatus.updated; Future install() async { if (!canInstall && !canUpdate) { @@ -107,7 +99,8 @@ class App extends BaseApp with DataModelMixin { final result = await InstallPlugin.install(file.path); if (result['isSuccess']) { await file.delete(); - await adapter.updateInstallStatus(app: this); + await adapter.ref.localApps.localAppAdapter + .updateInstallStatus(appId: id?.toString()); notifier.state = IdleInstallProgress(); } else { const msg = 'Android rejected installation'; @@ -171,10 +164,22 @@ class App extends BaseApp with DataModelMixin { mixin AppAdapter on Adapter { Future> loadAppModels(Map params) async { final includes = params.remove('includes') ?? false; - final apps = await super.findAll(params: params); - final releases = await ref.releases.findAll(params: { - '#a': apps.map((app) => app.getReplaceableEventLink().formatted) - }); + + final apps = await super.findAll( + params: { + ...params, + '#f': ['android-arm64-v8a'], + }, + ); + // Find all appid@version ($3) as we need to pick one tag to query on + // (filters by kind ($1) and pubkey ($2) done locally) + final releases = await ref.releases.findAll( + params: { + '#d': apps + .map((app) => app.linkedReplaceableEvents.firstOrNull?.$3) + .nonNulls + }, + ); final metadataIds = releases.map((r) => r.tagMap['e']).nonNulls.expand((_) => _); @@ -209,17 +214,19 @@ mixin AppAdapter on Adapter { if (appIds.isNotEmpty) { params['#d'] = appIds; params.remove('installed'); - await loadAppModels(params); + final apps = await loadAppModels(params); // Once apps are loaded, check for installed status - return await updateInstallStatus(); + await ref.localApps.localAppAdapter.updateInstallStatus(); + return apps; } } return await loadAppModels(params); } List findWhereIdInLocal(Iterable appIds) { + const len = 5 + 64 + 3; // kind + pubkey + separators length final result = db.select( - 'SELECT key, data, json_extract(data, \'\$.id\') AS id from apps where id in (${appIds.map((_) => '?').join(', ')})', + 'SELECT key, data, substr(json_extract(data, \'\$.id\'), $len) AS id from apps where id in (${appIds.map((_) => '?').join(', ')})', appIds.toList()); return deserializeFromResult(result); } @@ -242,6 +249,8 @@ mixin AppAdapter on Adapter { deleteLocalById(id); return null; } + await ref.localApps.localAppAdapter + .updateInstallStatus(appId: id.toString()); return apps.first; } @@ -253,131 +262,26 @@ mixin AppAdapter on Adapter { return { for (final i in infos!) if (i.packageName != null && - ![ - 'android', - 'com.android', - 'com.google', - 'org.chromium.webview_shell', - 'app.grapheneos', - 'app.vanadium' - ].any((e) => i.packageName!.startsWith(e))) + !kExcludedAppIdNamespaces.any((e) => i.packageName!.startsWith(e))) i.packageName!, }; } - Future> updateInstallStatus({App? app}) async { - if (!Platform.isAndroid) { - return []; - } - - final updatedApps = []; - final infos = app != null - ? [await packageManager.getPackageInfo(packageName: app.id.toString())] - .nonNulls - : await packageManager.getInstalledPackages(); - - final installedPackageInfos = infos!.where((i) => ![ - 'android', - 'com.android', - 'com.google', - 'org.chromium.webview_shell', - 'app.grapheneos', - 'app.vanadium' - ].any((e) => i.packageName!.startsWith(e))); - - final apps = findManyLocalByIds( - installedPackageInfos.map((i) => i.packageName).nonNulls); - - for (final i in installedPackageInfos) { - var app = apps.firstWhereOrNull((app) => i.packageName == app.id); - final installedVersion = i.versionName; - final installedVersionCode = i.versionCode; - - if (app == null) continue; - - final AppInstallStatus status = - _determineInstallStatus(app, installedVersion, installedVersionCode); - - if (app.installedVersionCode != installedVersionCode) { - app.addTags({ - ('installedVersion', installedVersion), - ('installedVersionCode', installedVersionCode), - ('status', status.name) - }, replace: true); - app.saveLocal(); - } - - updatedApps.add(app); - } - - // Update number of apps - final rs = db.select( - 'SELECT count(*) as c FROM apps, json_each(json_extract(apps.data, \'\$.tags\')) WHERE json_extract(value, \'\$[0]\') is \'status\' AND json_extract(value, \'\$[1]\') is \'updatable\''); - ref.read(appsToUpdateProvider.notifier).state = rs.first['c']; - - return updatedApps; - } - - AppInstallStatus _determineInstallStatus( - App app, String? installedVersion, int? installedVersionCode) { - if (app.releases.isEmpty) { - return AppInstallStatus.loading; - } - - if (app.releases.isNotEmpty && app.latestMetadata == null) { - return AppInstallStatus.differentArchitecture; - } - if (installedVersion == null) { - return AppInstallStatus.installable; - } - var comp = 0; - if (app.latestMetadata!.versionCode != null && - installedVersionCode != null && - app.id != 'store.zap.app') { - // Note: need to exclude zap.store because development versions always - // carry a lower version code (e.g. 12) than published ones (e.g. 2012) - comp = app.latestMetadata!.versionCode!.compareTo(installedVersionCode); - } - - if (comp == 0) { - comp = app.latestMetadata!.version!.compareTo(installedVersion); - } - - if (comp == 1) return AppInstallStatus.updatable; - if (comp == 0) return AppInstallStatus.updated; - // else it's a downgrade, which is not installable - return AppInstallStatus.downgrade; - } - @override DeserializedData deserialize(Object? data, {String? key}) { final list = data is Iterable ? data : [data as Map]; - for (final e in list) { - e['id'] = (e['tags'] as Iterable).where((t) => t[0] == 'd').first.last; - } - final savedApps = findManyLocalByIds(list.map((e) => e['id'])); - for (final e in list) { - final map = e as Map; - final tags = map['tags'] as Iterable; + for (final Map map in list) { + final tagMap = tagsToMap(map['tags']); + final appId = tagMap['d']!.first; + map['id'] = + (map['kind'] as int, map['pubkey'].toString(), appId).formatted; map['signer'] = map['pubkey']; - final zapTags = tags.where((t) => t[0] == 'zap'); - if (zapTags.length == 1) { - map['developer'] = (zapTags.first as List)[1]; - } + map['developer'] = tagMap['zap']?.firstOrNull?[1]; + map['localApp'] = appId; } - final deserialized = super.deserialize(data); - // Preserve installation data - for (final app in deserialized.models) { - final savedApp = savedApps.firstWhereOrNull((s) => s.id == app.id); - app.addTags({ - ('installedVersion', savedApp?.installedVersion), - ('installedVersionCode', savedApp?.installedVersionCode), - ('status', savedApp?.status?.name) - }, replace: true); - } - return deserialized; + return super.deserialize(data); } } @@ -408,17 +312,6 @@ Future _isHashMismatch(String path, String hash) async { }); } -// install support - -enum AppInstallStatus { - updated, - updatable, - installable, - downgrade, - differentArchitecture, - loading -} - // class sealed class AppInstallProgress {} @@ -444,3 +337,18 @@ final installationProgressProvider = (_, arg) => IdleInstallProgress()); final appsToUpdateProvider = StateProvider((_) => 0); + +// Constants + +const kAndroidMimeType = 'application/vnd.android.package-archive'; + +const kExcludedAppIdNamespaces = [ + 'android', + 'com.android', + 'com.google', + 'com.samsung', + 'com.sec.android', + 'org.chromium.webview_shell', + 'app.grapheneos', + 'app.vanadium', +]; diff --git a/lib/models/app.g.dart b/lib/models/app.g.dart index 03549df..ae662c8 100644 --- a/lib/models/app.g.dart +++ b/lib/models/app.g.dart @@ -28,6 +28,12 @@ mixin _$AppAdapter on Adapter { type: 'users', kind: 'BelongsTo', instance: (_) => (_ as App).developer, + ), + 'localApp': RelationshipMeta( + name: 'localApp', + type: 'localApps', + kind: 'BelongsTo', + instance: (_) => (_ as App).localApp, ) }; @@ -81,4 +87,11 @@ extension AppRelationshipGraphNodeX on RelationshipGraphNode { return meta.clone( parent: this is RelationshipMeta ? this as RelationshipMeta : null); } + + RelationshipGraphNode get localApp { + final meta = _$AppAdapter._kAppRelationshipMetas['localApp'] + as RelationshipMeta; + return meta.clone( + parent: this is RelationshipMeta ? this as RelationshipMeta : null); + } } diff --git a/lib/models/file_metadata.dart b/lib/models/file_metadata.dart index 4aaef88..cfc5277 100644 --- a/lib/models/file_metadata.dart +++ b/lib/models/file_metadata.dart @@ -1,5 +1,6 @@ import 'package:flutter_data/flutter_data.dart'; import 'package:purplebase/purplebase.dart'; +import 'package:zapstore/models/app.dart'; import 'package:zapstore/models/nostr_adapter.dart'; import 'package:zapstore/models/release.dart'; import 'package:zapstore/models/user.dart'; @@ -42,11 +43,21 @@ mixin FileMetadataAdapter on Adapter { for (final e in list) { final map = e as Map; map['author'] = map['pubkey']; - // Ensure tags are strings - for (var t in map['tags']) { - t[1] = t[1].toString(); - } } return super.deserialize(data); } + + Future> zzz(List appIds) async { + final req = RelayRequest( + kinds: {1063}, + tags: { + '#m': [kAndroidMimeType], + '#f': ['android-arm64-v8a'], + }, + ); + + final result = await (this as NostrAdapter).relay.queryRaw(req); + final deserialized = await deserializeAsync(result, save: true); + return deserialized.models; + } } diff --git a/lib/models/local_app.dart b/lib/models/local_app.dart new file mode 100644 index 0000000..a6594b5 --- /dev/null +++ b/lib/models/local_app.dart @@ -0,0 +1,128 @@ +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:flutter_data/flutter_data.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:zapstore/main.data.dart'; +import 'package:zapstore/models/app.dart'; +import 'package:zapstore/utils/system_info.dart'; + +part 'local_app.g.dart'; + +@DataAdapter([LocalAppAdapter]) +@JsonSerializable() +class LocalApp extends DataModel { + @override + final Object? id; + final String? installedVersion; + final int? installedVersionCode; + final AppInstallStatus? status; + // final String? apkSignatureHash; + + LocalApp( + {required this.id, + this.installedVersion, + this.installedVersionCode, + this.status}); + + LocalApp copyWith( + {String? installedVersion, + int? installedVersionCode, + AppInstallStatus? status}) { + return LocalApp( + id: id, + installedVersion: installedVersion ?? this.installedVersion, + installedVersionCode: installedVersionCode ?? this.installedVersionCode, + status: status ?? this.status, + ); + } +} + +mixin LocalAppAdapter on Adapter { + Future updateInstallStatus({String? appId}) async { + if (!Platform.isAndroid) { + return; + } + + final infos = appId != null + ? [await packageManager.getPackageInfo(packageName: appId)].nonNulls + : await packageManager.getInstalledPackages(); + + final installedPackageInfos = infos!.where((i) => + !kExcludedAppIdNamespaces.any((e) => i.packageName!.startsWith(e))); + + final localApps = findManyLocalByIds( + installedPackageInfos.map((i) => i.packageName).nonNulls); + + for (final i in installedPackageInfos) { + final appId = i.packageName!; + final localApp = localApps.firstWhereOrNull((app) => appId == app.id) ?? + LocalApp(id: i.packageName!); + final installedVersion = i.versionName; + final installedVersionCode = i.versionCode; + + final app = ref.apps.appAdapter.findWhereIdInLocal([appId]).firstOrNull; + final status = + determineInstallStatus(app, installedVersion, installedVersionCode); + + localApp + .copyWith( + installedVersion: installedVersion, + installedVersionCode: installedVersionCode, + status: status) + .saveLocal(); + } + + // Update number of apps + final rs = db.select( + 'SELECT count(*) as c FROM localApps WHERE json_extract(data, \'\$.status\') is \'updatable\''); + ref.read(appsToUpdateProvider.notifier).state = rs.first['c']; + } + + AppInstallStatus? determineInstallStatus( + App? app, String? installedVersion, int? installedVersionCode) { + if (app == null || app.releases.isEmpty || app.latestMetadata == null) { + return null; + } + if (installedVersion == null) { + return AppInstallStatus.installable; + } + var comp = 0; + if (app.latestMetadata!.versionCode != null && + installedVersionCode != null && + app.id != 'store.zap.app') { + // Note: need to exclude zap.store because development versions always + // carry a lower version code (e.g. 12) than published ones (e.g. 2012) + comp = app.latestMetadata!.versionCode!.compareTo(installedVersionCode); + } + + if (comp == 0) { + comp = app.latestMetadata!.version!.compareTo(installedVersion); + } + + if (comp == 1) return AppInstallStatus.updatable; + if (comp == 0) return AppInstallStatus.updated; + // else it's a downgrade, which is not installable + return AppInstallStatus.downgrade; + } + + @override + LocalApp deserializeLocal(Map map, {String? key}) { + // TODO: implement deserializeLocal + return super.deserializeLocal(map, key: key); + } + + @override + Map serializeLocal(LocalApp model, + {bool withRelationships = true}) { + final z = super.serializeLocal(model, withRelationships: withRelationships); + return z; + } +} + +enum AppInstallStatus { + updated, + updatable, + installable, + downgrade, +} diff --git a/lib/models/local_app.g.dart b/lib/models/local_app.g.dart new file mode 100644 index 0000000..228015c --- /dev/null +++ b/lib/models/local_app.g.dart @@ -0,0 +1,68 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'local_app.dart'; + +// ************************************************************************** +// AdapterGenerator +// ************************************************************************** + +// ignore_for_file: non_constant_identifier_names, duplicate_ignore + +mixin _$LocalAppAdapter on Adapter { + static final Map _kLocalAppRelationshipMetas = {}; + + @override + Map get relationshipMetas => + _kLocalAppRelationshipMetas; + + @override + LocalApp deserializeLocal(map, {String? key}) { + map = transformDeserialize(map); + return internalWrapStopInit(() => _$LocalAppFromJson(map), key: key); + } + + @override + Map serializeLocal(model, {bool withRelationships = true}) { + final map = _$LocalAppToJson(model); + return transformSerialize(map, withRelationships: withRelationships); + } +} + +final _localAppsFinders = {}; + +class $LocalAppAdapter = Adapter + with _$LocalAppAdapter, LocalAppAdapter; + +final localAppsAdapterProvider = Provider>( + (ref) => $LocalAppAdapter(ref, InternalHolder(_localAppsFinders))); + +extension LocalAppAdapterX on Adapter { + LocalAppAdapter get localAppAdapter => this as LocalAppAdapter; +} + +extension LocalAppRelationshipGraphNodeX on RelationshipGraphNode {} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +LocalApp _$LocalAppFromJson(Map json) => LocalApp( + id: json['id'], + installedVersion: json['installedVersion'] as String?, + installedVersionCode: (json['installedVersionCode'] as num?)?.toInt(), + status: $enumDecodeNullable(_$AppInstallStatusEnumMap, json['status']), + ); + +Map _$LocalAppToJson(LocalApp instance) => { + 'id': instance.id, + 'installedVersion': instance.installedVersion, + 'installedVersionCode': instance.installedVersionCode, + 'status': _$AppInstallStatusEnumMap[instance.status], + }; + +const _$AppInstallStatusEnumMap = { + AppInstallStatus.updated: 'updated', + AppInstallStatus.updatable: 'updatable', + AppInstallStatus.installable: 'installable', + AppInstallStatus.downgrade: 'downgrade', +}; diff --git a/lib/models/nostr_adapter.dart b/lib/models/nostr_adapter.dart index a5a558f..55ce1db 100644 --- a/lib/models/nostr_adapter.dart +++ b/lib/models/nostr_adapter.dart @@ -2,7 +2,7 @@ import 'package:flutter_data/flutter_data.dart'; import 'package:purplebase/purplebase.dart'; // NOTE: Very important to use const in relay args to preserve equality in Riverpod families -const kAppRelays = ['wss://relay.zap.store']; +const kAppRelays = ['ws://10.0.2.2:3000']; const kSocialRelays = ['wss://relay.primal.net', 'wss://relay.nostr.band']; mixin NostrAdapter> on Adapter { @@ -24,10 +24,15 @@ mixin NostrAdapter> on Adapter { for (final e in list) { final map = e as Map; - // ID should be the identifier in PREs - final dTags = (map['tags'] as Iterable).where((t) => t[0] == 'd'); - if (dTags.length == 1) { - map['id'] = (dTags.first as List)[1]; + // ID should be the replaceable link/reference so as to make it replaceable in local db too + final tagMap = tagsToMap(map['tags']); + final isReplaceable = tagMap.containsKey('d'); + if (isReplaceable) { + map['id'] = ( + map['kind'] as int, + map['pubkey'].toString(), + tagMap['d']?.firstOrNull + ).formatted; } // Collect models for current kind and included for others @@ -58,9 +63,11 @@ mixin NostrAdapter> on Adapter { } final additionalKinds = params?.remove('kinds'); + final limit = params?.remove('limit'); final req = RelayRequest( kinds: {kind, ...?additionalKinds}, tags: params ?? {}, + limit: limit, ); final result = await relay.queryRaw(req); diff --git a/lib/models/release.dart b/lib/models/release.dart index ff1db79..6611e8c 100644 --- a/lib/models/release.dart +++ b/lib/models/release.dart @@ -36,15 +36,11 @@ mixin ReleaseAdapter on Adapter { @override DeserializedData deserialize(Object? data, {String? key}) { final list = data is Iterable ? data : [data as Map]; - for (final e in list) { - final map = e as Map; - final eTags = (map['tags'] as Iterable) - .where((e) => e.first == 'e') - .map((e) => e[1].toString()); - map['artifacts'] = eTags.toList(); - final appIdentifier = - map['tags'].firstWhere((t) => t.first == 'd')[1].split('@').first; - map['app'] = appIdentifier; + + for (final Map map in list) { + final tagMap = tagsToMap(map['tags']); + map['app'] = tagMap['a']?.firstOrNull; + map['artifacts'] = tagMap['e']!.toList(); } return super.deserialize(data); } diff --git a/lib/models/user.dart b/lib/models/user.dart index d4f34c0..6a667f3 100644 --- a/lib/models/user.dart +++ b/lib/models/user.dart @@ -32,6 +32,7 @@ class User extends BaseUser with DataModelMixin { } mixin UserAdapter on NostrAdapter { + // TODO: All this still necessary with purplebase? @override DeserializedData deserialize(Object? data, {String? key}) { final Iterable> list = @@ -54,7 +55,6 @@ mixin UserAdapter on NostrAdapter { .toList() .groupSetsBy((e) => e['pubkey'] as String); - // TODO: This still necessary with purplebase? // collect contacts and then assign them to user final included = >{}; for (final _ in k3s.entries) { diff --git a/lib/navigation/router.dart b/lib/navigation/router.dart index a2f9fe0..0306cb0 100644 --- a/lib/navigation/router.dart +++ b/lib/navigation/router.dart @@ -6,6 +6,7 @@ import 'package:purplebase/purplebase.dart'; import 'package:zapstore/main.dart'; import 'package:zapstore/main.data.dart'; import 'package:zapstore/models/app.dart'; +import 'package:zapstore/models/local_app.dart'; import 'package:zapstore/models/nostr_adapter.dart'; import 'package:zapstore/navigation/desktop_scaffold.dart'; import 'package:zapstore/navigation/mobile_scaffold.dart'; @@ -145,11 +146,11 @@ final dataLibrariesInitializer = FutureProvider((ref) async { await Future.wait([relay.initialize(), socialRelays.initialize()]); // Trigger app install status calculations - await ref.apps.appAdapter.updateInstallStatus(); + await ref.localApps.localAppAdapter.updateInstallStatus(); _lifecycleListener = AppLifecycleListener( onStateChange: (state) async { if (state == AppLifecycleState.resumed) { - await ref.apps.appAdapter.updateInstallStatus(); + await ref.localApps.localAppAdapter.updateInstallStatus(); } }, ); diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index 2e7aa67..508ddb6 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -134,7 +134,7 @@ class SearchResultNotifier extends AsyncNotifier?> { Future?> build() async { final query = ref.watch(searchQueryProvider); if (query != null) { - return await ref.apps.findAll(params: {'search': query}); + return await ref.apps.findAll(params: {'search': query, 'limit': 16}); } return null; } diff --git a/lib/widgets/install_button.dart b/lib/widgets/install_button.dart index 1bb0762..dc99e76 100644 --- a/lib/widgets/install_button.dart +++ b/lib/widgets/install_button.dart @@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:percent_indicator/linear_percent_indicator.dart'; import 'package:zapstore/main.data.dart'; import 'package:zapstore/models/app.dart'; +import 'package:zapstore/models/local_app.dart'; import 'package:zapstore/models/settings.dart'; import 'package:zapstore/utils/extensions.dart'; import 'package:zapstore/utils/system_info.dart'; @@ -27,10 +28,10 @@ class InstallButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final progress = ref.watch(installationProgressProvider(app.identifier!)); + final status = app.localApp.value?.status; return GestureDetector( - onTap: switch (app.status) { - AppInstallStatus.differentArchitecture => null, + onTap: switch (status) { AppInstallStatus.downgrade => null, AppInstallStatus.updated => () { packageManager.openApp(app.id!.toString()); @@ -68,7 +69,7 @@ class InstallButton extends ConsumerWidget { percent: switch (progress) { VerifyingHashProgress() => 1, DownloadingInstallProgress(:final progress) => progress, - _ => switch (app.status) { + _ => switch (status) { AppInstallStatus.updated => 1, _ => 0, }, @@ -81,15 +82,9 @@ class InstallButton extends ConsumerWidget { barRadius: Radius.circular(20), padding: EdgeInsets.all(0), animateFromLastPercent: true, - center: switch (app.status) { - AppInstallStatus.loading => - SizedBox(width: 14, height: 14, child: CircularProgressIndicator()), - AppInstallStatus.differentArchitecture => Text( - 'Sorry, release does not support your device', - textAlign: TextAlign.center, - ), + center: switch (status) { AppInstallStatus.downgrade => Text( - 'Installed version ${app.installedVersion ?? ''} is higher, can\'t downgrade', + 'Installed version ${app.localApp.value?.installedVersion ?? ''} is higher, can\'t downgrade', textAlign: TextAlign.center, ), AppInstallStatus.updated => Text('Open'), diff --git a/lib/widgets/latest_releases_container.dart b/lib/widgets/latest_releases_container.dart index 4b6f6da..667434f 100644 --- a/lib/widgets/latest_releases_container.dart +++ b/lib/widgets/latest_releases_container.dart @@ -38,7 +38,7 @@ class LatestReleasesContainer extends HookConsumerWidget { class LatestReleasesAppNotifier extends AutoDisposeAsyncNotifier> { @override Future> build() async { - final timer = Timer.periodic(Duration(hours: 4), (_) => fetch()); + final timer = Timer.periodic(Duration(minutes: 10), (_) => fetch()); ref.onDispose(timer.cancel); fetch(); // .catchError((e) { @@ -49,20 +49,15 @@ class LatestReleasesAppNotifier extends AutoDisposeAsyncNotifier> { } List localFetch() { - return ref.releases + return ref.apps .findAllLocal() .sorted((a, b) => b.createdAt!.compareTo(a.createdAt!)) .take(10) - .map((r) => r.app.value) - .nonNulls - .toSet() .toList(); } Future fetch() async { - final releases = await ref.releases.findAll(params: {'limit': 10}); - final appIds = releases.map((r) => r.app.id!.toString()).toSet(); - await ref.apps.findAll(params: {'#d': appIds}); + await ref.apps.findAll(params: {'limit': 10}); update((_) => localFetch()); } } diff --git a/lib/widgets/signer_and_developer_row.dart b/lib/widgets/signer_and_developer_row.dart index 0aea39e..c1085d5 100644 --- a/lib/widgets/signer_and_developer_row.dart +++ b/lib/widgets/signer_and_developer_row.dart @@ -24,7 +24,9 @@ class SignerAndDeveloperRow extends StatelessWidget { launchUrl(url); }, child: AuthorContainer( - user: app.developer.value!, text: 'Built by', oneLine: false), + user: app.developer.value!, + text: 'Developed by', + oneLine: false), ), if (app.signer.isPresent) GestureDetector( diff --git a/lib/widgets/versioned_app_header.dart b/lib/widgets/versioned_app_header.dart index c9a032d..87d58c3 100644 --- a/lib/widgets/versioned_app_header.dart +++ b/lib/widgets/versioned_app_header.dart @@ -47,7 +47,9 @@ class VersionedAppHeader extends StatelessWidget { children: [ if (isUpdate) PillWidget( - text: app.installedVersion!, color: Colors.grey[800]!), + text: app.localApp.value!.installedVersion!, + color: Colors.grey[800]!, + ), if (isUpdate) Icon(Icons.arrow_right), if (app.latestMetadata != null) PillWidget( diff --git a/pubspec.yaml b/pubspec.yaml index 04d756d..c6acfb4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: cupertino_icons: ^1.0.2 flutter_data: ^2.0.0-rc3 hooks_riverpod: ^2.4.9 - json_annotation: ^4.8.1 + json_annotation: ^4.9.0 flutter_hooks: ^0.20.4 build_runner: ^2.4.8 collection: ^1.18.0