mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 20:48:24 +00:00
Data retrieval revamp, local apps
This commit is contained in:
@@ -9,6 +9,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:zapstore/models/app_curation_set.dart';
|
import 'package:zapstore/models/app_curation_set.dart';
|
||||||
import 'package:zapstore/models/app.dart';
|
import 'package:zapstore/models/app.dart';
|
||||||
import 'package:zapstore/models/file_metadata.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/release.dart';
|
||||||
import 'package:zapstore/models/settings.dart';
|
import 'package:zapstore/models/settings.dart';
|
||||||
import 'package:zapstore/models/user.dart';
|
import 'package:zapstore/models/user.dart';
|
||||||
@@ -17,6 +18,7 @@ final adapterProvidersMap = <String, Provider<Adapter<DataModelMixin>>>{
|
|||||||
'appCurationSets': appCurationSetsAdapterProvider,
|
'appCurationSets': appCurationSetsAdapterProvider,
|
||||||
'apps': appsAdapterProvider,
|
'apps': appsAdapterProvider,
|
||||||
'fileMetadata': fileMetadataAdapterProvider,
|
'fileMetadata': fileMetadataAdapterProvider,
|
||||||
|
'localApps': localAppsAdapterProvider,
|
||||||
'releases': releasesAdapterProvider,
|
'releases': releasesAdapterProvider,
|
||||||
'settings': settingsAdapterProvider,
|
'settings': settingsAdapterProvider,
|
||||||
'users': usersAdapterProvider
|
'users': usersAdapterProvider
|
||||||
@@ -26,6 +28,7 @@ extension AdapterWidgetRefX on WidgetRef {
|
|||||||
Adapter<AppCurationSet> get appCurationSets => watch(appCurationSetsAdapterProvider)..internalWatch = watch;
|
Adapter<AppCurationSet> get appCurationSets => watch(appCurationSetsAdapterProvider)..internalWatch = watch;
|
||||||
Adapter<App> get apps => watch(appsAdapterProvider)..internalWatch = watch;
|
Adapter<App> get apps => watch(appsAdapterProvider)..internalWatch = watch;
|
||||||
Adapter<FileMetadata> get fileMetadata => watch(fileMetadataAdapterProvider)..internalWatch = watch;
|
Adapter<FileMetadata> get fileMetadata => watch(fileMetadataAdapterProvider)..internalWatch = watch;
|
||||||
|
Adapter<LocalApp> get localApps => watch(localAppsAdapterProvider)..internalWatch = watch;
|
||||||
Adapter<Release> get releases => watch(releasesAdapterProvider)..internalWatch = watch;
|
Adapter<Release> get releases => watch(releasesAdapterProvider)..internalWatch = watch;
|
||||||
Adapter<Settings> get settings => watch(settingsAdapterProvider)..internalWatch = watch;
|
Adapter<Settings> get settings => watch(settingsAdapterProvider)..internalWatch = watch;
|
||||||
Adapter<User> get users => watch(usersAdapterProvider)..internalWatch = watch;
|
Adapter<User> get users => watch(usersAdapterProvider)..internalWatch = watch;
|
||||||
@@ -36,6 +39,7 @@ extension AdapterRefX on Ref {
|
|||||||
Adapter<AppCurationSet> get appCurationSets => watch(appCurationSetsAdapterProvider)..internalWatch = watch as Watcher;
|
Adapter<AppCurationSet> get appCurationSets => watch(appCurationSetsAdapterProvider)..internalWatch = watch as Watcher;
|
||||||
Adapter<App> get apps => watch(appsAdapterProvider)..internalWatch = watch as Watcher;
|
Adapter<App> get apps => watch(appsAdapterProvider)..internalWatch = watch as Watcher;
|
||||||
Adapter<FileMetadata> get fileMetadata => watch(fileMetadataAdapterProvider)..internalWatch = watch as Watcher;
|
Adapter<FileMetadata> get fileMetadata => watch(fileMetadataAdapterProvider)..internalWatch = watch as Watcher;
|
||||||
|
Adapter<LocalApp> get localApps => watch(localAppsAdapterProvider)..internalWatch = watch as Watcher;
|
||||||
Adapter<Release> get releases => watch(releasesAdapterProvider)..internalWatch = watch as Watcher;
|
Adapter<Release> get releases => watch(releasesAdapterProvider)..internalWatch = watch as Watcher;
|
||||||
Adapter<Settings> get settings => watch(settingsAdapterProvider)..internalWatch = watch as Watcher;
|
Adapter<Settings> get settings => watch(settingsAdapterProvider)..internalWatch = watch as Watcher;
|
||||||
Adapter<User> get users => watch(usersAdapterProvider)..internalWatch = watch as Watcher;
|
Adapter<User> get users => watch(usersAdapterProvider)..internalWatch = watch as Watcher;
|
||||||
|
|||||||
+55
-147
@@ -15,6 +15,7 @@ import 'package:permission_handler/permission_handler.dart';
|
|||||||
import 'package:purplebase/purplebase.dart';
|
import 'package:purplebase/purplebase.dart';
|
||||||
import 'package:zapstore/main.data.dart';
|
import 'package:zapstore/main.data.dart';
|
||||||
import 'package:zapstore/models/file_metadata.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/nostr_adapter.dart';
|
||||||
import 'package:zapstore/models/release.dart';
|
import 'package:zapstore/models/release.dart';
|
||||||
import 'package:zapstore/models/user.dart';
|
import 'package:zapstore/models/user.dart';
|
||||||
@@ -25,31 +26,22 @@ import 'package:zapstore/utils/system_info.dart';
|
|||||||
|
|
||||||
part 'app.g.dart';
|
part 'app.g.dart';
|
||||||
|
|
||||||
const kAndroidMimeType = 'application/vnd.android.package-archive';
|
|
||||||
|
|
||||||
@DataAdapter([NostrAdapter, AppAdapter])
|
@DataAdapter([NostrAdapter, AppAdapter])
|
||||||
class App extends BaseApp with DataModelMixin<App> {
|
class App extends BaseApp with DataModelMixin<App> {
|
||||||
final HasMany<Release> releases;
|
final HasMany<Release> releases;
|
||||||
final BelongsTo<User> signer;
|
final BelongsTo<User> signer;
|
||||||
final BelongsTo<User> developer;
|
final BelongsTo<User> developer;
|
||||||
|
final BelongsTo<LocalApp> localApp;
|
||||||
|
|
||||||
App.fromJson(super.map)
|
App.fromJson(super.map)
|
||||||
: developer = belongsTo(map['developer']),
|
: developer = belongsTo(map['developer']),
|
||||||
releases = hasMany(map['releases']),
|
releases = hasMany(map['releases']),
|
||||||
signer = belongsTo(map['signer']),
|
signer = belongsTo(map['signer']),
|
||||||
|
localApp = belongsTo(map['localApp']),
|
||||||
super.fromJson();
|
super.fromJson();
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => super.toMap();
|
Map<String, dynamic> 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 {
|
FileMetadata? get latestMetadata {
|
||||||
return releases.ordered.firstOrNull?.artifacts
|
return releases.ordered.firstOrNull?.artifacts
|
||||||
.where((a) =>
|
.where((a) =>
|
||||||
@@ -58,9 +50,9 @@ class App extends BaseApp with DataModelMixin<App> {
|
|||||||
.firstOrNull;
|
.firstOrNull;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get canInstall => status == AppInstallStatus.installable;
|
bool get canInstall => localApp.value?.status == AppInstallStatus.installable;
|
||||||
bool get canUpdate => status == AppInstallStatus.updatable;
|
bool get canUpdate => localApp.value?.status == AppInstallStatus.updatable;
|
||||||
bool get isUpdated => status == AppInstallStatus.updated;
|
bool get isUpdated => localApp.value?.status == AppInstallStatus.updated;
|
||||||
|
|
||||||
Future<void> install() async {
|
Future<void> install() async {
|
||||||
if (!canInstall && !canUpdate) {
|
if (!canInstall && !canUpdate) {
|
||||||
@@ -107,7 +99,8 @@ class App extends BaseApp with DataModelMixin<App> {
|
|||||||
final result = await InstallPlugin.install(file.path);
|
final result = await InstallPlugin.install(file.path);
|
||||||
if (result['isSuccess']) {
|
if (result['isSuccess']) {
|
||||||
await file.delete();
|
await file.delete();
|
||||||
await adapter.updateInstallStatus(app: this);
|
await adapter.ref.localApps.localAppAdapter
|
||||||
|
.updateInstallStatus(appId: id?.toString());
|
||||||
notifier.state = IdleInstallProgress();
|
notifier.state = IdleInstallProgress();
|
||||||
} else {
|
} else {
|
||||||
const msg = 'Android rejected installation';
|
const msg = 'Android rejected installation';
|
||||||
@@ -171,10 +164,22 @@ class App extends BaseApp with DataModelMixin<App> {
|
|||||||
mixin AppAdapter on Adapter<App> {
|
mixin AppAdapter on Adapter<App> {
|
||||||
Future<List<App>> loadAppModels(Map<String, dynamic> params) async {
|
Future<List<App>> loadAppModels(Map<String, dynamic> params) async {
|
||||||
final includes = params.remove('includes') ?? false;
|
final includes = params.remove('includes') ?? false;
|
||||||
final apps = await super.findAll(params: params);
|
|
||||||
final releases = await ref.releases.findAll(params: {
|
final apps = await super.findAll(
|
||||||
'#a': apps.map((app) => app.getReplaceableEventLink().formatted)
|
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 =
|
final metadataIds =
|
||||||
releases.map((r) => r.tagMap['e']).nonNulls.expand((_) => _);
|
releases.map((r) => r.tagMap['e']).nonNulls.expand((_) => _);
|
||||||
|
|
||||||
@@ -209,17 +214,19 @@ mixin AppAdapter on Adapter<App> {
|
|||||||
if (appIds.isNotEmpty) {
|
if (appIds.isNotEmpty) {
|
||||||
params['#d'] = appIds;
|
params['#d'] = appIds;
|
||||||
params.remove('installed');
|
params.remove('installed');
|
||||||
await loadAppModels(params);
|
final apps = await loadAppModels(params);
|
||||||
// Once apps are loaded, check for installed status
|
// Once apps are loaded, check for installed status
|
||||||
return await updateInstallStatus();
|
await ref.localApps.localAppAdapter.updateInstallStatus();
|
||||||
|
return apps;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return await loadAppModels(params);
|
return await loadAppModels(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<App> findWhereIdInLocal(Iterable<String> appIds) {
|
List<App> findWhereIdInLocal(Iterable<String> appIds) {
|
||||||
|
const len = 5 + 64 + 3; // kind + pubkey + separators length
|
||||||
final result = db.select(
|
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());
|
appIds.toList());
|
||||||
return deserializeFromResult(result);
|
return deserializeFromResult(result);
|
||||||
}
|
}
|
||||||
@@ -242,6 +249,8 @@ mixin AppAdapter on Adapter<App> {
|
|||||||
deleteLocalById(id);
|
deleteLocalById(id);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
await ref.localApps.localAppAdapter
|
||||||
|
.updateInstallStatus(appId: id.toString());
|
||||||
return apps.first;
|
return apps.first;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,131 +262,26 @@ mixin AppAdapter on Adapter<App> {
|
|||||||
return {
|
return {
|
||||||
for (final i in infos!)
|
for (final i in infos!)
|
||||||
if (i.packageName != null &&
|
if (i.packageName != null &&
|
||||||
![
|
!kExcludedAppIdNamespaces.any((e) => i.packageName!.startsWith(e)))
|
||||||
'android',
|
|
||||||
'com.android',
|
|
||||||
'com.google',
|
|
||||||
'org.chromium.webview_shell',
|
|
||||||
'app.grapheneos',
|
|
||||||
'app.vanadium'
|
|
||||||
].any((e) => i.packageName!.startsWith(e)))
|
|
||||||
i.packageName!,
|
i.packageName!,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<App>> updateInstallStatus({App? app}) async {
|
|
||||||
if (!Platform.isAndroid) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
final updatedApps = <App>[];
|
|
||||||
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
|
@override
|
||||||
DeserializedData<App> deserialize(Object? data, {String? key}) {
|
DeserializedData<App> deserialize(Object? data, {String? key}) {
|
||||||
final list = data is Iterable ? data : [data as Map];
|
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 Map<String, dynamic> map in list) {
|
||||||
for (final e in list) {
|
final tagMap = tagsToMap(map['tags']);
|
||||||
final map = e as Map<String, dynamic>;
|
final appId = tagMap['d']!.first;
|
||||||
final tags = map['tags'] as Iterable;
|
map['id'] =
|
||||||
|
(map['kind'] as int, map['pubkey'].toString(), appId).formatted;
|
||||||
map['signer'] = map['pubkey'];
|
map['signer'] = map['pubkey'];
|
||||||
final zapTags = tags.where((t) => t[0] == 'zap');
|
map['developer'] = tagMap['zap']?.firstOrNull?[1];
|
||||||
if (zapTags.length == 1) {
|
map['localApp'] = appId;
|
||||||
map['developer'] = (zapTags.first as List)[1];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final deserialized = super.deserialize(data);
|
return 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,17 +312,6 @@ Future<bool> _isHashMismatch(String path, String hash) async {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// install support
|
|
||||||
|
|
||||||
enum AppInstallStatus {
|
|
||||||
updated,
|
|
||||||
updatable,
|
|
||||||
installable,
|
|
||||||
downgrade,
|
|
||||||
differentArchitecture,
|
|
||||||
loading
|
|
||||||
}
|
|
||||||
|
|
||||||
// class
|
// class
|
||||||
|
|
||||||
sealed class AppInstallProgress {}
|
sealed class AppInstallProgress {}
|
||||||
@@ -444,3 +337,18 @@ final installationProgressProvider =
|
|||||||
(_, arg) => IdleInstallProgress());
|
(_, arg) => IdleInstallProgress());
|
||||||
|
|
||||||
final appsToUpdateProvider = StateProvider((_) => 0);
|
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',
|
||||||
|
];
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ mixin _$AppAdapter on Adapter<App> {
|
|||||||
type: 'users',
|
type: 'users',
|
||||||
kind: 'BelongsTo',
|
kind: 'BelongsTo',
|
||||||
instance: (_) => (_ as App).developer,
|
instance: (_) => (_ as App).developer,
|
||||||
|
),
|
||||||
|
'localApp': RelationshipMeta<LocalApp>(
|
||||||
|
name: 'localApp',
|
||||||
|
type: 'localApps',
|
||||||
|
kind: 'BelongsTo',
|
||||||
|
instance: (_) => (_ as App).localApp,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -81,4 +87,11 @@ extension AppRelationshipGraphNodeX on RelationshipGraphNode<App> {
|
|||||||
return meta.clone(
|
return meta.clone(
|
||||||
parent: this is RelationshipMeta ? this as RelationshipMeta : null);
|
parent: this is RelationshipMeta ? this as RelationshipMeta : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RelationshipGraphNode<LocalApp> get localApp {
|
||||||
|
final meta = _$AppAdapter._kAppRelationshipMetas['localApp']
|
||||||
|
as RelationshipMeta<LocalApp>;
|
||||||
|
return meta.clone(
|
||||||
|
parent: this is RelationshipMeta ? this as RelationshipMeta : null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter_data/flutter_data.dart';
|
import 'package:flutter_data/flutter_data.dart';
|
||||||
import 'package:purplebase/purplebase.dart';
|
import 'package:purplebase/purplebase.dart';
|
||||||
|
import 'package:zapstore/models/app.dart';
|
||||||
import 'package:zapstore/models/nostr_adapter.dart';
|
import 'package:zapstore/models/nostr_adapter.dart';
|
||||||
import 'package:zapstore/models/release.dart';
|
import 'package:zapstore/models/release.dart';
|
||||||
import 'package:zapstore/models/user.dart';
|
import 'package:zapstore/models/user.dart';
|
||||||
@@ -42,11 +43,21 @@ mixin FileMetadataAdapter on Adapter<FileMetadata> {
|
|||||||
for (final e in list) {
|
for (final e in list) {
|
||||||
final map = e as Map<String, dynamic>;
|
final map = e as Map<String, dynamic>;
|
||||||
map['author'] = map['pubkey'];
|
map['author'] = map['pubkey'];
|
||||||
// Ensure tags are strings
|
|
||||||
for (var t in map['tags']) {
|
|
||||||
t[1] = t[1].toString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return super.deserialize(data);
|
return super.deserialize(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<FileMetadata>> zzz(List<String> 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<LocalApp> {
|
||||||
|
@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<LocalApp> {
|
||||||
|
Future<void> 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<String, dynamic> map, {String? key}) {
|
||||||
|
// TODO: implement deserializeLocal
|
||||||
|
return super.deserializeLocal(map, key: key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> serializeLocal(LocalApp model,
|
||||||
|
{bool withRelationships = true}) {
|
||||||
|
final z = super.serializeLocal(model, withRelationships: withRelationships);
|
||||||
|
return z;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AppInstallStatus {
|
||||||
|
updated,
|
||||||
|
updatable,
|
||||||
|
installable,
|
||||||
|
downgrade,
|
||||||
|
}
|
||||||
@@ -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<LocalApp> {
|
||||||
|
static final Map<String, RelationshipMeta> _kLocalAppRelationshipMetas = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, RelationshipMeta> get relationshipMetas =>
|
||||||
|
_kLocalAppRelationshipMetas;
|
||||||
|
|
||||||
|
@override
|
||||||
|
LocalApp deserializeLocal(map, {String? key}) {
|
||||||
|
map = transformDeserialize(map);
|
||||||
|
return internalWrapStopInit(() => _$LocalAppFromJson(map), key: key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> serializeLocal(model, {bool withRelationships = true}) {
|
||||||
|
final map = _$LocalAppToJson(model);
|
||||||
|
return transformSerialize(map, withRelationships: withRelationships);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final _localAppsFinders = <String, dynamic>{};
|
||||||
|
|
||||||
|
class $LocalAppAdapter = Adapter<LocalApp>
|
||||||
|
with _$LocalAppAdapter, LocalAppAdapter;
|
||||||
|
|
||||||
|
final localAppsAdapterProvider = Provider<Adapter<LocalApp>>(
|
||||||
|
(ref) => $LocalAppAdapter(ref, InternalHolder(_localAppsFinders)));
|
||||||
|
|
||||||
|
extension LocalAppAdapterX on Adapter<LocalApp> {
|
||||||
|
LocalAppAdapter get localAppAdapter => this as LocalAppAdapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
extension LocalAppRelationshipGraphNodeX on RelationshipGraphNode<LocalApp> {}
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
LocalApp _$LocalAppFromJson(Map<String, dynamic> json) => LocalApp(
|
||||||
|
id: json['id'],
|
||||||
|
installedVersion: json['installedVersion'] as String?,
|
||||||
|
installedVersionCode: (json['installedVersionCode'] as num?)?.toInt(),
|
||||||
|
status: $enumDecodeNullable(_$AppInstallStatusEnumMap, json['status']),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$LocalAppToJson(LocalApp instance) => <String, dynamic>{
|
||||||
|
'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',
|
||||||
|
};
|
||||||
@@ -2,7 +2,7 @@ import 'package:flutter_data/flutter_data.dart';
|
|||||||
import 'package:purplebase/purplebase.dart';
|
import 'package:purplebase/purplebase.dart';
|
||||||
|
|
||||||
// NOTE: Very important to use const in relay args to preserve equality in Riverpod families
|
// 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'];
|
const kSocialRelays = ['wss://relay.primal.net', 'wss://relay.nostr.band'];
|
||||||
|
|
||||||
mixin NostrAdapter<T extends DataModelMixin<T>> on Adapter<T> {
|
mixin NostrAdapter<T extends DataModelMixin<T>> on Adapter<T> {
|
||||||
@@ -24,10 +24,15 @@ mixin NostrAdapter<T extends DataModelMixin<T>> on Adapter<T> {
|
|||||||
for (final e in list) {
|
for (final e in list) {
|
||||||
final map = e as Map<String, dynamic>;
|
final map = e as Map<String, dynamic>;
|
||||||
|
|
||||||
// ID should be the identifier in PREs
|
// ID should be the replaceable link/reference so as to make it replaceable in local db too
|
||||||
final dTags = (map['tags'] as Iterable).where((t) => t[0] == 'd');
|
final tagMap = tagsToMap(map['tags']);
|
||||||
if (dTags.length == 1) {
|
final isReplaceable = tagMap.containsKey('d');
|
||||||
map['id'] = (dTags.first as List)[1];
|
if (isReplaceable) {
|
||||||
|
map['id'] = (
|
||||||
|
map['kind'] as int,
|
||||||
|
map['pubkey'].toString(),
|
||||||
|
tagMap['d']?.firstOrNull
|
||||||
|
).formatted;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect models for current kind and included for others
|
// Collect models for current kind and included for others
|
||||||
@@ -58,9 +63,11 @@ mixin NostrAdapter<T extends DataModelMixin<T>> on Adapter<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final additionalKinds = params?.remove('kinds');
|
final additionalKinds = params?.remove('kinds');
|
||||||
|
final limit = params?.remove('limit');
|
||||||
final req = RelayRequest(
|
final req = RelayRequest(
|
||||||
kinds: {kind, ...?additionalKinds},
|
kinds: {kind, ...?additionalKinds},
|
||||||
tags: params ?? {},
|
tags: params ?? {},
|
||||||
|
limit: limit,
|
||||||
);
|
);
|
||||||
|
|
||||||
final result = await relay.queryRaw(req);
|
final result = await relay.queryRaw(req);
|
||||||
|
|||||||
@@ -36,15 +36,11 @@ mixin ReleaseAdapter on Adapter<Release> {
|
|||||||
@override
|
@override
|
||||||
DeserializedData<Release> deserialize(Object? data, {String? key}) {
|
DeserializedData<Release> deserialize(Object? data, {String? key}) {
|
||||||
final list = data is Iterable ? data : [data as Map];
|
final list = data is Iterable ? data : [data as Map];
|
||||||
for (final e in list) {
|
|
||||||
final map = e as Map<String, dynamic>;
|
for (final Map<String, dynamic> map in list) {
|
||||||
final eTags = (map['tags'] as Iterable)
|
final tagMap = tagsToMap(map['tags']);
|
||||||
.where((e) => e.first == 'e')
|
map['app'] = tagMap['a']?.firstOrNull;
|
||||||
.map((e) => e[1].toString());
|
map['artifacts'] = tagMap['e']!.toList();
|
||||||
map['artifacts'] = eTags.toList();
|
|
||||||
final appIdentifier =
|
|
||||||
map['tags'].firstWhere((t) => t.first == 'd')[1].split('@').first;
|
|
||||||
map['app'] = appIdentifier;
|
|
||||||
}
|
}
|
||||||
return super.deserialize(data);
|
return super.deserialize(data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class User extends BaseUser with DataModelMixin<User> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mixin UserAdapter on NostrAdapter<User> {
|
mixin UserAdapter on NostrAdapter<User> {
|
||||||
|
// TODO: All this still necessary with purplebase?
|
||||||
@override
|
@override
|
||||||
DeserializedData<User> deserialize(Object? data, {String? key}) {
|
DeserializedData<User> deserialize(Object? data, {String? key}) {
|
||||||
final Iterable<Map<String, dynamic>> list =
|
final Iterable<Map<String, dynamic>> list =
|
||||||
@@ -54,7 +55,6 @@ mixin UserAdapter on NostrAdapter<User> {
|
|||||||
.toList()
|
.toList()
|
||||||
.groupSetsBy((e) => e['pubkey'] as String);
|
.groupSetsBy((e) => e['pubkey'] as String);
|
||||||
|
|
||||||
// TODO: This still necessary with purplebase?
|
|
||||||
// collect contacts and then assign them to user
|
// collect contacts and then assign them to user
|
||||||
final included = <String, List<DataModelMixin>>{};
|
final included = <String, List<DataModelMixin>>{};
|
||||||
for (final _ in k3s.entries) {
|
for (final _ in k3s.entries) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:purplebase/purplebase.dart';
|
|||||||
import 'package:zapstore/main.dart';
|
import 'package:zapstore/main.dart';
|
||||||
import 'package:zapstore/main.data.dart';
|
import 'package:zapstore/main.data.dart';
|
||||||
import 'package:zapstore/models/app.dart';
|
import 'package:zapstore/models/app.dart';
|
||||||
|
import 'package:zapstore/models/local_app.dart';
|
||||||
import 'package:zapstore/models/nostr_adapter.dart';
|
import 'package:zapstore/models/nostr_adapter.dart';
|
||||||
import 'package:zapstore/navigation/desktop_scaffold.dart';
|
import 'package:zapstore/navigation/desktop_scaffold.dart';
|
||||||
import 'package:zapstore/navigation/mobile_scaffold.dart';
|
import 'package:zapstore/navigation/mobile_scaffold.dart';
|
||||||
@@ -145,11 +146,11 @@ final dataLibrariesInitializer = FutureProvider<void>((ref) async {
|
|||||||
await Future.wait([relay.initialize(), socialRelays.initialize()]);
|
await Future.wait([relay.initialize(), socialRelays.initialize()]);
|
||||||
|
|
||||||
// Trigger app install status calculations
|
// Trigger app install status calculations
|
||||||
await ref.apps.appAdapter.updateInstallStatus();
|
await ref.localApps.localAppAdapter.updateInstallStatus();
|
||||||
_lifecycleListener = AppLifecycleListener(
|
_lifecycleListener = AppLifecycleListener(
|
||||||
onStateChange: (state) async {
|
onStateChange: (state) async {
|
||||||
if (state == AppLifecycleState.resumed) {
|
if (state == AppLifecycleState.resumed) {
|
||||||
await ref.apps.appAdapter.updateInstallStatus();
|
await ref.localApps.localAppAdapter.updateInstallStatus();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ class SearchResultNotifier extends AsyncNotifier<List<App>?> {
|
|||||||
Future<List<App>?> build() async {
|
Future<List<App>?> build() async {
|
||||||
final query = ref.watch(searchQueryProvider);
|
final query = ref.watch(searchQueryProvider);
|
||||||
if (query != null) {
|
if (query != null) {
|
||||||
return await ref.apps.findAll(params: {'search': query});
|
return await ref.apps.findAll(params: {'search': query, 'limit': 16});
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:percent_indicator/linear_percent_indicator.dart';
|
import 'package:percent_indicator/linear_percent_indicator.dart';
|
||||||
import 'package:zapstore/main.data.dart';
|
import 'package:zapstore/main.data.dart';
|
||||||
import 'package:zapstore/models/app.dart';
|
import 'package:zapstore/models/app.dart';
|
||||||
|
import 'package:zapstore/models/local_app.dart';
|
||||||
import 'package:zapstore/models/settings.dart';
|
import 'package:zapstore/models/settings.dart';
|
||||||
import 'package:zapstore/utils/extensions.dart';
|
import 'package:zapstore/utils/extensions.dart';
|
||||||
import 'package:zapstore/utils/system_info.dart';
|
import 'package:zapstore/utils/system_info.dart';
|
||||||
@@ -27,10 +28,10 @@ class InstallButton extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final progress = ref.watch(installationProgressProvider(app.identifier!));
|
final progress = ref.watch(installationProgressProvider(app.identifier!));
|
||||||
|
final status = app.localApp.value?.status;
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: switch (app.status) {
|
onTap: switch (status) {
|
||||||
AppInstallStatus.differentArchitecture => null,
|
|
||||||
AppInstallStatus.downgrade => null,
|
AppInstallStatus.downgrade => null,
|
||||||
AppInstallStatus.updated => () {
|
AppInstallStatus.updated => () {
|
||||||
packageManager.openApp(app.id!.toString());
|
packageManager.openApp(app.id!.toString());
|
||||||
@@ -68,7 +69,7 @@ class InstallButton extends ConsumerWidget {
|
|||||||
percent: switch (progress) {
|
percent: switch (progress) {
|
||||||
VerifyingHashProgress() => 1,
|
VerifyingHashProgress() => 1,
|
||||||
DownloadingInstallProgress(:final progress) => progress,
|
DownloadingInstallProgress(:final progress) => progress,
|
||||||
_ => switch (app.status) {
|
_ => switch (status) {
|
||||||
AppInstallStatus.updated => 1,
|
AppInstallStatus.updated => 1,
|
||||||
_ => 0,
|
_ => 0,
|
||||||
},
|
},
|
||||||
@@ -81,15 +82,9 @@ class InstallButton extends ConsumerWidget {
|
|||||||
barRadius: Radius.circular(20),
|
barRadius: Radius.circular(20),
|
||||||
padding: EdgeInsets.all(0),
|
padding: EdgeInsets.all(0),
|
||||||
animateFromLastPercent: true,
|
animateFromLastPercent: true,
|
||||||
center: switch (app.status) {
|
center: switch (status) {
|
||||||
AppInstallStatus.loading =>
|
|
||||||
SizedBox(width: 14, height: 14, child: CircularProgressIndicator()),
|
|
||||||
AppInstallStatus.differentArchitecture => Text(
|
|
||||||
'Sorry, release does not support your device',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
AppInstallStatus.downgrade => Text(
|
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,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
AppInstallStatus.updated => Text('Open'),
|
AppInstallStatus.updated => Text('Open'),
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class LatestReleasesContainer extends HookConsumerWidget {
|
|||||||
class LatestReleasesAppNotifier extends AutoDisposeAsyncNotifier<List<App>> {
|
class LatestReleasesAppNotifier extends AutoDisposeAsyncNotifier<List<App>> {
|
||||||
@override
|
@override
|
||||||
Future<List<App>> build() async {
|
Future<List<App>> build() async {
|
||||||
final timer = Timer.periodic(Duration(hours: 4), (_) => fetch());
|
final timer = Timer.periodic(Duration(minutes: 10), (_) => fetch());
|
||||||
ref.onDispose(timer.cancel);
|
ref.onDispose(timer.cancel);
|
||||||
fetch();
|
fetch();
|
||||||
// .catchError((e) {
|
// .catchError((e) {
|
||||||
@@ -49,20 +49,15 @@ class LatestReleasesAppNotifier extends AutoDisposeAsyncNotifier<List<App>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<App> localFetch() {
|
List<App> localFetch() {
|
||||||
return ref.releases
|
return ref.apps
|
||||||
.findAllLocal()
|
.findAllLocal()
|
||||||
.sorted((a, b) => b.createdAt!.compareTo(a.createdAt!))
|
.sorted((a, b) => b.createdAt!.compareTo(a.createdAt!))
|
||||||
.take(10)
|
.take(10)
|
||||||
.map((r) => r.app.value)
|
|
||||||
.nonNulls
|
|
||||||
.toSet()
|
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetch() async {
|
Future<void> fetch() async {
|
||||||
final releases = await ref.releases.findAll(params: {'limit': 10});
|
await ref.apps.findAll(params: {'limit': 10});
|
||||||
final appIds = releases.map((r) => r.app.id!.toString()).toSet();
|
|
||||||
await ref.apps.findAll(params: {'#d': appIds});
|
|
||||||
update((_) => localFetch());
|
update((_) => localFetch());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ class SignerAndDeveloperRow extends StatelessWidget {
|
|||||||
launchUrl(url);
|
launchUrl(url);
|
||||||
},
|
},
|
||||||
child: AuthorContainer(
|
child: AuthorContainer(
|
||||||
user: app.developer.value!, text: 'Built by', oneLine: false),
|
user: app.developer.value!,
|
||||||
|
text: 'Developed by',
|
||||||
|
oneLine: false),
|
||||||
),
|
),
|
||||||
if (app.signer.isPresent)
|
if (app.signer.isPresent)
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
|
|||||||
@@ -47,7 +47,9 @@ class VersionedAppHeader extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
if (isUpdate)
|
if (isUpdate)
|
||||||
PillWidget(
|
PillWidget(
|
||||||
text: app.installedVersion!, color: Colors.grey[800]!),
|
text: app.localApp.value!.installedVersion!,
|
||||||
|
color: Colors.grey[800]!,
|
||||||
|
),
|
||||||
if (isUpdate) Icon(Icons.arrow_right),
|
if (isUpdate) Icon(Icons.arrow_right),
|
||||||
if (app.latestMetadata != null)
|
if (app.latestMetadata != null)
|
||||||
PillWidget(
|
PillWidget(
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@ dependencies:
|
|||||||
cupertino_icons: ^1.0.2
|
cupertino_icons: ^1.0.2
|
||||||
flutter_data: ^2.0.0-rc3
|
flutter_data: ^2.0.0-rc3
|
||||||
hooks_riverpod: ^2.4.9
|
hooks_riverpod: ^2.4.9
|
||||||
json_annotation: ^4.8.1
|
json_annotation: ^4.9.0
|
||||||
flutter_hooks: ^0.20.4
|
flutter_hooks: ^0.20.4
|
||||||
build_runner: ^2.4.8
|
build_runner: ^2.4.8
|
||||||
collection: ^1.18.0
|
collection: ^1.18.0
|
||||||
|
|||||||
Reference in New Issue
Block a user