mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 20:48:24 +00:00
Tablet support to fix #28, add custom curation set for recommendations, wot default fixes #11, add clear cache fixes #45, other adjustments
This commit is contained in:
Vendored
-3
@@ -1,7 +1,4 @@
|
|||||||
{
|
{
|
||||||
// Use IntelliSense to learn about possible attributes.
|
|
||||||
// Hover to view descriptions of existing attributes.
|
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -65,8 +65,6 @@ android {
|
|||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
// TODO: Add your own signing config for the release build.
|
|
||||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
|
||||||
signingConfig signingConfigs.debug
|
signingConfig signingConfigs.debug
|
||||||
signingConfig signingConfigs.release
|
signingConfig signingConfigs.release
|
||||||
}
|
}
|
||||||
|
|||||||
+87
-54
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_data/flutter_data.dart';
|
import 'package:flutter_data/flutter_data.dart';
|
||||||
|
import 'package:flutter_phoenix/flutter_phoenix.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
@@ -16,21 +17,22 @@ const kDbVersion = 1;
|
|||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(
|
runApp(
|
||||||
ProviderScope(
|
Phoenix(
|
||||||
overrides: [
|
child: ProviderScope(
|
||||||
localStorageProvider.overrideWithValue(
|
overrides: [
|
||||||
LocalStorage(
|
localStorageProvider.overrideWithValue(
|
||||||
baseDirFn: () async {
|
LocalStorage(
|
||||||
final path = (await getApplicationSupportDirectory()).path;
|
baseDirFn: () async {
|
||||||
print('initializing local storage at $path');
|
final path = (await getApplicationSupportDirectory()).path;
|
||||||
return path;
|
print('initializing local storage at $path');
|
||||||
},
|
return path;
|
||||||
// TODO RESTORE!
|
},
|
||||||
clear: LocalStorageClearStrategy.always,
|
clear: LocalStorageClearStrategy.whenError,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
child: const ZapstoreApp(),
|
child: const ZapstoreApp(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -107,9 +109,7 @@ final goRouter = GoRouter(
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
pageBuilder: (context, state) => NoTransitionPage(
|
pageBuilder: (context, state) => NoTransitionPage(
|
||||||
child: Center(
|
child: SettingsScreen(),
|
||||||
child: SettingsScreen(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -119,13 +119,30 @@ final goRouter = GoRouter(
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
AppLifecycleListener? _lifecycleListener;
|
||||||
|
|
||||||
final newInitializer = FutureProvider<void>((ref) async {
|
final newInitializer = FutureProvider<void>((ref) async {
|
||||||
await ref.read(initializeFlutterData(adapterProvidersMap).future);
|
await ref.read(initializeFlutterData(adapterProvidersMap).future);
|
||||||
ref
|
ref
|
||||||
.read(relayMessageNotifierProvider.notifier)
|
.read(relayMessageNotifierProvider.notifier)
|
||||||
.initialize(['wss://relay.zap.store', 'wss://relay.nostr.band']);
|
.initialize(['wss://relay.zap.store', 'wss://relay.nostr.band']);
|
||||||
|
_lifecycleListener = AppLifecycleListener(
|
||||||
|
onStateChange: (state) async {
|
||||||
|
if (state == AppLifecycleState.resumed) {
|
||||||
|
final adapter = ref.apps.appAdapter;
|
||||||
|
await adapter.getInstalledAppsMap();
|
||||||
|
adapter.triggerNotify();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ref.onDispose(() {
|
||||||
|
_lifecycleListener?.dispose();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Scaffolding
|
||||||
|
|
||||||
class ScaffoldWithNestedNavigation extends HookConsumerWidget {
|
class ScaffoldWithNestedNavigation extends HookConsumerWidget {
|
||||||
const ScaffoldWithNestedNavigation({
|
const ScaffoldWithNestedNavigation({
|
||||||
Key? key,
|
Key? key,
|
||||||
@@ -147,32 +164,41 @@ class ScaffoldWithNestedNavigation extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final initializer = ref.watch(newInitializer);
|
||||||
|
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
child: LayoutBuilder(
|
child: initializer.when(
|
||||||
builder: (context, constraints) {
|
data: (_) => LayoutBuilder(
|
||||||
if (constraints.maxWidth < 550) {
|
builder: (context, constraints) {
|
||||||
return ScaffoldWithNavigationBar(
|
if (constraints.maxWidth < 550) {
|
||||||
body: navigationShell,
|
return MobileScaffold(
|
||||||
selectedIndex: navigationShell.currentIndex,
|
body: navigationShell,
|
||||||
onDestinationSelected: _goBranch,
|
selectedIndex: navigationShell.currentIndex,
|
||||||
);
|
onDestinationSelected: _goBranch,
|
||||||
} else {
|
);
|
||||||
return ScaffoldWithNavigationRail(
|
} else {
|
||||||
body: navigationShell,
|
return DesktopScaffold(
|
||||||
selectedIndex: navigationShell.currentIndex,
|
body: navigationShell,
|
||||||
onDestinationSelected: _goBranch,
|
selectedIndex: navigationShell.currentIndex,
|
||||||
);
|
onDestinationSelected: _goBranch,
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
error: (e, stack) {
|
||||||
|
print(stack);
|
||||||
|
return Text('error $e');
|
||||||
},
|
},
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
|
final scaffoldKey = GlobalKey<ScaffoldState>();
|
||||||
|
|
||||||
class ScaffoldWithNavigationBar extends HookConsumerWidget {
|
class MobileScaffold extends StatelessWidget {
|
||||||
const ScaffoldWithNavigationBar({
|
const MobileScaffold({
|
||||||
super.key,
|
super.key,
|
||||||
required this.body,
|
required this.body,
|
||||||
required this.selectedIndex,
|
required this.selectedIndex,
|
||||||
@@ -183,20 +209,12 @@ class ScaffoldWithNavigationBar extends HookConsumerWidget {
|
|||||||
final ValueChanged<int> onDestinationSelected;
|
final ValueChanged<int> onDestinationSelected;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context) {
|
||||||
final initializer = ref.watch(newInitializer);
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
key: scaffoldKey,
|
key: scaffoldKey,
|
||||||
body: Padding(
|
body: Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 20, 16, 0),
|
padding: const EdgeInsets.fromLTRB(16, 20, 16, 0),
|
||||||
child: initializer.when(
|
child: body,
|
||||||
data: (_) => body,
|
|
||||||
error: (e, stack) {
|
|
||||||
print(stack);
|
|
||||||
return Text('error $e');
|
|
||||||
},
|
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
bottomNavigationBar: NavigationBar(
|
bottomNavigationBar: NavigationBar(
|
||||||
height: 60,
|
height: 60,
|
||||||
@@ -224,14 +242,17 @@ class ScaffoldWithNavigationBar extends HookConsumerWidget {
|
|||||||
onDestinationSelected: onDestinationSelected,
|
onDestinationSelected: onDestinationSelected,
|
||||||
),
|
),
|
||||||
drawer: Drawer(
|
drawer: Drawer(
|
||||||
child: AppDrawer(),
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 24, 16, 0),
|
||||||
|
child: LoginContainer(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ScaffoldWithNavigationRail extends StatelessWidget {
|
class DesktopScaffold extends StatelessWidget {
|
||||||
const ScaffoldWithNavigationRail({
|
const DesktopScaffold({
|
||||||
super.key,
|
super.key,
|
||||||
required this.body,
|
required this.body,
|
||||||
required this.selectedIndex,
|
required this.selectedIndex,
|
||||||
@@ -247,25 +268,37 @@ class ScaffoldWithNavigationRail extends StatelessWidget {
|
|||||||
body: Row(
|
body: Row(
|
||||||
children: [
|
children: [
|
||||||
NavigationRail(
|
NavigationRail(
|
||||||
|
minWidth: 120,
|
||||||
selectedIndex: selectedIndex,
|
selectedIndex: selectedIndex,
|
||||||
onDestinationSelected: onDestinationSelected,
|
onDestinationSelected: onDestinationSelected,
|
||||||
labelType: NavigationRailLabelType.all,
|
labelType: NavigationRailLabelType.all,
|
||||||
destinations: [
|
destinations: [
|
||||||
const NavigationRailDestination(
|
const NavigationRailDestination(
|
||||||
label: Text('Search'),
|
label: Text('Home'),
|
||||||
icon: Icon(Icons.search_outlined),
|
icon: Icon(Icons.home_outlined),
|
||||||
|
selectedIcon: Icon(Icons.home_filled),
|
||||||
),
|
),
|
||||||
const NavigationRailDestination(
|
const NavigationRailDestination(
|
||||||
label: Text('Updates'),
|
label: Text('Updates'),
|
||||||
icon: Icon(Icons.download_for_offline_outlined),
|
icon: Icon(Icons.download_for_offline_outlined),
|
||||||
|
selectedIcon: Icon(Icons.download_for_offline),
|
||||||
),
|
),
|
||||||
const NavigationRailDestination(
|
const NavigationRailDestination(
|
||||||
label: Text('Profile'),
|
label: Text('Settings'),
|
||||||
icon: Icon(Icons.person_outline),
|
icon: Icon(Icons.settings_outlined),
|
||||||
|
selectedIcon: Icon(Icons.settings),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body,
|
Flexible(
|
||||||
|
child: Container(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: 768,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 20, 16, 0),
|
||||||
|
child: body,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
+5
-1
@@ -6,6 +6,7 @@
|
|||||||
import 'package:flutter_data/flutter_data.dart';
|
import 'package:flutter_data/flutter_data.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
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/app.dart';
|
||||||
import 'package:zapstore/models/file_metadata.dart';
|
import 'package:zapstore/models/file_metadata.dart';
|
||||||
import 'package:zapstore/models/release.dart';
|
import 'package:zapstore/models/release.dart';
|
||||||
@@ -13,7 +14,8 @@ import 'package:zapstore/models/settings.dart';
|
|||||||
import 'package:zapstore/models/user.dart';
|
import 'package:zapstore/models/user.dart';
|
||||||
|
|
||||||
final adapterProvidersMap = <String, Provider<Adapter<DataModelMixin>>>{
|
final adapterProvidersMap = <String, Provider<Adapter<DataModelMixin>>>{
|
||||||
'apps': appsAdapterProvider,
|
'appCurationSets': appCurationSetsAdapterProvider,
|
||||||
|
'apps': appsAdapterProvider,
|
||||||
'fileMetadata': fileMetadataAdapterProvider,
|
'fileMetadata': fileMetadataAdapterProvider,
|
||||||
'releases': releasesAdapterProvider,
|
'releases': releasesAdapterProvider,
|
||||||
'settings': settingsAdapterProvider,
|
'settings': settingsAdapterProvider,
|
||||||
@@ -21,6 +23,7 @@ final adapterProvidersMap = <String, Provider<Adapter<DataModelMixin>>>{
|
|||||||
};
|
};
|
||||||
|
|
||||||
extension AdapterWidgetRefX on WidgetRef {
|
extension AdapterWidgetRefX on WidgetRef {
|
||||||
|
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<Release> get releases => watch(releasesAdapterProvider)..internalWatch = watch;
|
Adapter<Release> get releases => watch(releasesAdapterProvider)..internalWatch = watch;
|
||||||
@@ -30,6 +33,7 @@ extension AdapterWidgetRefX on WidgetRef {
|
|||||||
|
|
||||||
extension AdapterRefX on Ref {
|
extension AdapterRefX on Ref {
|
||||||
|
|
||||||
|
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<Release> get releases => watch(releasesAdapterProvider)..internalWatch = watch as Watcher;
|
Adapter<Release> get releases => watch(releasesAdapterProvider)..internalWatch = watch as Watcher;
|
||||||
|
|||||||
+139
-132
@@ -6,7 +6,6 @@ import 'dart:typed_data';
|
|||||||
import 'package:android_package_manager/android_package_manager.dart';
|
import 'package:android_package_manager/android_package_manager.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_data/flutter_data.dart';
|
import 'package:flutter_data/flutter_data.dart';
|
||||||
import 'package:install_plugin/install_plugin.dart';
|
import 'package:install_plugin/install_plugin.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
@@ -26,7 +25,7 @@ part 'app.g.dart';
|
|||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
@DataAdapter([NostrAdapter, AppAdapter])
|
@DataAdapter([NostrAdapter, AppAdapter])
|
||||||
class App extends Event<App> with BaseApp {
|
class App extends BaseApp with DataModelMixin<App> {
|
||||||
late final HasMany<Release> releases;
|
late final HasMany<Release> releases;
|
||||||
late final BelongsTo<User> signer;
|
late final BelongsTo<User> signer;
|
||||||
late final BelongsTo<User> developer;
|
late final BelongsTo<User> developer;
|
||||||
@@ -45,137 +44,7 @@ class App extends Event<App> with BaseApp {
|
|||||||
a.architectures.contains('arm64-v8a'))
|
a.architectures.contains('arm64-v8a'))
|
||||||
.firstOrNull;
|
.firstOrNull;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
mixin AppAdapter on Adapter<App> {
|
|
||||||
ProviderSubscription? _sub;
|
|
||||||
AppLifecycleListener? _lifecycleListener;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onInitialized() async {
|
|
||||||
if (!ref.read(localStorageProvider).inIsolate) {
|
|
||||||
_sub = ref.listen(installedAppProvider, (_, __) {
|
|
||||||
triggerNotify();
|
|
||||||
});
|
|
||||||
|
|
||||||
_lifecycleListener = AppLifecycleListener(
|
|
||||||
onStateChange: (state) async {
|
|
||||||
if (state == AppLifecycleState.resumed) {
|
|
||||||
await getInstalledAppsMap();
|
|
||||||
triggerNotify();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
super.onInitialized();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_sub?.close();
|
|
||||||
_lifecycleListener?.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<App>> loadAppModels(Map<String, dynamic> params) async {
|
|
||||||
final apps = await super.findAll(params: params);
|
|
||||||
final releases =
|
|
||||||
await ref.releases.findAll(params: {'#a': apps.map((app) => app.aTag)});
|
|
||||||
final metadataIds = releases.map((r) => r.tagMap['e']!).expand((_) => _);
|
|
||||||
await ref.fileMetadata.findAll(params: {
|
|
||||||
'ids': metadataIds,
|
|
||||||
'#m': [kAndroidMimeType]
|
|
||||||
});
|
|
||||||
|
|
||||||
if (params.containsKey('includes')) {
|
|
||||||
final userIds = {
|
|
||||||
for (final app in apps) app.signer.id,
|
|
||||||
for (final app in apps) app.developer.id
|
|
||||||
}.nonNulls;
|
|
||||||
await ref.users.findAll(params: {'authors': userIds});
|
|
||||||
}
|
|
||||||
return apps;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<List<App>> findAll(
|
|
||||||
{bool? remote = true,
|
|
||||||
bool? background,
|
|
||||||
Map<String, dynamic>? params = const {},
|
|
||||||
Map<String, String>? headers,
|
|
||||||
bool? syncLocal,
|
|
||||||
OnSuccessAll<App>? onSuccess,
|
|
||||||
OnErrorAll<App>? onError,
|
|
||||||
DataRequestLabel? label}) async {
|
|
||||||
final map = await getInstalledAppsMap();
|
|
||||||
|
|
||||||
if (params!.containsKey('installed')) {
|
|
||||||
if (map.keys.isNotEmpty) {
|
|
||||||
params['#d'] = map.keys;
|
|
||||||
params.remove('installed');
|
|
||||||
print('filtering by installed ${params['#d']}');
|
|
||||||
|
|
||||||
// final apps = findAllLocal();
|
|
||||||
// if (apps.isNotEmpty) {
|
|
||||||
// loadAppModels(params);
|
|
||||||
// return apps;
|
|
||||||
// }
|
|
||||||
return await loadAppModels(params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await loadAppModels(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<App?> findOne(Object id,
|
|
||||||
{bool remote = true,
|
|
||||||
bool background = false,
|
|
||||||
Map<String, dynamic>? params = const {},
|
|
||||||
Map<String, String>? headers,
|
|
||||||
OnSuccessOne<App>? onSuccess,
|
|
||||||
OnErrorOne<App>? onError,
|
|
||||||
DataRequestLabel? label}) async {
|
|
||||||
final apps = await loadAppModels({
|
|
||||||
...params!,
|
|
||||||
'#d': [id]
|
|
||||||
});
|
|
||||||
// If ID not found in relay then clear from local storage
|
|
||||||
if (apps.isEmpty) {
|
|
||||||
deleteLocalById(id);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return apps.first;
|
|
||||||
}
|
|
||||||
|
|
||||||
static AndroidPackageManager? _packageManager;
|
|
||||||
|
|
||||||
Future<Map<String, String>> getInstalledAppsMap() async {
|
|
||||||
late List<PackageInfo>? infos;
|
|
||||||
if (Platform.isAndroid) {
|
|
||||||
_packageManager ??= AndroidPackageManager();
|
|
||||||
infos = await _packageManager!.getInstalledPackages();
|
|
||||||
} else {
|
|
||||||
infos = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
final installedPackageInfos = infos!.where((i) => ![
|
|
||||||
'android',
|
|
||||||
'com.android',
|
|
||||||
'com.google',
|
|
||||||
'org.chromium.webview_shell',
|
|
||||||
'app.grapheneos',
|
|
||||||
'app.vanadium'
|
|
||||||
].any((e) => i.packageName!.startsWith(e)));
|
|
||||||
|
|
||||||
return ref.read(installedAppProvider.notifier).state = {
|
|
||||||
for (final info in installedPackageInfos)
|
|
||||||
info.packageName!: info.versionName!
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension AppX on App {
|
|
||||||
bool get canInstall => status == AppInstallStatus.installable;
|
bool get canInstall => status == AppInstallStatus.installable;
|
||||||
bool get canUpdate => status == AppInstallStatus.updatable;
|
bool get canUpdate => status == AppInstallStatus.updatable;
|
||||||
bool get isUpdated => status == AppInstallStatus.updated;
|
bool get isUpdated => status == AppInstallStatus.updated;
|
||||||
@@ -279,6 +148,144 @@ extension AppX on App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mixin AppAdapter on Adapter<App> {
|
||||||
|
ProviderSubscription? _sub;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onInitialized() async {
|
||||||
|
if (!inIsolate) {
|
||||||
|
_sub = ref.listen(installedAppProvider, (_, __) {
|
||||||
|
triggerNotify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
super.onInitialized();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_sub?.close();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<App>> loadAppModels(Map<String, dynamic> params) async {
|
||||||
|
final apps = await super.findAll(params: params);
|
||||||
|
final releases =
|
||||||
|
await ref.releases.findAll(params: {'#a': apps.map((app) => app.aTag)});
|
||||||
|
final metadataIds = releases.map((r) => r.tagMap['e']!).expand((_) => _);
|
||||||
|
await ref.fileMetadata.findAll(params: {
|
||||||
|
'ids': metadataIds,
|
||||||
|
'#m': [kAndroidMimeType]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (params.containsKey('includes')) {
|
||||||
|
final userIds = {
|
||||||
|
for (final app in apps) app.signer.id,
|
||||||
|
for (final app in apps) app.developer.id
|
||||||
|
}.nonNulls;
|
||||||
|
await ref.users.findAll(params: {'authors': userIds});
|
||||||
|
}
|
||||||
|
return apps;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<App>> findAll(
|
||||||
|
{bool? remote = true,
|
||||||
|
bool? background,
|
||||||
|
Map<String, dynamic>? params = const {},
|
||||||
|
Map<String, String>? headers,
|
||||||
|
bool? syncLocal,
|
||||||
|
OnSuccessAll<App>? onSuccess,
|
||||||
|
OnErrorAll<App>? onError,
|
||||||
|
DataRequestLabel? label}) async {
|
||||||
|
final map = await getInstalledAppsMap(defer: true);
|
||||||
|
|
||||||
|
if (params!.containsKey('installed')) {
|
||||||
|
if (map.keys.isNotEmpty) {
|
||||||
|
params['#d'] = map.keys;
|
||||||
|
params.remove('installed');
|
||||||
|
print('filtering by installed ${params['#d']}');
|
||||||
|
|
||||||
|
// final apps = findAllLocal();
|
||||||
|
// if (apps.isNotEmpty) {
|
||||||
|
// loadAppModels(params);
|
||||||
|
// return apps;
|
||||||
|
// }
|
||||||
|
return await loadAppModels(params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await loadAppModels(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<App?> findOne(Object id,
|
||||||
|
{bool remote = true,
|
||||||
|
bool background = false,
|
||||||
|
Map<String, dynamic>? params = const {},
|
||||||
|
Map<String, String>? headers,
|
||||||
|
OnSuccessOne<App>? onSuccess,
|
||||||
|
OnErrorOne<App>? onError,
|
||||||
|
DataRequestLabel? label}) async {
|
||||||
|
final apps = await loadAppModels({
|
||||||
|
...params!,
|
||||||
|
'#d': [id]
|
||||||
|
});
|
||||||
|
// If ID not found in relay then clear from local storage
|
||||||
|
if (apps.isEmpty) {
|
||||||
|
deleteLocalById(id);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return apps.first;
|
||||||
|
}
|
||||||
|
|
||||||
|
static AndroidPackageManager? _packageManager;
|
||||||
|
|
||||||
|
Future<Map<String, String>> getInstalledAppsMap({bool defer = false}) async {
|
||||||
|
late List<PackageInfo>? infos;
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
_packageManager ??= AndroidPackageManager();
|
||||||
|
infos = await _packageManager!.getInstalledPackages();
|
||||||
|
} else {
|
||||||
|
infos = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
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 newState = {
|
||||||
|
for (final info in installedPackageInfos)
|
||||||
|
info.packageName!: info.versionName!
|
||||||
|
};
|
||||||
|
|
||||||
|
// Providers can't set other providers state
|
||||||
|
// while initializing, so defer setting state
|
||||||
|
if (defer) {
|
||||||
|
Future.microtask(() {
|
||||||
|
ref.read(installedAppProvider.notifier).state = newState;
|
||||||
|
});
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ref.read(installedAppProvider.notifier).state = newState;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
DeserializedData<App> deserialize(Object? data, {String? key}) {
|
||||||
|
// 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];
|
||||||
|
// }
|
||||||
|
return super.deserialize(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> _isHashMismatch(String path, String hash) async {
|
Future<bool> _isHashMismatch(String path, String hash) async {
|
||||||
return await Isolate.run(() async {
|
return await Isolate.run(() async {
|
||||||
final bytes = await File(path).readAsBytes();
|
final bytes = await File(path).readAsBytes();
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import 'package:flutter_data/flutter_data.dart';
|
||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
import 'package:purplebase/purplebase.dart';
|
||||||
|
import 'package:zapstore/models/app.dart';
|
||||||
|
import 'package:zapstore/models/nostr_adapter.dart';
|
||||||
|
|
||||||
|
part 'app_curation_set.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
@DataAdapter([NostrAdapter, AppCurationSetAdapter])
|
||||||
|
class AppCurationSet extends BaseAppCurationSet
|
||||||
|
with DataModelMixin<AppCurationSet> {
|
||||||
|
late final HasMany<App> apps;
|
||||||
|
}
|
||||||
|
|
||||||
|
mixin AppCurationSetAdapter on Adapter<AppCurationSet> {
|
||||||
|
@override
|
||||||
|
DeserializedData<AppCurationSet> deserialize(Object? data, {String? key}) {
|
||||||
|
final list = data is Iterable ? data : [data as Map];
|
||||||
|
for (final e in list) {
|
||||||
|
final map = e as Map<String, dynamic>;
|
||||||
|
final aValues = (map['tags'] as Iterable).where((t) => t[0] == 'a');
|
||||||
|
map['apps'] = aValues.map((e) => e[1].split(':')[2]);
|
||||||
|
}
|
||||||
|
return super.deserialize(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'app_curation_set.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// AdapterGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// ignore_for_file: non_constant_identifier_names, duplicate_ignore
|
||||||
|
|
||||||
|
mixin _$AppCurationSetAdapter on Adapter<AppCurationSet> {
|
||||||
|
static final Map<String, RelationshipMeta> _kAppCurationSetRelationshipMetas =
|
||||||
|
{
|
||||||
|
'apps': RelationshipMeta<App>(
|
||||||
|
name: 'apps',
|
||||||
|
type: 'apps',
|
||||||
|
kind: 'HasMany',
|
||||||
|
instance: (_) => (_ as AppCurationSet).apps,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, RelationshipMeta> get relationshipMetas =>
|
||||||
|
_kAppCurationSetRelationshipMetas;
|
||||||
|
|
||||||
|
@override
|
||||||
|
AppCurationSet deserializeLocal(map, {String? key}) {
|
||||||
|
map = transformDeserialize(map);
|
||||||
|
return internalWrapStopInit(() => _$AppCurationSetFromJson(map), key: key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> serializeLocal(model, {bool withRelationships = true}) {
|
||||||
|
final map = _$AppCurationSetToJson(model);
|
||||||
|
return transformSerialize(map, withRelationships: withRelationships);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final _appCurationSetsFinders = <String, dynamic>{};
|
||||||
|
|
||||||
|
class $AppCurationSetAdapter = Adapter<AppCurationSet>
|
||||||
|
with
|
||||||
|
_$AppCurationSetAdapter,
|
||||||
|
NostrAdapter<AppCurationSet>,
|
||||||
|
AppCurationSetAdapter;
|
||||||
|
|
||||||
|
final appCurationSetsAdapterProvider = Provider<Adapter<AppCurationSet>>(
|
||||||
|
(ref) =>
|
||||||
|
$AppCurationSetAdapter(ref, InternalHolder(_appCurationSetsFinders)));
|
||||||
|
|
||||||
|
extension AppCurationSetAdapterX on Adapter<AppCurationSet> {
|
||||||
|
NostrAdapter<AppCurationSet> get nostrAdapter =>
|
||||||
|
this as NostrAdapter<AppCurationSet>;
|
||||||
|
AppCurationSetAdapter get appCurationSetAdapter =>
|
||||||
|
this as AppCurationSetAdapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AppCurationSetRelationshipGraphNodeX
|
||||||
|
on RelationshipGraphNode<AppCurationSet> {
|
||||||
|
RelationshipGraphNode<App> get apps {
|
||||||
|
final meta = _$AppCurationSetAdapter
|
||||||
|
._kAppCurationSetRelationshipMetas['apps'] as RelationshipMeta<App>;
|
||||||
|
return meta.clone(
|
||||||
|
parent: this is RelationshipMeta ? this as RelationshipMeta : null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
AppCurationSet _$AppCurationSetFromJson(Map<String, dynamic> json) =>
|
||||||
|
AppCurationSet()
|
||||||
|
..id = json['id']
|
||||||
|
..pubkey = json['pubkey'] as String
|
||||||
|
..createdAt = DateTime.parse(json['createdAt'] as String)
|
||||||
|
..content = json['content'] as String
|
||||||
|
..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?
|
||||||
|
..apps = HasMany<App>.fromJson(json['apps'] as Map<String, dynamic>);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$AppCurationSetToJson(AppCurationSet instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'pubkey': instance.pubkey,
|
||||||
|
'createdAt': instance.createdAt.toIso8601String(),
|
||||||
|
'content': instance.content,
|
||||||
|
'kind': instance.kind,
|
||||||
|
'tags': instance.tags,
|
||||||
|
'signature': instance.signature,
|
||||||
|
'apps': instance.apps,
|
||||||
|
};
|
||||||
@@ -9,7 +9,7 @@ part 'file_metadata.g.dart';
|
|||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
@DataAdapter([NostrAdapter, FileMetadataAdapter])
|
@DataAdapter([NostrAdapter, FileMetadataAdapter])
|
||||||
class FileMetadata extends Event<FileMetadata> with BaseFileMetadata {
|
class FileMetadata extends BaseFileMetadata with DataModelMixin<FileMetadata> {
|
||||||
late final BelongsTo<User> author;
|
late final BelongsTo<User> author;
|
||||||
late final BelongsTo<Release> release = BelongsTo();
|
late final BelongsTo<Release> release = BelongsTo();
|
||||||
late final BelongsTo<User> signer;
|
late final BelongsTo<User> signer;
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import 'package:flutter_data/flutter_data.dart';
|
import 'package:flutter_data/flutter_data.dart';
|
||||||
import 'package:purplebase/purplebase.dart';
|
import 'package:purplebase/purplebase.dart';
|
||||||
|
|
||||||
abstract class Event<T extends Event<T>> = BaseEvent with DataModelMixin<T>;
|
mixin NostrAdapter<T extends DataModelMixin<T>> on Adapter<T> {
|
||||||
|
|
||||||
mixin NostrAdapter<T extends Event<T>> on Adapter<T> {
|
|
||||||
late final RelayMessageNotifier notifier =
|
late final RelayMessageNotifier notifier =
|
||||||
ref.read(relayMessageNotifierProvider.notifier);
|
ref.read(relayMessageNotifierProvider.notifier);
|
||||||
|
|
||||||
|
// TODO rethink this
|
||||||
Map<int, String> kindType = {
|
Map<int, String> kindType = {
|
||||||
0: 'users',
|
0: 'users',
|
||||||
3: 'users',
|
3: 'users',
|
||||||
1063: 'fileMetadata',
|
1063: 'fileMetadata',
|
||||||
30063: 'releases',
|
30063: 'releases',
|
||||||
|
30267: 'appCurationSets',
|
||||||
32267: 'apps'
|
32267: 'apps'
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,6 +32,7 @@ mixin NostrAdapter<T extends Event<T>> on Adapter<T> {
|
|||||||
if (dTags.length == 1) {
|
if (dTags.length == 1) {
|
||||||
map['id'] = (dTags.first as List)[1];
|
map['id'] = (dTags.first as List)[1];
|
||||||
}
|
}
|
||||||
|
// TODO remove
|
||||||
map['signer'] = map['pubkey'];
|
map['signer'] = map['pubkey'];
|
||||||
final zapTags = (map['tags'] as Iterable).where((t) => t[0] == 'zap');
|
final zapTags = (map['tags'] as Iterable).where((t) => t[0] == 'zap');
|
||||||
if (zapTags.length == 1) {
|
if (zapTags.length == 1) {
|
||||||
@@ -52,6 +53,7 @@ mixin NostrAdapter<T extends Event<T>> on Adapter<T> {
|
|||||||
return DeserializedData<T>(models, included: included);
|
return DeserializedData<T>(models, included: included);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO remove
|
||||||
int get kind =>
|
int get kind =>
|
||||||
kindType.entries.firstWhere((e) => e.value == internalType).key;
|
kindType.entries.firstWhere((e) => e.value == internalType).key;
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ part 'release.g.dart';
|
|||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
@DataAdapter([NostrAdapter, ReleaseAdapter])
|
@DataAdapter([NostrAdapter, ReleaseAdapter])
|
||||||
class Release extends Event<Release> with BaseRelease {
|
class Release extends BaseRelease with DataModelMixin<Release> {
|
||||||
late final HasMany<FileMetadata> artifacts;
|
late final HasMany<FileMetadata> artifacts;
|
||||||
late final BelongsTo<App> app;
|
late final BelongsTo<App> app;
|
||||||
late final BelongsTo<User> signer;
|
late final BelongsTo<User> signer;
|
||||||
|
|||||||
+14
-4
@@ -10,7 +10,7 @@ part 'user.g.dart';
|
|||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
@DataAdapter([NostrAdapter, UserAdapter])
|
@DataAdapter([NostrAdapter, UserAdapter])
|
||||||
class User extends Event<User> with BaseUser {
|
class User extends BaseUser with DataModelMixin<User> {
|
||||||
@DataRelationship(inverse: 'followers')
|
@DataRelationship(inverse: 'followers')
|
||||||
late final HasMany<User> following;
|
late final HasMany<User> following;
|
||||||
@DataRelationship(inverse: 'following')
|
@DataRelationship(inverse: 'following')
|
||||||
@@ -102,6 +102,10 @@ mixin UserAdapter on NostrAdapter<User> {
|
|||||||
|
|
||||||
final result =
|
final result =
|
||||||
await notifier.query(req, relayUrls: ['wss://relay.nostr.band']);
|
await notifier.query(req, relayUrls: ['wss://relay.nostr.band']);
|
||||||
|
if (onSuccess != null) {
|
||||||
|
return await onSuccess.call(DataResponse(statusCode: 200, body: result),
|
||||||
|
label ?? DataRequestLabel('findAll', type: type), this);
|
||||||
|
}
|
||||||
final data = await deserializeAsync(result, save: true);
|
final data = await deserializeAsync(result, save: true);
|
||||||
return data.models;
|
return data.models;
|
||||||
}
|
}
|
||||||
@@ -153,8 +157,8 @@ mixin UserAdapter on NostrAdapter<User> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<User>> getTrusted(User u1, User u2) async {
|
Future<List<User>> getTrusted(String npub1, String npub2) async {
|
||||||
final url = 'https://zap.store/api/trust/${u1.npub}/${u2.npub}';
|
final url = 'https://zap.store/api/trust/$npub1/$npub2';
|
||||||
final users = await sendRequest(
|
final users = await sendRequest(
|
||||||
Uri.parse(url),
|
Uri.parse(url),
|
||||||
onSuccess: (response, label) async {
|
onSuccess: (response, label) async {
|
||||||
@@ -163,7 +167,13 @@ mixin UserAdapter on NostrAdapter<User> {
|
|||||||
Map<String, dynamic>.from(jsonDecode(response.body.toString()));
|
Map<String, dynamic>.from(jsonDecode(response.body.toString()));
|
||||||
|
|
||||||
final trustedKeys = map.keys.map((npub) => npub.hexKey);
|
final trustedKeys = map.keys.map((npub) => npub.hexKey);
|
||||||
return await findAll(params: {'authors': trustedKeys});
|
return await findAll(
|
||||||
|
params: {'authors': trustedKeys},
|
||||||
|
onSuccess: (response, label, adapter) {
|
||||||
|
final data = deserialize(response.body);
|
||||||
|
return data.models;
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return users!;
|
return users!;
|
||||||
|
|||||||
+167
-133
@@ -8,13 +8,15 @@ import 'package:flutter_markdown/flutter_markdown.dart';
|
|||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:percent_indicator/linear_percent_indicator.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.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/release.dart';
|
import 'package:zapstore/models/release.dart';
|
||||||
|
import 'package:zapstore/models/settings.dart';
|
||||||
import 'package:zapstore/models/user.dart';
|
import 'package:zapstore/models/user.dart';
|
||||||
import 'package:zapstore/utils/extensions.dart';
|
import 'package:zapstore/utils/extensions.dart';
|
||||||
|
import 'package:zapstore/widgets/app_drawer.dart';
|
||||||
import 'package:zapstore/widgets/author_container.dart';
|
import 'package:zapstore/widgets/author_container.dart';
|
||||||
import 'package:zapstore/widgets/pill_widget.dart';
|
import 'package:zapstore/widgets/pill_widget.dart';
|
||||||
import 'package:zapstore/widgets/rounded_image.dart';
|
import 'package:zapstore/widgets/rounded_image.dart';
|
||||||
@@ -196,15 +198,15 @@ class AppDetailScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
Divider(height: 50),
|
Divider(height: 50),
|
||||||
Text(
|
Text(
|
||||||
'Releases'.toUpperCase(),
|
'Latest release'.toUpperCase(),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
letterSpacing: 3,
|
letterSpacing: 3,
|
||||||
fontWeight: FontWeight.w300,
|
fontWeight: FontWeight.w300,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
for (final release in app.releases.ordered)
|
if (app.releases.ordered.isNotEmpty)
|
||||||
ReleaseCard(release: release),
|
ReleaseCard(release: app.releases.ordered.first),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -413,55 +415,67 @@ class InstallButton extends ConsumerWidget {
|
|||||||
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));
|
||||||
|
|
||||||
return SizedBox(
|
return GestureDetector(
|
||||||
width: double.infinity,
|
onTap: switch (app.status) {
|
||||||
child: ElevatedButton(
|
AppInstallStatus.differentArchitecture => null,
|
||||||
onPressed: switch (app.status) {
|
AppInstallStatus.downgrade => null,
|
||||||
AppInstallStatus.differentArchitecture => null,
|
AppInstallStatus.updated => () {
|
||||||
AppInstallStatus.downgrade => null,
|
LaunchApp.openApp(androidPackageName: app.id!.toString());
|
||||||
AppInstallStatus.updated => () {
|
},
|
||||||
LaunchApp.openApp(androidPackageName: app.id!.toString());
|
_ => switch (progress) {
|
||||||
|
IdleInstallProgress() => () {
|
||||||
|
print(app.canInstall);
|
||||||
|
// show trust dialog only if first install
|
||||||
|
if (app.canInstall) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return InstallAlertDialog(app: app);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else if (app.canUpdate) {
|
||||||
|
app.install();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ErrorInstallProgress(:final e) => () {
|
||||||
|
// show error and reset state to idle
|
||||||
|
context.showError(e.toString());
|
||||||
|
ref
|
||||||
|
.read(installationProgressProvider(app.id!.toString())
|
||||||
|
.notifier)
|
||||||
|
.state = IdleInstallProgress();
|
||||||
|
},
|
||||||
|
_ => null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: LinearPercentIndicator(
|
||||||
|
lineHeight: 40,
|
||||||
|
percent: switch (progress) {
|
||||||
|
DeviceInstallProgress() => 1,
|
||||||
|
DownloadingInstallProgress(:final progress) => progress,
|
||||||
|
_ => switch (app.status) {
|
||||||
|
AppInstallStatus.updated => 1,
|
||||||
|
_ => 0,
|
||||||
},
|
},
|
||||||
_ => switch (progress) {
|
|
||||||
IdleInstallProgress() => () {
|
|
||||||
// show trust dialog only if first install
|
|
||||||
if (app.canInstall) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
return InstallAlertDialog(app: app);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else if (app.canUpdate) {
|
|
||||||
app.install();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ErrorInstallProgress(:final e) => () {
|
|
||||||
// show error and reset state to idle
|
|
||||||
context.showError(e.toString());
|
|
||||||
ref
|
|
||||||
.read(installationProgressProvider(app.id!.toString())
|
|
||||||
.notifier)
|
|
||||||
.state = IdleInstallProgress();
|
|
||||||
},
|
|
||||||
_ => null,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
backgroundColor: switch (progress) {
|
||||||
disabledForegroundColor: Colors.white,
|
ErrorInstallProgress() => Colors.red,
|
||||||
disabledBackgroundColor: Colors.blue[700],
|
_ => Colors.blue[700],
|
||||||
foregroundColor: Colors.white,
|
},
|
||||||
backgroundColor: switch (progress) {
|
progressColor: Colors.blue[800],
|
||||||
ErrorInstallProgress() => Colors.red,
|
barRadius: Radius.circular(18),
|
||||||
_ => Colors.blue[700],
|
animateFromLastPercent: true,
|
||||||
}),
|
center: switch (app.status) {
|
||||||
child: switch (app.status) {
|
|
||||||
AppInstallStatus.loading =>
|
AppInstallStatus.loading =>
|
||||||
SizedBox(width: 14, height: 14, child: CircularProgressIndicator()),
|
SizedBox(width: 14, height: 14, child: CircularProgressIndicator()),
|
||||||
AppInstallStatus.differentArchitecture =>
|
AppInstallStatus.differentArchitecture => Text(
|
||||||
Text('Sorry, release does not support your device'),
|
'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.installedVersion ?? ''} is higher, can\'t downgrade',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
AppInstallStatus.updated => Text('Open'),
|
AppInstallStatus.updated => Text('Open'),
|
||||||
_ => switch (progress) {
|
_ => switch (progress) {
|
||||||
IdleInstallProgress() => app.canUpdate
|
IdleInstallProgress() => app.canUpdate
|
||||||
@@ -498,59 +512,71 @@ class InstallAlertDialog extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final user = ref.settings.watchOne('_').model!.user.value;
|
final user = ref.settings
|
||||||
|
.watchOne('_', alsoWatch: (_) => {_.user})
|
||||||
|
.model!
|
||||||
|
.user
|
||||||
|
.value;
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
elevation: 10,
|
elevation: 10,
|
||||||
title: Text(
|
title: Text(
|
||||||
'Are you sure you want to install ${app.name}?',
|
'Are you sure you want to install ${app.name}?',
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
content: Column(
|
content: SingleChildScrollView(
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Text(
|
children: [
|
||||||
'By installing this app you are trusting the signer now and for all future updates. Make sure you know who they are.'),
|
Text(
|
||||||
Gap(20),
|
'By installing this app you are trusting the signer now and for all future updates. Make sure you know who they are.'),
|
||||||
// SignerAndDeveloperRow(app: app),
|
Gap(20),
|
||||||
AuthorContainer(
|
// SignerAndDeveloperRow(app: app),
|
||||||
user: app.signer.value!, text: 'Signed by', oneLine: true),
|
if (app.signer.value != null)
|
||||||
Gap(20),
|
AuthorContainer(
|
||||||
if (user != null)
|
user: app.signer.value!, text: 'Signed by', oneLine: true),
|
||||||
Column(
|
Gap(20),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
if (app.signer.value != null)
|
||||||
children: [
|
WebOfTrustContainer(
|
||||||
WebOfTrustContainer(user: user, app: app),
|
user: user, npub: user?.npub, npub2: app.signer.value!.npub),
|
||||||
Gap(20),
|
if (user != null)
|
||||||
Text('The app will be downloaded from:\n'),
|
Column(
|
||||||
Text(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
app.latestMetadata!.urls.firstOrNull ??
|
children: [
|
||||||
'https://cdn.zap.store/${app.latestMetadata!.hash}',
|
Gap(20),
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
Text('The app will be downloaded from:\n'),
|
||||||
),
|
Text(
|
||||||
],
|
app.latestMetadata!.urls.firstOrNull ??
|
||||||
),
|
'https://cdn.zap.store/${app.latestMetadata!.hash}',
|
||||||
],
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
if (user == null)
|
if (user == null)
|
||||||
TextButton(
|
LoginContainer(
|
||||||
onPressed: () {
|
minimal: true,
|
||||||
Navigator.of(context).pop();
|
labelText:
|
||||||
scaffoldKey.currentState!.openDrawer();
|
'Log in to view your own connections (NIP-05 or npub, no nsec!)',
|
||||||
},
|
),
|
||||||
child: Text('Log in to view web of trust',
|
Padding(
|
||||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
app.install();
|
||||||
|
// NOTE: can't use context.pop()
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: user != null
|
||||||
|
? Text('Install', style: TextStyle(fontWeight: FontWeight.bold))
|
||||||
|
: Text(
|
||||||
|
'I trust the signer, install anyway',
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
app.install();
|
|
||||||
// NOTE: can't use context.pop()
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
child: user != null
|
|
||||||
? Text('Install', style: TextStyle(fontWeight: FontWeight.bold))
|
|
||||||
: Text('I trust the signer, install anyway'),
|
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -565,66 +591,74 @@ class InstallAlertDialog extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final wotProvider = FutureProvider.autoDispose
|
final wotProvider = FutureProvider.autoDispose
|
||||||
.family<List<User>, ({User user, App app})>((ref, arg) async {
|
.family<List<User>, ({String npub1, String npub2})>((ref, arg) async {
|
||||||
return ref.users.userAdapter.getTrusted(arg.user, arg.app.signer.value!);
|
final _ =
|
||||||
|
ref.settings.watchOne('_', alsoWatch: (_) => {_.user}).model!.user.value;
|
||||||
|
return ref.users.userAdapter.getTrusted(arg.npub1, arg.npub2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const franzapsNpub =
|
||||||
|
'npub1wf4pufsucer5va8g9p0rj5dnhvfeh6d8w0g6eayaep5dhps6rsgs43dgh9';
|
||||||
|
|
||||||
class WebOfTrustContainer extends HookConsumerWidget {
|
class WebOfTrustContainer extends HookConsumerWidget {
|
||||||
const WebOfTrustContainer({
|
const WebOfTrustContainer({
|
||||||
super.key,
|
super.key,
|
||||||
required this.user,
|
this.user,
|
||||||
required this.app,
|
String? npub,
|
||||||
|
required this.npub2,
|
||||||
});
|
});
|
||||||
|
|
||||||
final User user;
|
final User? user;
|
||||||
final App app;
|
final String npub2;
|
||||||
|
String get npub => user?.npub ?? franzapsNpub;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return switch (ref.watch(wotProvider((user: user, app: app)))) {
|
return switch (ref.watch(wotProvider((npub1: npub, npub2: npub2)))) {
|
||||||
AsyncData<List<User>>(value: final trustedUsers) => () {
|
AsyncData<List<User>>(value: final trustedUsers) => Builder(
|
||||||
// Crappy workaround until we fix the graph,
|
builder: (context) {
|
||||||
// jack no longer follows zap.store but it still shows
|
final hasUser =
|
||||||
const jacksNpub =
|
trustedUsers.firstWhereOrNull((u) => u.npub == npub) != null;
|
||||||
'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m';
|
return RichText(
|
||||||
final hasUser = trustedUsers.contains(user);
|
text: TextSpan(
|
||||||
return Wrap(
|
children: [
|
||||||
children: [
|
if (hasUser && npub != franzapsNpub) TextSpan(text: 'You, '),
|
||||||
if (hasUser) Text('You, '),
|
for (final tu in trustedUsers)
|
||||||
for (final tu in trustedUsers)
|
if (tu.npub != npub)
|
||||||
if (tu != user && tu.npub != jacksNpub)
|
TextSpan(
|
||||||
Wrap(
|
style: TextStyle(height: 1.6),
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
|
||||||
children: [
|
|
||||||
Wrap(
|
|
||||||
children: [
|
children: [
|
||||||
RoundedImage(url: tu.avatarUrl, size: 22),
|
WidgetSpan(
|
||||||
SizedBox(width: 4),
|
alignment: PlaceholderAlignment.middle,
|
||||||
Text(
|
child: Wrap(
|
||||||
softWrap: true,
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
'${tu.nameOrNpub}${trustedUsers.indexOf(tu) == trustedUsers.length - 1 ? '' : ','}',
|
children: [
|
||||||
|
RoundedImage(url: tu.avatarUrl, size: 20),
|
||||||
|
Text(
|
||||||
|
' ${tu.nameOrNpub}${trustedUsers.indexOf(tu) == trustedUsers.length - 1 ? '' : ', '}',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
SizedBox(width: 6),
|
|
||||||
if (trustedUsers.indexOf(tu) ==
|
if (trustedUsers.indexOf(tu) ==
|
||||||
trustedUsers.length - 1)
|
trustedUsers.length - 1)
|
||||||
Text('and others follow this signer.',
|
TextSpan(
|
||||||
softWrap: true)
|
text: ' and others follow this signer on nostr.',
|
||||||
|
)
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
);
|
||||||
);
|
},
|
||||||
}(),
|
),
|
||||||
AsyncError(:final error) =>
|
AsyncError(:final error) =>
|
||||||
Center(child: Text('Error checking web of trust: $error')),
|
Center(child: Text('Error checking web of trust: $error')),
|
||||||
_ => Center(
|
_ => Center(
|
||||||
child: Column(
|
child: SizedBox(
|
||||||
children: [
|
width: 14, height: 14, child: CircularProgressIndicator()),
|
||||||
Text('Loading web of trust connections...'),
|
)
|
||||||
SizedBox(width: 14, height: 14, child: CircularProgressIndicator()),
|
|
||||||
],
|
|
||||||
))
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,16 +83,6 @@ class SearchScreen extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
Gap(10),
|
Gap(10),
|
||||||
// if (state.hasMessage)
|
|
||||||
// Expanded(
|
|
||||||
// child: Center(
|
|
||||||
// child: Text(
|
|
||||||
// state.message!,
|
|
||||||
// textAlign: TextAlign.center,
|
|
||||||
// style: context.theme.textTheme.bodyLarge,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
if (state.hasError) Text(state.error!.toString()),
|
if (state.hasError) Text(state.error!.toString()),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
@@ -120,11 +110,16 @@ class SearchScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
final categoriesAppProvider =
|
final categoriesAppProvider =
|
||||||
FutureProvider.family<List<App>, AppCategory>((ref, category) async {
|
FutureProvider.family<List<App>, AppCategory>((ref, category) async {
|
||||||
final apps = await ref.apps.findAll(params: {'#d': appCategories[category]!});
|
final appSets = await ref.appCurationSets.findAll(params: {
|
||||||
|
'#d': [category.name]
|
||||||
|
});
|
||||||
|
if (appSets.isEmpty) return [];
|
||||||
|
final apps = await ref.apps
|
||||||
|
.findAll(params: {'#d': appSets.first.aTags.map((a) => a.split(':')[2])});
|
||||||
return apps..shuffle();
|
return apps..shuffle();
|
||||||
});
|
});
|
||||||
|
|
||||||
final selectedAppCategoryProvider = StateProvider((_) => AppCategory.wallets);
|
final selectedAppCategoryProvider = StateProvider((_) => AppCategory.basics);
|
||||||
|
|
||||||
class CategoriesContainer extends HookConsumerWidget {
|
class CategoriesContainer extends HookConsumerWidget {
|
||||||
const CategoriesContainer({
|
const CategoriesContainer({
|
||||||
@@ -196,10 +191,10 @@ class WrapLayout extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return LayoutGrid(
|
return LayoutGrid(
|
||||||
key: UniqueKey(),
|
key: UniqueKey(),
|
||||||
columnGap: 10, // Adjust the gap between columns as needed
|
columnGap: 10,
|
||||||
rowGap: 10, // Adjust the gap between rows as needed
|
rowGap: 10,
|
||||||
rowSizes:
|
rowSizes:
|
||||||
List<FixedTrackSize>.generate((8 / columns).ceil(), (_) => 110.px),
|
List<FixedTrackSize>.generate((8 / columns).ceil(), (_) => 130.px),
|
||||||
columnSizes: List<FlexibleTrackSize>.generate(columns, (_) => 1.fr),
|
columnSizes: List<FlexibleTrackSize>.generate(columns, (_) => 1.fr),
|
||||||
children:
|
children:
|
||||||
List.generate(8, (i) => TinyAppCard(app: apps.elementAtOrNull(i))),
|
List.generate(8, (i) => TinyAppCard(app: apps.elementAtOrNull(i))),
|
||||||
@@ -259,65 +254,11 @@ class SearchResultNotifier extends AsyncNotifier<List<App>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum AppCategory {
|
enum AppCategory {
|
||||||
wallets(label: 'Wallets'),
|
|
||||||
nostr(label: 'Nostr'),
|
|
||||||
basics(label: 'Basics'),
|
basics(label: 'Basics'),
|
||||||
privacy(label: 'Privacy & Security'),
|
nostr(label: 'Nostr'),
|
||||||
productivity(label: 'Productivity');
|
bitcoin(label: 'Bitcoin'),
|
||||||
|
privacy(label: 'Privacy & Security');
|
||||||
|
|
||||||
final String label;
|
final String label;
|
||||||
const AppCategory({required this.label});
|
const AppCategory({required this.label});
|
||||||
}
|
}
|
||||||
|
|
||||||
final appCategories = {
|
|
||||||
AppCategory.basics: [
|
|
||||||
"org.fossify.notes",
|
|
||||||
"org.fossify.filemanager",
|
|
||||||
"org.fossify.contacts",
|
|
||||||
"org.fossify.calendar",
|
|
||||||
"io.sanford.wormhole_william",
|
|
||||||
"me.zhanghai.android.files",
|
|
||||||
"org.breezyweather",
|
|
||||||
"app.organicmaps.web",
|
|
||||||
],
|
|
||||||
AppCategory.wallets: [
|
|
||||||
"com.greenaddress.greenbits_android_wallet",
|
|
||||||
"io.nunchuk.android",
|
|
||||||
"io.bluewallet.bluewallet",
|
|
||||||
"io.aquawallet.android",
|
|
||||||
"app.zeusln.zeus",
|
|
||||||
"com.mutinywallet.mutinywallet",
|
|
||||||
"xyz.elliptica.enuts.beta",
|
|
||||||
"fr.acinq.phoenix.mainnet",
|
|
||||||
],
|
|
||||||
AppCategory.privacy: [
|
|
||||||
"chat.simplex.app",
|
|
||||||
"im.molly.app",
|
|
||||||
"com.kunzisoft.keepass.free",
|
|
||||||
"com.x8bit.bitwarden",
|
|
||||||
"io.simplelogin.android.fdroid",
|
|
||||||
"eu.darken.myperm",
|
|
||||||
"net.ivpn.client",
|
|
||||||
"ch.protonvpn.android",
|
|
||||||
],
|
|
||||||
AppCategory.nostr: [
|
|
||||||
"com.greenart7c3.citrine",
|
|
||||||
"com.greenart7c3.nostrsigner",
|
|
||||||
"net.primal.android",
|
|
||||||
"com.oxchat.nostr",
|
|
||||||
"com.vitorpamplona.amethyst",
|
|
||||||
"com.nostr.universe",
|
|
||||||
"com.dluvian.voyage",
|
|
||||||
"com.apps.freerse",
|
|
||||||
],
|
|
||||||
AppCategory.productivity: [
|
|
||||||
"io.ente.photos.independent",
|
|
||||||
"md.obsidian",
|
|
||||||
"com.logseq.app",
|
|
||||||
"com.nutomic.syncthingandroid",
|
|
||||||
"ch.protonmail.android",
|
|
||||||
"org.localsend.localsend_app",
|
|
||||||
"org.fossify.gallery",
|
|
||||||
"org.fossify.musicplayer",
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import 'package:async_button_builder/async_button_builder.dart';
|
import 'package:async_button_builder/async_button_builder.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_data/flutter_data.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:flutter_phoenix/flutter_phoenix.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import 'package:purplebase/purplebase.dart';
|
import 'package:purplebase/purplebase.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/settings.dart';
|
||||||
import 'package:zapstore/utils/extensions.dart';
|
import 'package:zapstore/utils/extensions.dart';
|
||||||
|
import 'package:zapstore/widgets/app_drawer.dart';
|
||||||
|
|
||||||
class SettingsScreen extends HookConsumerWidget {
|
class SettingsScreen extends HookConsumerWidget {
|
||||||
const SettingsScreen({super.key});
|
const SettingsScreen({super.key});
|
||||||
@@ -15,11 +20,14 @@ class SettingsScreen extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final controller = useTextEditingController();
|
final controller = useTextEditingController();
|
||||||
final user = ref.settings.watchOne('_').model!.user.value;
|
final user = ref.settings
|
||||||
|
.watchOne('_', alsoWatch: (_) => {_.user})
|
||||||
|
.model!
|
||||||
|
.user
|
||||||
|
.value;
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
@@ -40,27 +48,69 @@ class SettingsScreen extends HookConsumerWidget {
|
|||||||
maxLines: 10,
|
maxLines: 10,
|
||||||
),
|
),
|
||||||
Gap(20),
|
Gap(20),
|
||||||
AsyncButtonBuilder(
|
if (user == null) LoginContainer(minimal: true),
|
||||||
loadingWidget: SizedBox(
|
if (user != null)
|
||||||
width: 14, height: 14, child: CircularProgressIndicator()),
|
AsyncButtonBuilder(
|
||||||
onPressed: () async {
|
loadingWidget: SizedBox(
|
||||||
if (user == null) {
|
width: 14, height: 14, child: CircularProgressIndicator()),
|
||||||
scaffoldKey.currentState!.openDrawer();
|
onPressed: () async {
|
||||||
return;
|
final text =
|
||||||
}
|
'${controller.text.trim()} [from ${user.npub} on ${DateFormat('MMMM d, y').format(DateTime.now())}]';
|
||||||
final text = '${controller.text.trim()} [from ${user.npub}]';
|
final event = BaseEvent.partial(content: text).sign(kI);
|
||||||
final event = BaseEvent.partial(content: text).sign(kI);
|
await ref.apps.nostrAdapter.notifier
|
||||||
await ref.apps.nostrAdapter.notifier
|
.publish(event, relayUrls: ['wss://relay.zap.store']);
|
||||||
.publish(event, relayUrls: ['wss://relay.zap.store']);
|
controller.clear();
|
||||||
controller.clear();
|
},
|
||||||
},
|
builder: (context, child, callback, state) {
|
||||||
builder: (context, child, callback, buttonState) {
|
return switch (state) {
|
||||||
return ElevatedButton(
|
_ => ElevatedButton(
|
||||||
onPressed: callback,
|
onPressed: callback,
|
||||||
child: child,
|
child: child,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
child: Text('Send as ${user.nameOrNpub}'),
|
||||||
|
),
|
||||||
|
Gap(40),
|
||||||
|
Divider(),
|
||||||
|
Gap(40),
|
||||||
|
Text(
|
||||||
|
'Tools',
|
||||||
|
style: context.theme.textTheme.headlineLarge!
|
||||||
|
.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text('Confirm clear'),
|
||||||
|
content: Text(
|
||||||
|
'Are you sure you want to clear the local cache and restart the app?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
child: Text('Cancel'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
child: Text('Confirm'),
|
||||||
|
onPressed: () {
|
||||||
|
ref.read(localStorageProvider).destroy().then((_) {
|
||||||
|
Phoenix.rebirth(context);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
context.go('/');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Text(user != null ? 'Send' : 'Tap to log in'),
|
child: Text('Delete local cache'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
+20
-20
@@ -5,7 +5,6 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:zapstore/models/app.dart';
|
import 'package:zapstore/models/app.dart';
|
||||||
import 'package:zapstore/models/release.dart';
|
|
||||||
import 'package:zapstore/widgets/pill_widget.dart';
|
import 'package:zapstore/widgets/pill_widget.dart';
|
||||||
import 'package:zapstore/widgets/rounded_image.dart';
|
import 'package:zapstore/widgets/rounded_image.dart';
|
||||||
|
|
||||||
@@ -74,6 +73,7 @@ class AppCard extends HookConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
Gap(2),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
@@ -82,7 +82,7 @@ class AppCard extends HookConsumerWidget {
|
|||||||
app!.name!,
|
app!.name!,
|
||||||
minFontSize: 16,
|
minFontSize: 16,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 20, fontWeight: FontWeight.bold),
|
fontSize: 19, fontWeight: FontWeight.bold),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
),
|
),
|
||||||
@@ -95,17 +95,14 @@ class AppCard extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Padding(
|
Gap(6),
|
||||||
padding: const EdgeInsets.only(top: 4),
|
Text(
|
||||||
child: Text(
|
app!.content,
|
||||||
// TODO fix markdown?
|
style:
|
||||||
app!.content,
|
TextStyle(fontSize: 14, fontWeight: FontWeight.w300),
|
||||||
style: TextStyle(
|
overflow: TextOverflow.ellipsis,
|
||||||
fontSize: 14, fontWeight: FontWeight.w300),
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
softWrap: true,
|
||||||
maxLines: 1,
|
|
||||||
softWrap: true,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -131,16 +128,19 @@ class TinyAppCard extends HookConsumerWidget {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
context.go('/details', extra: app);
|
context.go('/details', extra: app);
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Container(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: 120,
|
||||||
|
),
|
||||||
padding: const EdgeInsets.all(10),
|
padding: const EdgeInsets.all(10),
|
||||||
child: app == null
|
child: app == null
|
||||||
? Skeletonizer.zone(
|
? Skeletonizer.zone(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Bone.square(uniRadius: 10, size: 58),
|
Bone.square(uniRadius: 10, size: 60),
|
||||||
Gap(8),
|
Gap(8),
|
||||||
Bone.text(),
|
Bone.multiText(lines: 2, fontSize: 10),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -150,7 +150,7 @@ class TinyAppCard extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
RoundedImage(
|
RoundedImage(
|
||||||
url: app!.icons.firstOrNull,
|
url: app!.icons.firstOrNull,
|
||||||
size: 60,
|
size: 58,
|
||||||
radius: 12,
|
radius: 12,
|
||||||
),
|
),
|
||||||
Gap(8),
|
Gap(8),
|
||||||
@@ -160,10 +160,10 @@ class TinyAppCard extends HookConsumerWidget {
|
|||||||
app!.name!,
|
app!.name!,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
minFontSize: 10,
|
minFontSize: 10,
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 11),
|
||||||
fontSize: 13, fontWeight: FontWeight.bold),
|
|
||||||
overflow: TextOverflow.clip,
|
overflow: TextOverflow.clip,
|
||||||
maxLines: 1,
|
maxLines: 2,
|
||||||
|
wrapWords: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
+79
-71
@@ -7,8 +7,13 @@ import 'package:zapstore/main.data.dart';
|
|||||||
import 'package:zapstore/models/settings.dart';
|
import 'package:zapstore/models/settings.dart';
|
||||||
import 'package:zapstore/widgets/rounded_image.dart';
|
import 'package:zapstore/widgets/rounded_image.dart';
|
||||||
|
|
||||||
class AppDrawer extends HookConsumerWidget {
|
class LoginContainer extends HookConsumerWidget {
|
||||||
AppDrawer({super.key});
|
final String labelText;
|
||||||
|
final bool minimal;
|
||||||
|
LoginContainer(
|
||||||
|
{super.key,
|
||||||
|
this.minimal = false,
|
||||||
|
this.labelText = 'Input your NIP-05 or npub (no nsec!)'});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -19,83 +24,86 @@ class AppDrawer extends HookConsumerWidget {
|
|||||||
.value;
|
.value;
|
||||||
final controller = useTextEditingController();
|
final controller = useTextEditingController();
|
||||||
|
|
||||||
return Padding(
|
return Column(
|
||||||
padding: const EdgeInsets.only(top: 24, left: 16),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: ListView(
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
if (!minimal) RoundedImage(url: user?.avatarUrl, size: 46),
|
||||||
|
if (!minimal) Gap(10),
|
||||||
|
if (user != null && !minimal)
|
||||||
|
Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
user.nameOrNpub,
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
Gap(4),
|
||||||
|
Icon(Icons.verified, color: Colors.lightBlue, size: 18),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
// if (user.following.isNotEmpty)
|
||||||
|
// Text('${user.following.length} contacts'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (user == null)
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Gap(20),
|
||||||
padding: EdgeInsets.only(top: 4, bottom: 4),
|
Text(labelText),
|
||||||
child: Row(
|
TextField(
|
||||||
children: [
|
autocorrect: false,
|
||||||
RoundedImage(url: user?.avatarUrl, size: 46),
|
controller: controller,
|
||||||
Gap(10),
|
decoration: InputDecoration(
|
||||||
if (user != null)
|
suffixIcon: AsyncButtonBuilder(
|
||||||
Column(
|
loadingWidget: SizedBox(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
width: 14,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
height: 14,
|
||||||
children: [
|
child: CircularProgressIndicator(),
|
||||||
Row(
|
),
|
||||||
children: [
|
onPressed: () async {
|
||||||
Text(
|
final user =
|
||||||
user.nameOrNpub,
|
await ref.users.findOne(controller.text.trim());
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
ref.settings.findOneLocalById('_')!.user.value = user;
|
||||||
),
|
},
|
||||||
Gap(4),
|
builder: (context, child, callback, buttonState) {
|
||||||
Icon(Icons.verified,
|
return Padding(
|
||||||
color: Colors.lightBlue, size: 18),
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
],
|
child: SizedBox(
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: callback,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
disabledBackgroundColor: Colors.transparent,
|
||||||
|
backgroundColor: Colors.transparent),
|
||||||
|
child: child,
|
||||||
),
|
),
|
||||||
// if (user.following.isNotEmpty)
|
),
|
||||||
// Text('${user.following.length} contacts'),
|
);
|
||||||
],
|
},
|
||||||
),
|
child: Text('Log in'),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (user == null)
|
|
||||||
TextField(
|
|
||||||
autocorrect: false,
|
|
||||||
controller: controller,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'NIP-05 address or npub (no nsec!)',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Gap(5),
|
|
||||||
if (user == null)
|
|
||||||
AsyncButtonBuilder(
|
|
||||||
loadingWidget: SizedBox(
|
|
||||||
width: 14,
|
|
||||||
height: 14,
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
onPressed: () async {
|
|
||||||
final user =
|
|
||||||
await ref.users.findOne(controller.text.trim());
|
|
||||||
ref.settings.findOneLocalById('_')!.user.value = user;
|
|
||||||
},
|
|
||||||
builder: (context, child, callback, buttonState) {
|
|
||||||
return ElevatedButton(
|
|
||||||
onPressed: callback,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Text('Log in'),
|
|
||||||
),
|
|
||||||
if (user != null)
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () async {
|
|
||||||
ref.settings.findOneLocalById('_')!.user.value = null;
|
|
||||||
controller.clear();
|
|
||||||
},
|
|
||||||
child: Text('Log out'),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
Gap(5),
|
||||||
),
|
if (user != null && !minimal)
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
ref.settings.findOneLocalById('_')!.user.value = null;
|
||||||
|
controller.clear();
|
||||||
|
},
|
||||||
|
child: Text('Log out'),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:zapstore/main.data.dart';
|
import 'package:zapstore/main.data.dart';
|
||||||
|
import 'package:zapstore/models/settings.dart';
|
||||||
import 'package:zapstore/widgets/rounded_image.dart';
|
import 'package:zapstore/widgets/rounded_image.dart';
|
||||||
|
|
||||||
class UserAvatar extends HookConsumerWidget {
|
class UserAvatar extends HookConsumerWidget {
|
||||||
@@ -8,7 +9,11 @@ class UserAvatar extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final user = ref.settings.watchOne('_').model!.user.value;
|
final user = ref.settings
|
||||||
|
.watchOne('_', alsoWatch: (_) => {_.user})
|
||||||
|
.model!
|
||||||
|
.user
|
||||||
|
.value;
|
||||||
return RoundedImage(url: user?.avatarUrl, size: 46);
|
return RoundedImage(url: user?.avatarUrl, size: 46);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -394,6 +394,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.1"
|
version: "0.7.1"
|
||||||
|
flutter_phoenix:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_phoenix
|
||||||
|
sha256: "39589dac934ea476d0e43fb60c1ddfba58f14960743640c8250dea11c4333378"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
flutter_riverpod:
|
flutter_riverpod:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -716,6 +724,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.2.1"
|
||||||
|
percent_indicator:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: percent_indicator
|
||||||
|
sha256: c37099ad833a883c9d71782321cb65c3a848c21b6939b6185f0ff6640d05814c
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.2.3"
|
||||||
permission_handler:
|
permission_handler:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
+3
-1
@@ -2,7 +2,7 @@ name: zapstore
|
|||||||
description: The permissionless app store
|
description: The permissionless app store
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 0.1.1
|
version: 0.1.2+12
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.3.0 <4.0.0'
|
sdk: '>=3.3.0 <4.0.0'
|
||||||
@@ -40,6 +40,8 @@ dependencies:
|
|||||||
install_plugin: ^2.1.0
|
install_plugin: ^2.1.0
|
||||||
flutter_layout_grid: ^2.0.6
|
flutter_layout_grid: ^2.0.6
|
||||||
skeletonizer: ^1.1.2+1
|
skeletonizer: ^1.1.2+1
|
||||||
|
flutter_phoenix: ^1.1.1
|
||||||
|
percent_indicator: ^4.2.3
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
purplebase:
|
purplebase:
|
||||||
|
|||||||
Reference in New Issue
Block a user