mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 20:48:24 +00:00
Aim for MVP
This commit is contained in:
@@ -23,6 +23,8 @@ linter:
|
||||
rules:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
prefer_const_constructors: false
|
||||
avoid_print: false
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
|
||||
+57
-29
@@ -1,3 +1,5 @@
|
||||
// ignore_for_file: prefer_const_literals_to_create_immutables, prefer_const_constructors
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_data/flutter_data.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -29,6 +31,10 @@ class ZapstoreApp extends StatelessWidget {
|
||||
return MaterialApp.router(
|
||||
routerConfig: goRouter,
|
||||
debugShowCheckedModeBanner: false,
|
||||
darkTheme: ThemeData(
|
||||
primarySwatch: Colors.purple,
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
theme: ThemeData.dark(useMaterial3: true),
|
||||
);
|
||||
}
|
||||
@@ -39,6 +45,8 @@ final _rootNavigatorKey = GlobalKey<NavigatorState>();
|
||||
final _searchNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'search');
|
||||
final _updatesNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'updates');
|
||||
final _profileNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'profile');
|
||||
final _notificationsNavigatorKey =
|
||||
GlobalKey<NavigatorState>(debugLabel: 'notifications');
|
||||
|
||||
final goRouter = GoRouter(
|
||||
initialLocation: '/search',
|
||||
@@ -82,8 +90,8 @@ final goRouter = GoRouter(
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: Center(
|
||||
child: ElevatedButton(
|
||||
onPressed: () => context.go('/search/details', extra: 1),
|
||||
child: const Text('Go to the Details screen'),
|
||||
onPressed: () => {},
|
||||
child: const Text('Updates coming soon!'),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -103,6 +111,22 @@ final goRouter = GoRouter(
|
||||
),
|
||||
],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
navigatorKey: _notificationsNavigatorKey,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/notifications',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: Center(
|
||||
child: ElevatedButton(
|
||||
onPressed: () => {},
|
||||
child: const Text('Notifications coming soon!'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -136,34 +160,29 @@ class ScaffoldWithNestedNavigation extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final initializer = ref.watch(newInitializer);
|
||||
return SafeArea(
|
||||
child: initializer.when(
|
||||
data: (_) => LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.maxWidth < 450) {
|
||||
return ScaffoldWithNavigationBar(
|
||||
body: navigationShell,
|
||||
selectedIndex: navigationShell.currentIndex,
|
||||
onDestinationSelected: _goBranch,
|
||||
);
|
||||
} else {
|
||||
return ScaffoldWithNavigationRail(
|
||||
body: navigationShell,
|
||||
selectedIndex: navigationShell.currentIndex,
|
||||
onDestinationSelected: _goBranch,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
error: (e, _) => Text('error'),
|
||||
loading: () => CircularProgressIndicator(),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.maxWidth < 450) {
|
||||
return ScaffoldWithNavigationBar(
|
||||
body: navigationShell,
|
||||
selectedIndex: navigationShell.currentIndex,
|
||||
onDestinationSelected: _goBranch,
|
||||
);
|
||||
} else {
|
||||
return ScaffoldWithNavigationRail(
|
||||
body: navigationShell,
|
||||
selectedIndex: navigationShell.currentIndex,
|
||||
onDestinationSelected: _goBranch,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ScaffoldWithNavigationBar extends StatelessWidget {
|
||||
class ScaffoldWithNavigationBar extends HookConsumerWidget {
|
||||
const ScaffoldWithNavigationBar({
|
||||
super.key,
|
||||
required this.body,
|
||||
@@ -175,9 +194,14 @@ class ScaffoldWithNavigationBar extends StatelessWidget {
|
||||
final ValueChanged<int> onDestinationSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final initializer = ref.watch(newInitializer);
|
||||
return Scaffold(
|
||||
body: body,
|
||||
body: initializer.when(
|
||||
data: (_) => body,
|
||||
error: (e, _) => const Text('error'),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
bottomNavigationBar: NavigationBar(
|
||||
selectedIndex: selectedIndex,
|
||||
labelBehavior: NavigationDestinationLabelBehavior.alwaysHide,
|
||||
@@ -226,18 +250,22 @@ class ScaffoldWithNavigationRail extends StatelessWidget {
|
||||
onDestinationSelected: onDestinationSelected,
|
||||
labelType: NavigationRailLabelType.all,
|
||||
destinations: [
|
||||
NavigationRailDestination(
|
||||
const NavigationRailDestination(
|
||||
label: Text('Search'),
|
||||
icon: Icon(Icons.search_outlined),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
const NavigationRailDestination(
|
||||
label: Text('Updates'),
|
||||
icon: Icon(Icons.download_for_offline_outlined),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
const NavigationRailDestination(
|
||||
label: Text('Profile'),
|
||||
icon: Icon(Icons.person_outline),
|
||||
),
|
||||
const NavigationRailDestination(
|
||||
label: Text('Notifications'),
|
||||
icon: Icon(Icons.notifications_outlined),
|
||||
),
|
||||
],
|
||||
),
|
||||
body,
|
||||
|
||||
@@ -1,14 +1,81 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:android_package_installer/android_package_installer.dart';
|
||||
import 'package:android_package_manager/android_package_manager.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:flutter_data/flutter_data.dart';
|
||||
import 'package:ndk/ndk.dart' as ndk;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:zapstore/models/release.dart';
|
||||
import 'package:zapstore/models/user.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
part 'file_metadata.g.dart';
|
||||
|
||||
@DataRepository([NostrAdapter],
|
||||
fromJson: 'FileMetadata.fromMap(map)', toJson: 'model.toMap()')
|
||||
final packageManager = AndroidPackageManager();
|
||||
|
||||
@DataRepository([NostrAdapter, FileMetadataAdapter],
|
||||
fromJson: 'FileMetadata.fromMapFactory(map)', toJson: 'model.toMap()')
|
||||
class FileMetadata extends BaseEvent<FileMetadata> with ndk.FileMetadata {
|
||||
FileMetadata.fromMap(super.map) : super.fromMap();
|
||||
|
||||
factory FileMetadata.fromMapFactory(Map<String, dynamic> map) {
|
||||
final m = FileMetadata.fromMap(map);
|
||||
m.author = BelongsTo<User>.fromJson(map['author']);
|
||||
return m;
|
||||
}
|
||||
|
||||
late final BelongsTo<User> author;
|
||||
late final BelongsTo<Release> release = BelongsTo();
|
||||
|
||||
Future<void> install() async {
|
||||
// throw Exception('something nasty');
|
||||
var status = await Permission.storage.status;
|
||||
if (!status.isGranted) {
|
||||
// If not we will ask for permission first
|
||||
await Permission.storage.request();
|
||||
}
|
||||
|
||||
// download file
|
||||
|
||||
final uri = Uri.parse(url);
|
||||
final response = await http.get(uri);
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final file = File(path.join(dir.path, path.basename(url)));
|
||||
await file.writeAsBytes(response.bodyBytes);
|
||||
|
||||
// check hash
|
||||
|
||||
final digest = sha256.convert(response.bodyBytes);
|
||||
final shaOk = digest.toString() == hash;
|
||||
|
||||
if (!shaOk) {
|
||||
throw Exception('sha not ok');
|
||||
}
|
||||
|
||||
int? code =
|
||||
await AndroidPackageInstaller.installApk(apkFilePath: file.path);
|
||||
if (code != 0) {
|
||||
throw Exception(
|
||||
'installation error: ${code != null ? PackageInstallerStatus.byCode(code) : ''}');
|
||||
}
|
||||
// final apps = await packageManager.getInstalledPackages();
|
||||
// apps?.firstWhere((e) => e.packageName == release.value!.identifier);
|
||||
}
|
||||
}
|
||||
|
||||
mixin FileMetadataAdapter on RemoteAdapter<FileMetadata> {
|
||||
@override
|
||||
Future<DeserializedData<FileMetadata>> deserialize(Object? data) async {
|
||||
final list = data is Iterable ? data : [data as Map];
|
||||
for (final e in list) {
|
||||
final map = e as Map<String, dynamic>;
|
||||
map['author'] = map['pubkey'];
|
||||
}
|
||||
final result = await super.deserialize(data);
|
||||
print(result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,12 @@ part of 'file_metadata.dart';
|
||||
|
||||
mixin $FileMetadataLocalAdapter on LocalAdapter<FileMetadata> {
|
||||
static final Map<String, RelationshipMeta> _kFileMetadataRelationshipMetas = {
|
||||
'author': RelationshipMeta<User>(
|
||||
name: 'author',
|
||||
type: 'users',
|
||||
kind: 'BelongsTo',
|
||||
instance: (_) => (_ as FileMetadata).author,
|
||||
),
|
||||
'release': RelationshipMeta<Release>(
|
||||
name: 'release',
|
||||
inverseName: 'artifacts',
|
||||
@@ -26,7 +32,7 @@ mixin $FileMetadataLocalAdapter on LocalAdapter<FileMetadata> {
|
||||
@override
|
||||
FileMetadata deserialize(map) {
|
||||
map = transformDeserialize(map);
|
||||
return FileMetadata.fromMap(map);
|
||||
return FileMetadata.fromMapFactory(map);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -43,7 +49,7 @@ class $FileMetadataHiveLocalAdapter = HiveLocalAdapter<FileMetadata>
|
||||
with $FileMetadataLocalAdapter;
|
||||
|
||||
class $FileMetadataRemoteAdapter = RemoteAdapter<FileMetadata>
|
||||
with NostrAdapter<FileMetadata>;
|
||||
with NostrAdapter<FileMetadata>, FileMetadataAdapter;
|
||||
|
||||
final internalFileMetadataRemoteAdapterProvider =
|
||||
Provider<RemoteAdapter<FileMetadata>>((ref) => $FileMetadataRemoteAdapter(
|
||||
@@ -56,10 +62,19 @@ final fileMetadataRepositoryProvider =
|
||||
extension FileMetadataDataRepositoryX on Repository<FileMetadata> {
|
||||
NostrAdapter<FileMetadata> get nostrAdapter =>
|
||||
remoteAdapter as NostrAdapter<FileMetadata>;
|
||||
FileMetadataAdapter get fileMetadataAdapter =>
|
||||
remoteAdapter as FileMetadataAdapter;
|
||||
}
|
||||
|
||||
extension FileMetadataRelationshipGraphNodeX
|
||||
on RelationshipGraphNode<FileMetadata> {
|
||||
RelationshipGraphNode<User> get author {
|
||||
final meta = $FileMetadataLocalAdapter
|
||||
._kFileMetadataRelationshipMetas['author'] as RelationshipMeta<User>;
|
||||
return meta.clone(
|
||||
parent: this is RelationshipMeta ? this as RelationshipMeta : null);
|
||||
}
|
||||
|
||||
RelationshipGraphNode<Release> get release {
|
||||
final meta =
|
||||
$FileMetadataLocalAdapter._kFileMetadataRelationshipMetas['release']
|
||||
|
||||
+2
-20
@@ -10,6 +10,8 @@ part 'release.g.dart';
|
||||
class Release extends BaseEvent<Release> with ndk.Release {
|
||||
Release.fromMap(super.map) : super.fromMap();
|
||||
|
||||
String get identifier => tagMap['i']!.first;
|
||||
|
||||
factory Release.fromMapFactory(Map<String, dynamic> map) {
|
||||
final m = Release.fromMap(map);
|
||||
m.artifacts = HasMany<FileMetadata>.fromJson(map['artifacts']);
|
||||
@@ -35,23 +37,3 @@ mixin ReleaseAdapter on RemoteAdapter<Release> {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// main() async {
|
||||
// final container = ProviderContainer();
|
||||
// await container.read(repositoryInitializerProvider.future);
|
||||
// // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
|
||||
// final fm = container
|
||||
// .read(releasesRepositoryProvider)
|
||||
// .remoteAdapter
|
||||
// .localAdapter
|
||||
// .deserialize({
|
||||
// 'id': 's938j89j4f',
|
||||
// 'content': 'some shit',
|
||||
// 'pubkey': '78hf47hf',
|
||||
// 'kind': 1063,
|
||||
// 'tags': [
|
||||
// ['m', 'application/pwa+zip']
|
||||
// ]
|
||||
// });
|
||||
// print(fm.bs);
|
||||
// }
|
||||
|
||||
+95
-73
@@ -1,4 +1,4 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_data/flutter_data.dart';
|
||||
import 'package:ndk/ndk.dart' as ndk;
|
||||
|
||||
@@ -7,14 +7,14 @@ part 'user.g.dart';
|
||||
abstract class BaseEvent<T extends BaseEvent<T>> = ndk.Event
|
||||
with DataModelMixin<T>;
|
||||
|
||||
@DataRepository([NostrAdapter],
|
||||
@DataRepository([NostrAdapter, UserAdapter],
|
||||
fromJson: 'User.fromMapFactory(map)', toJson: 'model.toMap()')
|
||||
class User extends BaseEvent<User> with ndk.User {
|
||||
class User extends BaseEvent<User> with ndk.User, EquatableMixin {
|
||||
User.fromMap(super.map) : super.fromMap();
|
||||
|
||||
factory User.fromMapFactory(Map<String, dynamic> map) {
|
||||
final user = User.fromMap(map);
|
||||
user.following = HasMany<User>.fromJson({});
|
||||
user.following = HasMany<User>.fromJson(map['following']);
|
||||
return user;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,82 @@ class User extends BaseEvent<User> with ndk.User {
|
||||
late final HasMany<User> following;
|
||||
@DataRelationship(inverse: 'following')
|
||||
late final HasMany<User> followers = HasMany();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id];
|
||||
}
|
||||
|
||||
mixin UserAdapter on NostrAdapter<User> {
|
||||
@override
|
||||
Future<DeserializedData<User>> deserialize(Object? data) async {
|
||||
final users = <User>[];
|
||||
final list = data is Iterable ? data : [data as Map];
|
||||
for (final e in list) {
|
||||
final map = e as Map<String, dynamic>;
|
||||
if (map['kind'] == 3) {
|
||||
for (final [_, id, ..._] in map['tags'] as Iterable) {
|
||||
users.add(User.fromMapFactory({
|
||||
'id': id,
|
||||
'content': '',
|
||||
'pubkey': id,
|
||||
'created_at': DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
'kind': 0,
|
||||
'tags': [],
|
||||
'following': {'_': null},
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final e in list) {
|
||||
final map = e as Map<String, dynamic>;
|
||||
if (map['kind'] == 0) {
|
||||
map['id'] = map['pubkey'];
|
||||
// TODO workaround - should not assume empty is null
|
||||
if (users.isNotEmpty) {
|
||||
map['following'] = users.map((e) => e.id).toList();
|
||||
}
|
||||
final data0 = await super.deserialize(map);
|
||||
final user = data0.model;
|
||||
if (user != null) {
|
||||
users.add(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return DeserializedData(users);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<User?> findOne(Object id,
|
||||
{bool? remote,
|
||||
bool? background,
|
||||
Map<String, dynamic>? params,
|
||||
Map<String, String>? headers,
|
||||
OnSuccessOne<User>? onSuccess,
|
||||
OnErrorOne<User>? onError,
|
||||
DataRequestLabel? label}) async {
|
||||
if (remote == false) {
|
||||
final key = graph.getKeyForId(internalType, id.toString());
|
||||
return localAdapter.findOne(key!);
|
||||
}
|
||||
|
||||
final req = ndk.Req(
|
||||
authors: {id.toString()},
|
||||
kinds: {kind, ...?params?.remove('kinds')},
|
||||
tags: params ?? {},
|
||||
);
|
||||
|
||||
print('[findOne] req $req');
|
||||
final result = await notifier.query(req);
|
||||
final data = await deserialize(result);
|
||||
|
||||
for (final m in data.models) {
|
||||
print('saving model of kind ${m.kind}: ${m.content}');
|
||||
m.init().saveLocal();
|
||||
}
|
||||
return data.models.firstWhere((e) => e.id == id);
|
||||
}
|
||||
}
|
||||
|
||||
mixin NostrAdapter<T extends BaseEvent<T>> on RemoteAdapter<T> {
|
||||
@@ -51,9 +127,11 @@ mixin NostrAdapter<T extends BaseEvent<T>> on RemoteAdapter<T> {
|
||||
// super.dispose();
|
||||
// }
|
||||
|
||||
Map<String, int> typeKind = {
|
||||
'fileMetadata': 1063,
|
||||
'releases': 30063,
|
||||
Map<int, String> kindType = {
|
||||
0: 'users',
|
||||
3: 'users',
|
||||
1063: 'fileMetadata',
|
||||
30063: 'releases',
|
||||
};
|
||||
|
||||
@override
|
||||
@@ -65,8 +143,7 @@ mixin NostrAdapter<T extends BaseEvent<T>> on RemoteAdapter<T> {
|
||||
for (final e in list) {
|
||||
final map = e as Map<String, dynamic>;
|
||||
final kind = map['kind'] as int;
|
||||
final xType =
|
||||
typeKind.entries.firstWhereOrNull((e) => e.value == kind)?.key;
|
||||
final xType = kindType[kind];
|
||||
if (xType != null) {
|
||||
if (xType == internalType) {
|
||||
final newData = await super.deserialize(map);
|
||||
@@ -80,6 +157,9 @@ mixin NostrAdapter<T extends BaseEvent<T>> on RemoteAdapter<T> {
|
||||
return DeserializedData<T>(models, included: included);
|
||||
}
|
||||
|
||||
int get kind =>
|
||||
kindType.entries.firstWhere((e) => e.value == internalType).key;
|
||||
|
||||
@override
|
||||
Future<List<T>> findAll(
|
||||
{bool? remote,
|
||||
@@ -90,81 +170,23 @@ mixin NostrAdapter<T extends BaseEvent<T>> on RemoteAdapter<T> {
|
||||
OnSuccessAll<T>? onSuccess,
|
||||
OnErrorAll<T>? onError,
|
||||
DataRequestLabel? label}) async {
|
||||
final req = ndk.Req(
|
||||
kinds: {typeKind[internalType] as int, ...?params?.remove('kinds')},
|
||||
tags: params ?? {},
|
||||
);
|
||||
if (remote == false) {
|
||||
return localAdapter.findAll();
|
||||
}
|
||||
|
||||
final req = ndk.Req(
|
||||
kinds: {kind, ...?params?.remove('kinds')},
|
||||
tags: params ?? {},
|
||||
);
|
||||
|
||||
print('req $req');
|
||||
final result = await notifier.query(req);
|
||||
final data = await deserialize(result);
|
||||
print('req result ${data.models.length}');
|
||||
for (final m in data.models) {
|
||||
print('saving model of kind ${m.kind}: ${m.content}');
|
||||
m.init().saveLocal();
|
||||
}
|
||||
return data.models;
|
||||
}
|
||||
}
|
||||
|
||||
// final profileProvider =
|
||||
// FutureProvider.family<User, (String, bool)>((ref, record) async {
|
||||
// final (pubkey, loadContacts) = record;
|
||||
// final completer = Completer<User>();
|
||||
// final notifier = ref.read(frameProvider.notifier);
|
||||
// print('profile provider sending req');
|
||||
// notifier.send(jsonEncode([
|
||||
// "REQ",
|
||||
// pubkey,
|
||||
// {
|
||||
// 'authors': [pubkey],
|
||||
// 'kinds': [0, 3],
|
||||
// }
|
||||
// ]));
|
||||
|
||||
// late Function _sub;
|
||||
|
||||
// User? u;
|
||||
// List<String>? contacts;
|
||||
// _sub = ref.watch(frameProvider.notifier).addListener((frame) async {
|
||||
// print('listener: ${frame.event}');
|
||||
// final event = frame.event;
|
||||
|
||||
// if (event is Metadata) {
|
||||
// u = (await ref.users.findOne(event.pubkey, remote: false)) ??
|
||||
// User(id: event.pubkey);
|
||||
// final map = jsonDecode(event.content);
|
||||
// u!.name = map['displayName'] ?? map['display_name'] ?? map['name'];
|
||||
// u!.nip05 = map['nip05'];
|
||||
// }
|
||||
|
||||
// if (loadContacts == false && u != null && completer.isCompleted == false) {
|
||||
// completer.complete(u);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// if (event is ContactList) {
|
||||
// contacts = [...?contacts, ...event.tagMap['p']!];
|
||||
// }
|
||||
|
||||
// if (u != null && contacts != null) {
|
||||
// if (completer.isCompleted == false) {
|
||||
// print('completing with $u');
|
||||
// contacts!.forEach((c) {
|
||||
// final uc = User(id: c);
|
||||
// u!.following.add(uc);
|
||||
// uc.saveLocal();
|
||||
// });
|
||||
// u!.saveLocal();
|
||||
// completer.complete(u);
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// ref.onDispose(() {
|
||||
// print('disposing');
|
||||
// _sub();
|
||||
// });
|
||||
// return completer.future;
|
||||
// });
|
||||
@@ -48,7 +48,8 @@ final _usersFinders = <String, dynamic>{};
|
||||
// ignore: must_be_immutable
|
||||
class $UserHiveLocalAdapter = HiveLocalAdapter<User> with $UserLocalAdapter;
|
||||
|
||||
class $UserRemoteAdapter = RemoteAdapter<User> with NostrAdapter<User>;
|
||||
class $UserRemoteAdapter = RemoteAdapter<User>
|
||||
with NostrAdapter<User>, UserAdapter;
|
||||
|
||||
final internalUsersRemoteAdapterProvider = Provider<RemoteAdapter<User>>(
|
||||
(ref) => $UserRemoteAdapter(
|
||||
@@ -59,6 +60,7 @@ final usersRepositoryProvider =
|
||||
|
||||
extension UserDataRepositoryX on Repository<User> {
|
||||
NostrAdapter<User> get nostrAdapter => remoteAdapter as NostrAdapter<User>;
|
||||
UserAdapter get userAdapter => remoteAdapter as UserAdapter;
|
||||
}
|
||||
|
||||
extension UserRelationshipGraphNodeX on RelationshipGraphNode<User> {
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import 'package:async_button_builder/async_button_builder.dart';
|
||||
import 'package:expansion_tile_card/expansion_tile_card.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_styled_toast/flutter_styled_toast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:ndk/ndk.dart' as ndk;
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:zapstore/main.data.dart';
|
||||
import 'package:zapstore/models/file_metadata.dart';
|
||||
import 'package:zapstore/models/release.dart';
|
||||
import 'package:zapstore/models/user.dart';
|
||||
import 'package:zapstore/screens/profile_screen.dart';
|
||||
|
||||
class AppDetailScreen extends HookConsumerWidget {
|
||||
final Release release;
|
||||
@@ -11,15 +21,195 @@ class AppDetailScreen extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Column(
|
||||
final artifacts = release.artifacts.toList();
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: release.artifacts.length,
|
||||
itemBuilder: (context, index) {
|
||||
final event = artifacts[index];
|
||||
return CardWidget(fileMetadata: event);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CardWidget extends HookConsumerWidget {
|
||||
final FileMetadata fileMetadata;
|
||||
|
||||
const CardWidget({super.key, required this.fileMetadata});
|
||||
|
||||
User? get author => fileMetadata.author.value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final currentUser = ref.read(loggedInUser);
|
||||
final fetchUser = useMemoized(() => ref.users.findOne(author!.id!));
|
||||
useFuture(fetchUser);
|
||||
|
||||
final isWebOfTrust = [...?currentUser?.following.toList()]
|
||||
.contains(fileMetadata.author.value);
|
||||
|
||||
return ExpansionTileCard(
|
||||
leading: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
child: Image.network(
|
||||
fileMetadata.release.value!.image,
|
||||
width: 80,
|
||||
height: 80,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, err, _) => Text(err.toString()),
|
||||
),
|
||||
),
|
||||
onExpansionChanged: (_) {
|
||||
ref.users.findOne(author!.id!);
|
||||
},
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
fileMetadata.content,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 26),
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundImage: NetworkImage(author?.profilePicture ?? ''),
|
||||
maxRadius: 10,
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Text(
|
||||
'${author?.name ?? author?.id}',
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
children: [
|
||||
for (final artifact in release.artifacts.toList())
|
||||
Expanded(
|
||||
child: Card(
|
||||
child: Text(
|
||||
'${artifact.kind.toString()} / ${artifact.release.value?.content}'),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (isWebOfTrust)
|
||||
Text(
|
||||
'You are following this author!',
|
||||
style: TextStyle(color: Colors.green),
|
||||
),
|
||||
if (!isWebOfTrust)
|
||||
Row(
|
||||
children: [
|
||||
Text('Author is not in your web of trust',
|
||||
style: TextStyle(color: Colors.red)),
|
||||
GestureDetector(
|
||||
onTap: () => launchUrlString(
|
||||
'https://primal.net/p/${author?.id.toString().npub}',
|
||||
),
|
||||
child: Text(
|
||||
'see profile',
|
||||
style: TextStyle(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
AsyncButtonBuilder(
|
||||
loadingWidget: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: SizedBox(
|
||||
height: 16.0,
|
||||
width: 16.0,
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
successWidget: const Padding(
|
||||
padding: EdgeInsets.all(4.0),
|
||||
child: Icon(
|
||||
Icons.check,
|
||||
color: Colors.purpleAccent,
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
try {
|
||||
await fileMetadata.install();
|
||||
showToastWidget(
|
||||
Text(
|
||||
'Success!',
|
||||
style: TextStyle(fontSize: 20),
|
||||
),
|
||||
// ignore: use_build_context_synchronously
|
||||
context: context,
|
||||
position: StyledToastPosition.center,
|
||||
animation: StyledToastAnimation.scale,
|
||||
reverseAnimation: StyledToastAnimation.fade,
|
||||
duration: Duration(seconds: 4),
|
||||
animDuration: Duration(seconds: 1),
|
||||
curve: Curves.elasticOut,
|
||||
reverseCurve: Curves.linear);
|
||||
} on Exception catch (e) {
|
||||
showToastWidget(
|
||||
Text(
|
||||
'Error! $e',
|
||||
style: TextStyle(fontSize: 20),
|
||||
),
|
||||
// ignore: use_build_context_synchronously
|
||||
context: context,
|
||||
position: StyledToastPosition.center,
|
||||
animation: StyledToastAnimation.scale,
|
||||
reverseAnimation: StyledToastAnimation.fade,
|
||||
duration: Duration(seconds: 4),
|
||||
animDuration: Duration(seconds: 1),
|
||||
curve: Curves.elasticOut,
|
||||
reverseCurve: Curves.linear);
|
||||
}
|
||||
},
|
||||
loadingSwitchInCurve: Curves.bounceInOut,
|
||||
loadingTransitionBuilder: (child, animation) {
|
||||
return SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0, 1.0),
|
||||
end: const Offset(0, 0),
|
||||
).animate(animation),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
builder: (context, child, callback, state) {
|
||||
return Material(
|
||||
color: state.maybeWhen(
|
||||
success: () => Colors.purple[100],
|
||||
orElse: () => Colors.black12,
|
||||
),
|
||||
// This prevents the loading indicator showing below the
|
||||
// button
|
||||
clipBehavior: Clip.hardEdge,
|
||||
shape: const StadiumBorder(),
|
||||
child: InkWell(
|
||||
onTap: callback,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: const Icon(Icons.download),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,55 +2,62 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:ndk/ndk.dart' as ndk;
|
||||
import 'package:zapstore/main.data.dart';
|
||||
import 'package:zapstore/models/user.dart';
|
||||
|
||||
final loggedInUser = StateProvider<User?>((ref) => null);
|
||||
final downloadsPath = StateProvider<String>((ref) => '/tmp');
|
||||
|
||||
class ProfileScreen extends HookConsumerWidget {
|
||||
const ProfileScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final u2 = ref.watch(loggedInUser);
|
||||
final controller = useTextEditingController();
|
||||
final controller2 = useTextEditingController(text: ref.read(downloadsPath));
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
children: [
|
||||
TextField(controller: controller),
|
||||
if (u2 == null) TextField(controller: controller),
|
||||
SizedBox(height: 10),
|
||||
ElevatedButton(
|
||||
if (u2 == null)
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
final pubkey = controller.text.hexKey;
|
||||
ref.read(loggedInUser.notifier).state =
|
||||
await ref.read(usersRepositoryProvider).findOne(pubkey);
|
||||
await ref.users.findOne(pubkey, params: {
|
||||
'kinds': {3}
|
||||
});
|
||||
},
|
||||
child: Text('Log in with npub')),
|
||||
child: Text('Log in with npub'),
|
||||
),
|
||||
if (u2 != null)
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Text('Logged in as ${u2.name}'),
|
||||
Text('(following: ${u2.following.length})'),
|
||||
Card(
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
leading: CircleAvatar(
|
||||
backgroundImage: NetworkImage(u2.profilePicture ?? ''),
|
||||
),
|
||||
title: Text(
|
||||
u2.name ?? u2.id?.toString() ?? '',
|
||||
),
|
||||
subtitle: Text('following ${u2.following.length}'),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
ref.read(loggedInUser.notifier).state = null;
|
||||
controller.clear();
|
||||
},
|
||||
child: Text('Log out'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
// ListView.builder(
|
||||
// shrinkWrap: true,
|
||||
// itemCount: contacts.length,
|
||||
// itemBuilder: (context, i) {
|
||||
// return Text(' - ${contacts[i].id}');
|
||||
// }),
|
||||
TextField(controller: controller2),
|
||||
SizedBox(height: 10),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
ref.read(downloadsPath.notifier).state = controller2.text;
|
||||
},
|
||||
child: Text('Save downloads path'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
+24
-194
@@ -1,115 +1,63 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:android_package_installer/android_package_installer.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:expansion_tile_card/expansion_tile_card.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_data/flutter_data.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:zapstore/models/file_metadata.dart';
|
||||
import 'package:zapstore/models/user.dart';
|
||||
import 'package:zapstore/screens/profile_screen.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:zapstore/screens/app_detail_screen.dart';
|
||||
|
||||
import '../main.data.dart';
|
||||
|
||||
const kAndroidMimeType = 'application/vnd.android.package-archive';
|
||||
|
||||
class SearchScreen extends HookConsumerWidget {
|
||||
SearchScreen({super.key});
|
||||
final r = Random();
|
||||
const SearchScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final searchState = useState<String?>(null);
|
||||
final labelState = useState<DataRequestLabel?>(null);
|
||||
|
||||
final value = ref.releases.watchAll(remote: false);
|
||||
final value = ref.fileMetadata.watchAll(remote: false);
|
||||
final artifacts = value.model
|
||||
.where((a) => a.release.isPresent && a.mimeType == kAndroidMimeType)
|
||||
.where((a) => searchState.value != null
|
||||
? a.release.value!.title
|
||||
.toLowerCase()
|
||||
.contains(searchState.value!.toLowerCase())
|
||||
: true)
|
||||
.toList();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
padding: const EdgeInsets.all(18.0),
|
||||
child: Column(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
var status = await Permission.storage.status;
|
||||
if (!status.isGranted) {
|
||||
// If not we will ask for permission first
|
||||
await Permission.storage.request();
|
||||
}
|
||||
final apk = await rootBundle.load('tmp/gg.apk');
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
await dir.create(recursive: true);
|
||||
final buffer = apk.buffer;
|
||||
await File('${dir.path}/gg.apk').writeAsBytes(
|
||||
buffer.asUint8List(apk.offsetInBytes, apk.lengthInBytes));
|
||||
|
||||
int? code = await AndroidPackageInstaller.installApk(
|
||||
apkFilePath: '${dir.path}/gg.apk');
|
||||
if (code != null) {
|
||||
PackageInstallerStatus installationStatus =
|
||||
PackageInstallerStatus.byCode(code);
|
||||
print(installationStatus.name);
|
||||
}
|
||||
},
|
||||
child: Text('install')),
|
||||
SearchBar(
|
||||
elevation: MaterialStatePropertyAll(2.2),
|
||||
elevation: const MaterialStatePropertyAll(2.2),
|
||||
onSubmitted: (query) async {
|
||||
searchState.value = query;
|
||||
labelState.value = DataRequestLabel('query',
|
||||
type: 'releases', requestId: 'sub-${r.nextInt(9999999)}');
|
||||
|
||||
await ref.releases.findAll();
|
||||
await ref.releases.findAll(params: {
|
||||
'since': 1708658457,
|
||||
});
|
||||
await ref.fileMetadata.findAll(
|
||||
params: {
|
||||
// 'kinds': {1063},
|
||||
// 'limit': 20,
|
||||
// 'search': query,
|
||||
'#m': [
|
||||
// 'application/pwa+zip',
|
||||
'application/vnd.android.package-archive'
|
||||
],
|
||||
// 'since': DateTime.now()
|
||||
// .subtract(Duration(days: 1))
|
||||
// .millisecondsSinceEpoch ~/
|
||||
// 1000,
|
||||
'#m': [kAndroidMimeType],
|
||||
'since': 1708658457,
|
||||
},
|
||||
label: labelState.value,
|
||||
);
|
||||
|
||||
// final notifier = ref.read(frameProvider.notifier);
|
||||
// notifier.send(jsonEncode([
|
||||
// "REQ",
|
||||
// subscriptionIdState.value!,
|
||||
// ]));
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
child: Text('Total results: ${value.model.length}'),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: value.model.length,
|
||||
itemCount: artifacts.length,
|
||||
itemBuilder: (context, index) {
|
||||
final event = value.model[index];
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
context.push('/search/details', extra: event);
|
||||
},
|
||||
child: Card(
|
||||
child: Text(
|
||||
'${event.content} / ${event.id} / ${event.kind} / ${event.artifacts.keys} - ${event.runtimeType}'),
|
||||
),
|
||||
);
|
||||
// return CardWidget(note: event as FileMetadata);
|
||||
final event = artifacts[index];
|
||||
return CardWidget(fileMetadata: event);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -118,121 +66,3 @@ class SearchScreen extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CardWidget extends HookConsumerWidget {
|
||||
final FileMetadata fm;
|
||||
|
||||
const CardWidget({super.key, required this.fm});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final authorUser = useState<User?>(null);
|
||||
final currentUser = ref.read(loggedInUser);
|
||||
|
||||
final isWebOfTrust =
|
||||
currentUser?.following.contains(authorUser.value) ?? false;
|
||||
final isSha256Ok = useState<bool?>(null);
|
||||
|
||||
return ExpansionTileCard(
|
||||
leading: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
child: Image.network(
|
||||
fm.tagMap['thumb']!.first,
|
||||
width: 80,
|
||||
height: 80,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, err, _) => Text(err.toString()),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
fm.content,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Type: ${fm.mimeType} (${fm.id})'),
|
||||
],
|
||||
),
|
||||
onExpansionChanged: (isOpen) async {
|
||||
if (isOpen && currentUser != null) {
|
||||
// final existingUser =
|
||||
// await ref.users.findOne(note.pubkey, remote: false);
|
||||
// if (existingUser?.name != null) {
|
||||
// print('fd has ${note.pubkey}! (${existingUser!.name}) ');
|
||||
// authorUser.value = existingUser;
|
||||
// } else {
|
||||
// print('fetch in network');
|
||||
// authorUser.value =
|
||||
// await ref.read(profileProvider((note.pubkey, false)).future);
|
||||
// }
|
||||
}
|
||||
},
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// if (authorUser.value != null)
|
||||
// Text(
|
||||
// 'Author: ${authorUser.value!.name} (${authorUser.value!.nip05})'),
|
||||
if (isWebOfTrust)
|
||||
Text(
|
||||
'Author is in your web of trust',
|
||||
style: TextStyle(color: Colors.green),
|
||||
),
|
||||
if (!isWebOfTrust)
|
||||
Text('Author is not in your web of trust',
|
||||
style: TextStyle(color: Colors.red)),
|
||||
if (isSha256Ok.value == true)
|
||||
Text(
|
||||
'Installed! Hash and signature matched',
|
||||
style: TextStyle(color: Colors.green),
|
||||
),
|
||||
if (isSha256Ok.value == false)
|
||||
Text(
|
||||
'Removed! Hash or signature did not match',
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
if (isSha256Ok.value != false)
|
||||
ElevatedButton.icon(
|
||||
label: Text('Install'),
|
||||
icon: Icon(Icons.arrow_downward),
|
||||
onPressed: () async {
|
||||
final uri = Uri.parse(fm.tagMap['url']!.first);
|
||||
final response = await http.get(uri);
|
||||
final dir = ref.read(downloadsPath);
|
||||
|
||||
File file =
|
||||
File(path.join(dir, fm.tagMap['name']!.first));
|
||||
await file.writeAsBytes(response.bodyBytes);
|
||||
|
||||
final digest = sha256.convert(response.bodyBytes);
|
||||
isSha256Ok.value =
|
||||
digest.toString() == fm.tagMap['x']!.first;
|
||||
|
||||
if (isSha256Ok.value == false) {
|
||||
print('actual digest $digest');
|
||||
await file.delete();
|
||||
} else {
|
||||
print('digest ok $digest');
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include <handy_window/handy_window_plugin.h>
|
||||
#include <screen_retriever/screen_retriever_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
@@ -17,6 +18,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) screen_retriever_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin");
|
||||
screen_retriever_plugin_register_with_registrar(screen_retriever_registrar);
|
||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) window_manager_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
|
||||
window_manager_plugin_register_with_registrar(window_manager_registrar);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
handy_window
|
||||
screen_retriever
|
||||
url_launcher_linux
|
||||
window_manager
|
||||
)
|
||||
|
||||
|
||||
@@ -7,10 +7,12 @@ import Foundation
|
||||
|
||||
import path_provider_foundation
|
||||
import screen_retriever
|
||||
import url_launcher_macos
|
||||
import window_manager
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
|
||||
}
|
||||
|
||||
+111
-2
@@ -26,6 +26,14 @@ packages:
|
||||
url: "https://github.com/ImranR98/android_package_installer"
|
||||
source: git
|
||||
version: "0.0.1"
|
||||
android_package_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: android_package_manager
|
||||
sha256: "2de859fae7226a7de1c1ff9a2308f1967599408800330501a1ce97927c051153"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.1"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -50,6 +58,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.11.0"
|
||||
async_button_builder:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: async_button_builder
|
||||
sha256: "51221ec05e48b14c2c346071e2fb968207f6120cc9fe5d727b251987bbafe869"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0+1"
|
||||
bech32:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -219,7 +235,7 @@ packages:
|
||||
source: hosted
|
||||
version: "2.3.4"
|
||||
equatable:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: equatable
|
||||
sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2
|
||||
@@ -303,6 +319,11 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
flutter_localizations:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_riverpod:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -311,6 +332,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.10"
|
||||
flutter_styled_toast:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_styled_toast
|
||||
sha256: e667f13a665820eb0fa8506547e47eacbcddf1948d6d3036cfd3b089bd4b0516
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@@ -321,6 +350,14 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
freezed_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: freezed_annotation
|
||||
sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -425,6 +462,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.3+2"
|
||||
intl:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: intl
|
||||
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.18.1"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -861,6 +906,70 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
url_launcher:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.5"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.4"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_platform_interface
|
||||
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
url_launcher_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "3692a459204a33e04bc94f5fb91158faf4f2c8903281ddd82915adecdb1a901d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -951,4 +1060,4 @@ packages:
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.3.0 <4.0.0"
|
||||
flutter: ">=3.16.0"
|
||||
flutter: ">=3.19.0"
|
||||
|
||||
@@ -40,6 +40,11 @@ dependencies:
|
||||
permission_handler: ^11.3.0
|
||||
path: ^1.9.0
|
||||
http: ^1.2.1
|
||||
async_button_builder: ^3.0.0+1
|
||||
android_package_manager: ^0.7.1
|
||||
flutter_styled_toast: ^2.2.1
|
||||
equatable: ^2.0.5
|
||||
url_launcher: ^6.2.5
|
||||
|
||||
dependency_overrides:
|
||||
android_package_installer:
|
||||
|
||||
Reference in New Issue
Block a user