mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 20:48:24 +00:00
Routing, local storage, relationships
This commit is contained in:
@@ -41,3 +41,5 @@ app.*.map.json
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
/flutter_data
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "db7ef5bf9f59442b0e200a90587e8fa5e0c6336a"
|
||||
revision: "41456452f29d64e8deb623a3c927524bcf9f111b"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
@@ -13,11 +13,26 @@ project_type: app
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a
|
||||
base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a
|
||||
create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
- platform: android
|
||||
create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
- platform: ios
|
||||
create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
- platform: linux
|
||||
create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a
|
||||
base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a
|
||||
create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
- platform: macos
|
||||
create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
- platform: web
|
||||
create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
- platform: windows
|
||||
create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
|
||||
# User provided section
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"dart.flutterSdkPath": "~/fvm/versions/3.16.0",
|
||||
"dart.flutterSdkPath": "~/fvm/default",
|
||||
// Remove .fvm files from search
|
||||
"search.exclude": {
|
||||
"**/.fvm": true
|
||||
|
||||
@@ -45,7 +45,7 @@ android {
|
||||
applicationId "com.example.zapstore"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
||||
minSdkVersion flutter.minSdkVersion
|
||||
minSdkVersion 21
|
||||
targetSdkVersion flutter.targetSdkVersion
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.7.10'
|
||||
ext.kotlin_version = '1.9.22'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
org.gradle.jvmargs=-Xmx4G
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
android.enableJetifier=true
|
||||
+235
-117
@@ -1,175 +1,293 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:bitsdojo_window/bitsdojo_window.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:ndk/ndk.dart';
|
||||
import 'package:ndk/ndk.dart' as ndk;
|
||||
import 'package:zapstore/main.data.dart';
|
||||
import 'package:zapstore/models/release.dart';
|
||||
import 'package:zapstore/screens/app_detail_screen.dart';
|
||||
import 'package:zapstore/screens/profile_screen.dart';
|
||||
import 'package:zapstore/screens/search_screen.dart';
|
||||
|
||||
void main() {
|
||||
appWindow.size = const Size(400, 700);
|
||||
runApp(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
configureRepositoryLocalStorage(
|
||||
clear: LocalStorageClearStrategy.always),
|
||||
],
|
||||
child: const ZapStoreApp(),
|
||||
child: const ZapstoreApp(),
|
||||
),
|
||||
);
|
||||
appWindow.show();
|
||||
doWhenWindowReady(() {
|
||||
final win = appWindow;
|
||||
const initialSize = Size(400, 700);
|
||||
const initialSize = Size(200, 350);
|
||||
win.minSize = initialSize;
|
||||
win.size = initialSize;
|
||||
win.alignment = Alignment.center;
|
||||
// win.title = "Custom window with Flutter";
|
||||
win.show();
|
||||
});
|
||||
}
|
||||
|
||||
const borderColor = Color(0xFF805306);
|
||||
|
||||
final newInitializer = FutureProvider<void>((ref) async {
|
||||
await ref.read(repositoryInitializerProvider.future);
|
||||
await ref
|
||||
.read(frameProvider.notifier)
|
||||
.initialize({'wss://relay.damus.io', 'wss://relay.nostr.band'});
|
||||
});
|
||||
|
||||
class ZapStoreApp extends HookConsumerWidget {
|
||||
const ZapStoreApp({super.key});
|
||||
class ZapstoreApp extends StatelessWidget {
|
||||
const ZapstoreApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final initializer = ref.watch(newInitializer);
|
||||
return MaterialApp(
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp.router(
|
||||
routerConfig: goRouter,
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData.dark(),
|
||||
home: Scaffold(
|
||||
body: Column(
|
||||
children: [
|
||||
WindowTitleBarBox(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: MoveWindow()),
|
||||
// const WindowButtons()
|
||||
],
|
||||
theme: ThemeData.dark(useMaterial3: true),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// private navigators
|
||||
final _rootNavigatorKey = GlobalKey<NavigatorState>();
|
||||
final _searchNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'search');
|
||||
final _updatesNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'updates');
|
||||
final _profileNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'profile');
|
||||
|
||||
final goRouter = GoRouter(
|
||||
initialLocation: '/search',
|
||||
// * Passing a navigatorKey causes an issue on hot reload:
|
||||
// * https://github.com/flutter/flutter/issues/113757#issuecomment-1518421380
|
||||
// * However it's still necessary otherwise the navigator pops back to
|
||||
// * root on hot reload
|
||||
navigatorKey: _rootNavigatorKey,
|
||||
// debugLogDiagnostics: true,
|
||||
routes: [
|
||||
// Stateful navigation based on:
|
||||
// https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_shell_route.dart
|
||||
StatefulShellRoute.indexedStack(
|
||||
builder: (context, state, navigationShell) {
|
||||
return ScaffoldWithNestedNavigation(navigationShell: navigationShell);
|
||||
},
|
||||
branches: [
|
||||
StatefulShellBranch(
|
||||
navigatorKey: _searchNavigatorKey,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/search',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: SearchScreen(),
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'details',
|
||||
builder: (context, state) =>
|
||||
AppDetailScreen(release: state.extra as Release),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: initializer.when(
|
||||
data: (_) => NavigationRailPage(),
|
||||
error: (err, stack) => Text('error $err'),
|
||||
loading: () => CircularProgressIndicator(),
|
||||
],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
navigatorKey: _updatesNavigatorKey,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/updates',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: Center(
|
||||
child: ElevatedButton(
|
||||
onPressed: () => context.go('/search/details', extra: 1),
|
||||
child: const Text('Go to the Details screen'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
navigatorKey: _profileNavigatorKey,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/profile',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: Center(
|
||||
child: ProfileScreen(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final newInitializer = FutureProvider<void>((ref) async {
|
||||
await ref.read(repositoryInitializerProvider.future);
|
||||
await ref.read(ndk.frameProvider.notifier).initialize('wss://relay.damus.io');
|
||||
});
|
||||
|
||||
// Stateful navigation based on:
|
||||
// https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_shell_route.dart
|
||||
class ScaffoldWithNestedNavigation extends HookConsumerWidget {
|
||||
const ScaffoldWithNestedNavigation({
|
||||
Key? key,
|
||||
required this.navigationShell,
|
||||
}) : super(
|
||||
key: key ?? const ValueKey<String>('ScaffoldWithNestedNavigation'));
|
||||
final StatefulNavigationShell navigationShell;
|
||||
|
||||
void _goBranch(int index) {
|
||||
navigationShell.goBranch(
|
||||
index,
|
||||
// A common pattern when using bottom navigation bars is to support
|
||||
// navigating to the initial location when tapping the item that is
|
||||
// already active. This example demonstrates how to support this behavior,
|
||||
// using the initialLocation parameter of goBranch.
|
||||
initialLocation: index == navigationShell.currentIndex,
|
||||
);
|
||||
}
|
||||
|
||||
@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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NavigationRailPage extends HookConsumerWidget {
|
||||
const NavigationRailPage({super.key});
|
||||
class ScaffoldWithNavigationBar extends StatelessWidget {
|
||||
const ScaffoldWithNavigationBar({
|
||||
super.key,
|
||||
required this.body,
|
||||
required this.selectedIndex,
|
||||
required this.onDestinationSelected,
|
||||
});
|
||||
final Widget body;
|
||||
final int selectedIndex;
|
||||
final ValueChanged<int> onDestinationSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final selectedIndex = useState(0);
|
||||
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
final bool isSmallScreen = width < 600;
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: body,
|
||||
bottomNavigationBar: NavigationBar(
|
||||
selectedIndex: selectedIndex,
|
||||
labelBehavior: NavigationDestinationLabelBehavior.alwaysHide,
|
||||
destinations: const [
|
||||
NavigationDestination(
|
||||
label: 'Search',
|
||||
icon: Icon(Icons.search_outlined),
|
||||
),
|
||||
NavigationDestination(
|
||||
label: 'Updates',
|
||||
icon: Icon(Icons.download_for_offline_outlined),
|
||||
),
|
||||
NavigationDestination(
|
||||
label: 'Profile',
|
||||
icon: Icon(Icons.person_outline),
|
||||
),
|
||||
NavigationDestination(
|
||||
label: 'Notifications',
|
||||
icon: Icon(Icons.notifications_outlined),
|
||||
),
|
||||
],
|
||||
onDestinationSelected: onDestinationSelected,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ScaffoldWithNavigationRail extends StatelessWidget {
|
||||
const ScaffoldWithNavigationRail({
|
||||
super.key,
|
||||
required this.body,
|
||||
required this.selectedIndex,
|
||||
required this.onDestinationSelected,
|
||||
});
|
||||
final Widget body;
|
||||
final int selectedIndex;
|
||||
final ValueChanged<int> onDestinationSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
bottomNavigationBar: isSmallScreen
|
||||
? BottomNavigationBar(
|
||||
items: _navBarItems,
|
||||
currentIndex: selectedIndex.value,
|
||||
onTap: (int index) {
|
||||
selectedIndex.value = index;
|
||||
})
|
||||
: null,
|
||||
body: Row(
|
||||
children: [
|
||||
if (!isSmallScreen)
|
||||
NavigationRail(
|
||||
selectedIndex: selectedIndex.value,
|
||||
onDestinationSelected: (int index) {
|
||||
selectedIndex.value = index;
|
||||
},
|
||||
extended: !isSmallScreen,
|
||||
destinations: _navBarItems
|
||||
.map((item) => NavigationRailDestination(
|
||||
icon: item.icon,
|
||||
selectedIcon: item.activeIcon,
|
||||
label: Text(item.label!)))
|
||||
.toList(),
|
||||
),
|
||||
// const VerticalDivider(thickness: 1, width: 1),
|
||||
switch (selectedIndex.value) {
|
||||
0 => SearchScreen(),
|
||||
1 => const Image(image: AssetImage('assets/images/logo.png')),
|
||||
2 => ProfileScreen(),
|
||||
_ => throw Error(),
|
||||
},
|
||||
NavigationRail(
|
||||
selectedIndex: selectedIndex,
|
||||
onDestinationSelected: onDestinationSelected,
|
||||
labelType: NavigationRailLabelType.all,
|
||||
destinations: [
|
||||
NavigationRailDestination(
|
||||
label: Text('Search'),
|
||||
icon: Icon(Icons.search_outlined),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
label: Text('Updates'),
|
||||
icon: Icon(Icons.download_for_offline_outlined),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
label: Text('Profile'),
|
||||
icon: Icon(Icons.person_outline),
|
||||
),
|
||||
],
|
||||
),
|
||||
body,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const _navBarItems = [
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.search_outlined),
|
||||
activeIcon: Icon(Icons.search_rounded),
|
||||
label: 'Search',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.download_for_offline_outlined),
|
||||
activeIcon: Icon(Icons.download_for_offline_rounded),
|
||||
label: 'Updates',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.person_outline_rounded),
|
||||
activeIcon: Icon(Icons.person_rounded),
|
||||
label: 'Profile',
|
||||
),
|
||||
];
|
||||
|
||||
const sidebarColor = Color(0xFFF6A00C);
|
||||
const backgroundStartColor = Color(0xFFFFD500);
|
||||
const backgroundEndColor = Color(0xFFF6A00C);
|
||||
// const borderColor = Color(0xFF805306);
|
||||
|
||||
final buttonColors = WindowButtonColors(
|
||||
iconNormal: const Color(0xFF805306),
|
||||
mouseOver: const Color(0xFFF6A00C),
|
||||
mouseDown: const Color(0xFF805306),
|
||||
iconMouseOver: const Color(0xFF805306),
|
||||
iconMouseDown: const Color(0xFFFFD500));
|
||||
// const sidebarColor = Color(0xFFF6A00C);
|
||||
// const backgroundStartColor = Color(0xFFFFD500);
|
||||
// const backgroundEndColor = Color(0xFFF6A00C);
|
||||
|
||||
final closeButtonColors = WindowButtonColors(
|
||||
mouseOver: const Color(0xFFD32F2F),
|
||||
mouseDown: const Color(0xFFB71C1C),
|
||||
iconNormal: const Color(0xFF805306),
|
||||
iconMouseOver: Colors.white);
|
||||
// final buttonColors = WindowButtonColors(
|
||||
// iconNormal: const Color(0xFF805306),
|
||||
// mouseOver: const Color(0xFFF6A00C),
|
||||
// mouseDown: const Color(0xFF805306),
|
||||
// iconMouseOver: const Color(0xFF805306),
|
||||
// iconMouseDown: const Color(0xFFFFD500));
|
||||
|
||||
class WindowButtons extends StatelessWidget {
|
||||
const WindowButtons({super.key});
|
||||
// final closeButtonColors = WindowButtonColors(
|
||||
// mouseOver: const Color(0xFFD32F2F),
|
||||
// mouseDown: const Color(0xFFB71C1C),
|
||||
// iconNormal: const Color(0xFF805306),
|
||||
// iconMouseOver: Colors.white);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
MinimizeWindowButton(colors: buttonColors),
|
||||
MaximizeWindowButton(colors: buttonColors),
|
||||
CloseWindowButton(colors: closeButtonColors),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
// class WindowButtons extends StatelessWidget {
|
||||
// const WindowButtons({super.key});
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return Row(
|
||||
// children: [
|
||||
// MinimizeWindowButton(colors: buttonColors),
|
||||
// MaximizeWindowButton(colors: buttonColors),
|
||||
// CloseWindowButton(colors: closeButtonColors),
|
||||
// ],
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
+13
-3
@@ -8,6 +8,8 @@ import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:zapstore/models/file_metadata.dart';
|
||||
import 'package:zapstore/models/release.dart';
|
||||
import 'package:zapstore/models/user.dart';
|
||||
|
||||
// ignore: prefer_function_declarations_over_variables
|
||||
@@ -29,14 +31,18 @@ ConfigureRepositoryLocalStorage configureRepositoryLocalStorage = ({FutureFn<Str
|
||||
};
|
||||
|
||||
final repositoryProviders = <String, Provider<Repository<DataModelMixin>>>{
|
||||
'users': usersRepositoryProvider
|
||||
'fileMetadata': fileMetadataRepositoryProvider,
|
||||
'releases': releasesRepositoryProvider,
|
||||
'users': usersRepositoryProvider
|
||||
};
|
||||
|
||||
final repositoryInitializerProvider =
|
||||
FutureProvider<RepositoryInitializer>((ref) async {
|
||||
DataHelpers.setInternalType<FileMetadata>('fileMetadata');
|
||||
DataHelpers.setInternalType<Release>('releases');
|
||||
DataHelpers.setInternalType<User>('users');
|
||||
final adapters = <String, RemoteAdapter>{'users': ref.watch(internalUsersRemoteAdapterProvider)};
|
||||
final remotes = <String, bool>{'users': true};
|
||||
final adapters = <String, RemoteAdapter>{'fileMetadata': ref.watch(internalFileMetadataRemoteAdapterProvider), 'releases': ref.watch(internalReleasesRemoteAdapterProvider), 'users': ref.watch(internalUsersRemoteAdapterProvider)};
|
||||
final remotes = <String, bool>{'fileMetadata': true, 'releases': true, 'users': true};
|
||||
|
||||
await ref.watch(graphNotifierProvider).initialize();
|
||||
|
||||
@@ -54,10 +60,14 @@ final repositoryInitializerProvider =
|
||||
return RepositoryInitializer();
|
||||
});
|
||||
extension RepositoryWidgetRefX on WidgetRef {
|
||||
Repository<FileMetadata> get fileMetadata => watch(fileMetadataRepositoryProvider)..remoteAdapter.internalWatch = watch;
|
||||
Repository<Release> get releases => watch(releasesRepositoryProvider)..remoteAdapter.internalWatch = watch;
|
||||
Repository<User> get users => watch(usersRepositoryProvider)..remoteAdapter.internalWatch = watch;
|
||||
}
|
||||
|
||||
extension RepositoryRefX on Ref {
|
||||
|
||||
Repository<FileMetadata> get fileMetadata => watch(fileMetadataRepositoryProvider)..remoteAdapter.internalWatch = watch as Watcher;
|
||||
Repository<Release> get releases => watch(releasesRepositoryProvider)..remoteAdapter.internalWatch = watch as Watcher;
|
||||
Repository<User> get users => watch(usersRepositoryProvider)..remoteAdapter.internalWatch = watch as Watcher;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import 'package:flutter_data/flutter_data.dart';
|
||||
import 'package:ndk/ndk.dart' as ndk;
|
||||
import 'package:zapstore/models/release.dart';
|
||||
import 'package:zapstore/models/user.dart';
|
||||
|
||||
part 'file_metadata.g.dart';
|
||||
|
||||
@DataRepository([NostrAdapter],
|
||||
fromJson: 'FileMetadata.fromMap(map)', toJson: 'model.toMap()')
|
||||
class FileMetadata extends BaseEvent<FileMetadata> with ndk.FileMetadata {
|
||||
FileMetadata.fromMap(super.map) : super.fromMap();
|
||||
|
||||
late final BelongsTo<Release> release = BelongsTo();
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'file_metadata.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RepositoryGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// ignore_for_file: non_constant_identifier_names, duplicate_ignore
|
||||
|
||||
mixin $FileMetadataLocalAdapter on LocalAdapter<FileMetadata> {
|
||||
static final Map<String, RelationshipMeta> _kFileMetadataRelationshipMetas = {
|
||||
'release': RelationshipMeta<Release>(
|
||||
name: 'release',
|
||||
inverseName: 'artifacts',
|
||||
type: 'releases',
|
||||
kind: 'BelongsTo',
|
||||
instance: (_) => (_ as FileMetadata).release,
|
||||
)
|
||||
};
|
||||
|
||||
@override
|
||||
Map<String, RelationshipMeta> get relationshipMetas =>
|
||||
_kFileMetadataRelationshipMetas;
|
||||
|
||||
@override
|
||||
FileMetadata deserialize(map) {
|
||||
map = transformDeserialize(map);
|
||||
return FileMetadata.fromMap(map);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> serialize(model, {bool withRelationships = true}) {
|
||||
final map = model.toMap();
|
||||
return transformSerialize(map, withRelationships: withRelationships);
|
||||
}
|
||||
}
|
||||
|
||||
final _fileMetadataFinders = <String, dynamic>{};
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class $FileMetadataHiveLocalAdapter = HiveLocalAdapter<FileMetadata>
|
||||
with $FileMetadataLocalAdapter;
|
||||
|
||||
class $FileMetadataRemoteAdapter = RemoteAdapter<FileMetadata>
|
||||
with NostrAdapter<FileMetadata>;
|
||||
|
||||
final internalFileMetadataRemoteAdapterProvider =
|
||||
Provider<RemoteAdapter<FileMetadata>>((ref) => $FileMetadataRemoteAdapter(
|
||||
$FileMetadataHiveLocalAdapter(ref),
|
||||
InternalHolder(_fileMetadataFinders)));
|
||||
|
||||
final fileMetadataRepositoryProvider =
|
||||
Provider<Repository<FileMetadata>>((ref) => Repository<FileMetadata>(ref));
|
||||
|
||||
extension FileMetadataDataRepositoryX on Repository<FileMetadata> {
|
||||
NostrAdapter<FileMetadata> get nostrAdapter =>
|
||||
remoteAdapter as NostrAdapter<FileMetadata>;
|
||||
}
|
||||
|
||||
extension FileMetadataRelationshipGraphNodeX
|
||||
on RelationshipGraphNode<FileMetadata> {
|
||||
RelationshipGraphNode<Release> get release {
|
||||
final meta =
|
||||
$FileMetadataLocalAdapter._kFileMetadataRelationshipMetas['release']
|
||||
as RelationshipMeta<Release>;
|
||||
return meta.clone(
|
||||
parent: this is RelationshipMeta ? this as RelationshipMeta : null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import 'package:flutter_data/flutter_data.dart';
|
||||
import 'package:ndk/ndk.dart' as ndk;
|
||||
import 'package:zapstore/models/file_metadata.dart';
|
||||
import 'package:zapstore/models/user.dart';
|
||||
|
||||
part 'release.g.dart';
|
||||
|
||||
@DataRepository([NostrAdapter, ReleaseAdapter],
|
||||
fromJson: 'Release.fromMapFactory(map)', toJson: 'model.toMap()')
|
||||
class Release extends BaseEvent<Release> with ndk.Release {
|
||||
Release.fromMap(super.map) : super.fromMap();
|
||||
|
||||
factory Release.fromMapFactory(Map<String, dynamic> map) {
|
||||
final m = Release.fromMap(map);
|
||||
m.artifacts = HasMany<FileMetadata>.fromJson(map['artifacts']);
|
||||
return m;
|
||||
}
|
||||
|
||||
late final HasMany<FileMetadata> artifacts;
|
||||
}
|
||||
|
||||
mixin ReleaseAdapter on RemoteAdapter<Release> {
|
||||
@override
|
||||
Future<DeserializedData<Release>> deserialize(Object? data) {
|
||||
final list = data is Iterable ? data : [data as Map];
|
||||
for (final e in list) {
|
||||
final map = e as Map<String, dynamic>;
|
||||
final eTags = (map['tags'] as Iterable)
|
||||
.where((e) => e.first == 'e')
|
||||
.map((e) => e[1].toString());
|
||||
map['artifacts'] = eTags.toList();
|
||||
}
|
||||
final result = super.deserialize(data);
|
||||
|
||||
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);
|
||||
// }
|
||||
@@ -0,0 +1,68 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'release.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RepositoryGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// ignore_for_file: non_constant_identifier_names, duplicate_ignore
|
||||
|
||||
mixin $ReleaseLocalAdapter on LocalAdapter<Release> {
|
||||
static final Map<String, RelationshipMeta> _kReleaseRelationshipMetas = {
|
||||
'artifacts': RelationshipMeta<FileMetadata>(
|
||||
name: 'artifacts',
|
||||
inverseName: 'release',
|
||||
type: 'fileMetadata',
|
||||
kind: 'HasMany',
|
||||
instance: (_) => (_ as Release).artifacts,
|
||||
)
|
||||
};
|
||||
|
||||
@override
|
||||
Map<String, RelationshipMeta> get relationshipMetas =>
|
||||
_kReleaseRelationshipMetas;
|
||||
|
||||
@override
|
||||
Release deserialize(map) {
|
||||
map = transformDeserialize(map);
|
||||
return Release.fromMapFactory(map);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> serialize(model, {bool withRelationships = true}) {
|
||||
final map = model.toMap();
|
||||
return transformSerialize(map, withRelationships: withRelationships);
|
||||
}
|
||||
}
|
||||
|
||||
final _releasesFinders = <String, dynamic>{};
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class $ReleaseHiveLocalAdapter = HiveLocalAdapter<Release>
|
||||
with $ReleaseLocalAdapter;
|
||||
|
||||
class $ReleaseRemoteAdapter = RemoteAdapter<Release>
|
||||
with NostrAdapter<Release>, ReleaseAdapter;
|
||||
|
||||
final internalReleasesRemoteAdapterProvider = Provider<RemoteAdapter<Release>>(
|
||||
(ref) => $ReleaseRemoteAdapter(
|
||||
$ReleaseHiveLocalAdapter(ref), InternalHolder(_releasesFinders)));
|
||||
|
||||
final releasesRepositoryProvider =
|
||||
Provider<Repository<Release>>((ref) => Repository<Release>(ref));
|
||||
|
||||
extension ReleaseDataRepositoryX on Repository<Release> {
|
||||
NostrAdapter<Release> get nostrAdapter =>
|
||||
remoteAdapter as NostrAdapter<Release>;
|
||||
ReleaseAdapter get releaseAdapter => remoteAdapter as ReleaseAdapter;
|
||||
}
|
||||
|
||||
extension ReleaseRelationshipGraphNodeX on RelationshipGraphNode<Release> {
|
||||
RelationshipGraphNode<FileMetadata> get artifacts {
|
||||
final meta = $ReleaseLocalAdapter._kReleaseRelationshipMetas['artifacts']
|
||||
as RelationshipMeta<FileMetadata>;
|
||||
return meta.clone(
|
||||
parent: this is RelationshipMeta ? this as RelationshipMeta : null);
|
||||
}
|
||||
}
|
||||
+155
-29
@@ -1,44 +1,170 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_data/flutter_data.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:ndk/ndk.dart' as ndk;
|
||||
|
||||
part 'user.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
@DataRepository([UserAdapter])
|
||||
class User extends DataModel<User> with EquatableMixin {
|
||||
final String id;
|
||||
String? name;
|
||||
String? pictureUrl;
|
||||
String? nip05;
|
||||
abstract class BaseEvent<T extends BaseEvent<T>> = ndk.Event
|
||||
with DataModelMixin<T>;
|
||||
|
||||
@DataRepository([NostrAdapter],
|
||||
fromJson: 'User.fromMapFactory(map)', toJson: 'model.toMap()')
|
||||
class User extends BaseEvent<User> with ndk.User {
|
||||
User.fromMap(super.map) : super.fromMap();
|
||||
|
||||
factory User.fromMapFactory(Map<String, dynamic> map) {
|
||||
final user = User.fromMap(map);
|
||||
user.following = HasMany<User>.fromJson({});
|
||||
return user;
|
||||
}
|
||||
|
||||
@DataRelationship(inverse: 'followers')
|
||||
final HasMany<User> following;
|
||||
late final HasMany<User> following;
|
||||
@DataRelationship(inverse: 'following')
|
||||
final HasMany<User> followers;
|
||||
|
||||
User({required this.id, HasMany<User>? following, HasMany<User>? followers})
|
||||
: this.following = following ?? HasMany(),
|
||||
this.followers = followers ?? HasMany();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id];
|
||||
late final HasMany<User> followers = HasMany();
|
||||
}
|
||||
|
||||
mixin UserAdapter on RemoteAdapter<User> {
|
||||
mixin NostrAdapter<T extends BaseEvent<T>> on RemoteAdapter<T> {
|
||||
// void Function()? _closeSub;
|
||||
late final ndk.FrameNotifier notifier;
|
||||
|
||||
@override
|
||||
DataStateNotifier<List<User>> watchAllNotifier(
|
||||
Future<void> onInitialized() async {
|
||||
notifier = ref.read(ndk.frameProvider.notifier);
|
||||
// _closeSub = notifier.addListener((state) async {
|
||||
// if (state is ndk.EventFrame &&
|
||||
// typeKind[internalType] == state.event['kind']) {
|
||||
// final data = await deserialize(state.event);
|
||||
// for (final m in [...data.models, ...data.included]) {
|
||||
// print(
|
||||
// 'r deserialize/save (k:${(m as BaseEvent).kind}) ${m.id} for type $internalType');
|
||||
// // DataModel.adapterFor(m).saveLocal(m.init());
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
super.onInitialized();
|
||||
}
|
||||
|
||||
// @override
|
||||
// void dispose() {
|
||||
// _closeSub?.call();
|
||||
// super.dispose();
|
||||
// }
|
||||
|
||||
Map<String, int> typeKind = {
|
||||
'fileMetadata': 1063,
|
||||
'releases': 30063,
|
||||
};
|
||||
|
||||
@override
|
||||
Future<DeserializedData<T>> deserialize(Object? data) async {
|
||||
final list = data is Iterable ? data : [data as Map];
|
||||
final models = <T>[];
|
||||
final included = <DataModelMixin>[];
|
||||
|
||||
for (final e in list) {
|
||||
final map = e as Map<String, dynamic>;
|
||||
final kind = map['kind'] as int;
|
||||
final xType =
|
||||
typeKind.entries.firstWhereOrNull((e) => e.value == kind)?.key;
|
||||
if (xType != null) {
|
||||
if (xType == internalType) {
|
||||
final newData = await super.deserialize(map);
|
||||
models.addAll(newData.models as Iterable<T>);
|
||||
} else {
|
||||
final newData = await adapters[xType]!.deserialize(map);
|
||||
included.addAll(newData.models as Iterable<DataModelMixin>);
|
||||
}
|
||||
}
|
||||
}
|
||||
return DeserializedData<T>(models, included: included);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<T>> findAll(
|
||||
{bool? remote,
|
||||
bool? background,
|
||||
Map<String, dynamic>? params,
|
||||
Map<String, String>? headers,
|
||||
bool? syncLocal,
|
||||
String? finder,
|
||||
DataRequestLabel? label}) {
|
||||
return super.watchAllNotifier(
|
||||
remote: remote,
|
||||
params: params,
|
||||
headers: headers,
|
||||
syncLocal: syncLocal,
|
||||
finder: finder,
|
||||
label: label);
|
||||
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();
|
||||
}
|
||||
|
||||
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) {
|
||||
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;
|
||||
// });
|
||||
+4
-30
@@ -33,12 +33,12 @@ mixin $UserLocalAdapter on LocalAdapter<User> {
|
||||
@override
|
||||
User deserialize(map) {
|
||||
map = transformDeserialize(map);
|
||||
return _$UserFromJson(map);
|
||||
return User.fromMapFactory(map);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> serialize(model, {bool withRelationships = true}) {
|
||||
final map = _$UserToJson(model);
|
||||
final map = model.toMap();
|
||||
return transformSerialize(map, withRelationships: withRelationships);
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ final _usersFinders = <String, dynamic>{};
|
||||
// ignore: must_be_immutable
|
||||
class $UserHiveLocalAdapter = HiveLocalAdapter<User> with $UserLocalAdapter;
|
||||
|
||||
class $UserRemoteAdapter = RemoteAdapter<User> with UserAdapter;
|
||||
class $UserRemoteAdapter = RemoteAdapter<User> with NostrAdapter<User>;
|
||||
|
||||
final internalUsersRemoteAdapterProvider = Provider<RemoteAdapter<User>>(
|
||||
(ref) => $UserRemoteAdapter(
|
||||
@@ -58,7 +58,7 @@ final usersRepositoryProvider =
|
||||
Provider<Repository<User>>((ref) => Repository<User>(ref));
|
||||
|
||||
extension UserDataRepositoryX on Repository<User> {
|
||||
UserAdapter get userAdapter => remoteAdapter as UserAdapter;
|
||||
NostrAdapter<User> get nostrAdapter => remoteAdapter as NostrAdapter<User>;
|
||||
}
|
||||
|
||||
extension UserRelationshipGraphNodeX on RelationshipGraphNode<User> {
|
||||
@@ -76,29 +76,3 @@ extension UserRelationshipGraphNodeX on RelationshipGraphNode<User> {
|
||||
parent: this is RelationshipMeta ? this as RelationshipMeta : null);
|
||||
}
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
User _$UserFromJson(Map<String, dynamic> json) => User(
|
||||
id: json['id'] as String,
|
||||
following: json['following'] == null
|
||||
? null
|
||||
: HasMany<User>.fromJson(json['following'] as Map<String, dynamic>),
|
||||
followers: json['followers'] == null
|
||||
? null
|
||||
: HasMany<User>.fromJson(json['followers'] as Map<String, dynamic>),
|
||||
)
|
||||
..name = json['name'] as String?
|
||||
..pictureUrl = json['pictureUrl'] as String?
|
||||
..nip05 = json['nip05'] as String?;
|
||||
|
||||
Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'pictureUrl': instance.pictureUrl,
|
||||
'nip05': instance.nip05,
|
||||
'following': instance.following,
|
||||
'followers': instance.followers,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:zapstore/models/release.dart';
|
||||
|
||||
class AppDetailScreen extends HookConsumerWidget {
|
||||
final Release release;
|
||||
const AppDetailScreen({
|
||||
required this.release,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Column(
|
||||
children: [
|
||||
for (final artifact in release.artifacts.toList())
|
||||
Expanded(
|
||||
child: Card(
|
||||
child: Text(
|
||||
'${artifact.kind.toString()} / ${artifact.release.value?.content}'),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
+36
-154
@@ -1,176 +1,58 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:bech32/bech32.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_data/flutter_data.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:ndk/ndk.dart';
|
||||
import 'package:zapstore/main.data.dart';
|
||||
import 'package:ndk/ndk.dart' as ndk;
|
||||
import 'package:zapstore/models/user.dart';
|
||||
import 'package:convert/convert.dart';
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
final loggedInUser = StateProvider<User?>((ref) => null);
|
||||
final downloadsPath = StateProvider<String>((ref) => '/tmp');
|
||||
|
||||
class ProfileScreen extends HookConsumerWidget {
|
||||
ProfileScreen({super.key});
|
||||
final codec = Bech32Codec();
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final u2 = ref.watch(loggedInUser);
|
||||
final controller = useTextEditingController();
|
||||
final controller2 = useTextEditingController(text: ref.read(downloadsPath));
|
||||
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
children: [
|
||||
TextField(controller: controller),
|
||||
SizedBox(height: 10),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
final pubkey = bech32Decode(controller.text);
|
||||
final uid =
|
||||
await ref.read(profileProvider((pubkey, true)).future);
|
||||
ref.read(loggedInUser.notifier).state =
|
||||
await ref.users.findOne(uid, remote: false);
|
||||
},
|
||||
child: Text('Log in with npub')),
|
||||
if (u2 != null)
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Text('Logged in as ${u2.name}'),
|
||||
Text('(following: ${u2.following.length})'),
|
||||
],
|
||||
),
|
||||
),
|
||||
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(
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
children: [
|
||||
TextField(controller: controller),
|
||||
SizedBox(height: 10),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
ref.read(downloadsPath.notifier).state = controller2.text;
|
||||
final pubkey = controller.text.hexKey;
|
||||
ref.read(loggedInUser.notifier).state =
|
||||
await ref.read(usersRepositoryProvider).findOne(pubkey);
|
||||
},
|
||||
child: Text('Save downloads path'),
|
||||
child: Text('Log in with npub')),
|
||||
if (u2 != null)
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Text('Logged in as ${u2.name}'),
|
||||
Text('(following: ${u2.following.length})'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String bech32Encode(String prefix, String hexData) {
|
||||
final data = hex.decode(hexData);
|
||||
final convertedData = convertBits(data, 8, 5, true);
|
||||
final bech32Data = Bech32(prefix, convertedData);
|
||||
return bech32.encode(bech32Data);
|
||||
}
|
||||
|
||||
String bech32Decode(String bech32Data) {
|
||||
final decodedData = bech32.decode(bech32Data);
|
||||
final convertedData = convertBits(decodedData.data, 5, 8, false);
|
||||
final hexData = hex.encode(convertedData);
|
||||
|
||||
return hexData;
|
||||
}
|
||||
|
||||
List<int> convertBits(List<int> data, int fromBits, int toBits, bool pad) {
|
||||
var acc = 0;
|
||||
var bits = 0;
|
||||
final maxv = (1 << toBits) - 1;
|
||||
final result = <int>[];
|
||||
|
||||
for (final value in data) {
|
||||
if (value < 0 || value >> fromBits != 0) {
|
||||
throw Exception('Invalid value: $value');
|
||||
}
|
||||
acc = (acc << fromBits) | value;
|
||||
bits += fromBits;
|
||||
|
||||
while (bits >= toBits) {
|
||||
bits -= toBits;
|
||||
result.add((acc >> bits) & maxv);
|
||||
}
|
||||
}
|
||||
|
||||
if (pad) {
|
||||
if (bits > 0) {
|
||||
result.add((acc << (toBits - bits)) & maxv);
|
||||
}
|
||||
} else if (bits >= fromBits || ((acc << (toBits - bits)) & maxv) != 0) {
|
||||
throw Exception('Invalid data');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
import 'package:expansion_tile_card/expansion_tile_card.dart';
|
||||
import 'package:flutter/material.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:ndk/ndk.dart';
|
||||
import 'package:zapstore/main.data.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 '../main.data.dart';
|
||||
|
||||
class SearchScreen extends HookConsumerWidget {
|
||||
SearchScreen({super.key});
|
||||
final r = Random();
|
||||
@@ -22,88 +23,79 @@ class SearchScreen extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final searchState = useState<String?>(null);
|
||||
final subscriptionIdState = useState<String?>(null);
|
||||
final value = ref
|
||||
.watch(subscriptionFrameProvider(subscriptionIdState.value))
|
||||
.where((e) => searchState.value != null
|
||||
? e.content.contains(searchState.value!)
|
||||
: true)
|
||||
.toList();
|
||||
final labelState = useState<DataRequestLabel?>(null);
|
||||
|
||||
final ids = value.map((e) => e.id).toSet();
|
||||
value.retainWhere((x) => ids.remove(x.id));
|
||||
final value = ref.releases.watchAll(remote: false);
|
||||
|
||||
return Expanded(
|
||||
child: DropTarget(
|
||||
onDragDone: (detail) async {
|
||||
if (detail.files.isNotEmpty) {
|
||||
final bytes = await detail.files.first.readAsBytes();
|
||||
final digest = sha256.convert(bytes).toString();
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
children: [
|
||||
SearchBar(
|
||||
elevation: MaterialStatePropertyAll(2.2),
|
||||
onSubmitted: (query) async {
|
||||
searchState.value = query;
|
||||
labelState.value = DataRequestLabel('query',
|
||||
type: 'releases', requestId: 'sub-${r.nextInt(9999999)}');
|
||||
|
||||
subscriptionIdState.value = 'sub-${digest}';
|
||||
final notifier = ref.read(frameProvider.notifier);
|
||||
notifier.send(jsonEncode([
|
||||
"REQ",
|
||||
subscriptionIdState.value!,
|
||||
{
|
||||
'kinds': [1063],
|
||||
'#x': [digest],
|
||||
}
|
||||
]));
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
children: [
|
||||
SearchBar(
|
||||
elevation: MaterialStatePropertyAll(2.2),
|
||||
onSubmitted: (query) {
|
||||
searchState.value = query;
|
||||
subscriptionIdState.value = 'sub-${r.nextInt(9999999)}';
|
||||
final notifier = ref.read(frameProvider.notifier);
|
||||
notifier.send(jsonEncode([
|
||||
"REQ",
|
||||
subscriptionIdState.value!,
|
||||
{
|
||||
'kinds': [1063],
|
||||
// 'limit': 20,
|
||||
// 'search': query,
|
||||
'#m': ['application/pwa+zip'],
|
||||
// 'since': DateTime.now()
|
||||
// .subtract(Duration(days: 1))
|
||||
// .millisecondsSinceEpoch ~/
|
||||
// 1000,
|
||||
}
|
||||
]));
|
||||
await ref.releases.findAll();
|
||||
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,
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
child: Text('Total results: ${value.length}'),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: value.length,
|
||||
itemBuilder: (context, index) {
|
||||
final event = value[index];
|
||||
return CardWidget(note: event as FileMetadata);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
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}'),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: value.model.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);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CardWidget extends HookConsumerWidget {
|
||||
final FileMetadata note;
|
||||
final FileMetadata fm;
|
||||
|
||||
const CardWidget({super.key, required this.note});
|
||||
const CardWidget({super.key, required this.fm});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -118,7 +110,7 @@ class CardWidget extends HookConsumerWidget {
|
||||
leading: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
child: Image.network(
|
||||
note.tagMap['thumb']!.first,
|
||||
fm.tagMap['thumb']!.first,
|
||||
width: 80,
|
||||
height: 80,
|
||||
fit: BoxFit.cover,
|
||||
@@ -126,7 +118,7 @@ class CardWidget extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
note.content,
|
||||
fm.content,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
|
||||
@@ -134,21 +126,21 @@ class CardWidget extends HookConsumerWidget {
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Type: ${note.tagMap['m']?.first} (${note.id})'),
|
||||
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);
|
||||
}
|
||||
// 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: [
|
||||
@@ -162,9 +154,9 @@ class CardWidget extends HookConsumerWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (authorUser.value != null)
|
||||
Text(
|
||||
'Author: ${authorUser.value!.name} (${authorUser.value!.nip05})'),
|
||||
// if (authorUser.value != null)
|
||||
// Text(
|
||||
// 'Author: ${authorUser.value!.name} (${authorUser.value!.nip05})'),
|
||||
if (isWebOfTrust)
|
||||
Text(
|
||||
'Author is in your web of trust',
|
||||
@@ -189,17 +181,17 @@ class CardWidget extends HookConsumerWidget {
|
||||
label: Text('Install'),
|
||||
icon: Icon(Icons.arrow_downward),
|
||||
onPressed: () async {
|
||||
final uri = Uri.parse(note.tagMap['url']!.first);
|
||||
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, note.tagMap['name']!.first));
|
||||
File(path.join(dir, fm.tagMap['name']!.first));
|
||||
await file.writeAsBytes(response.bodyBytes);
|
||||
|
||||
final digest = sha256.convert(response.bodyBytes);
|
||||
isSha256Ok.value =
|
||||
digest.toString() == note.tagMap['x']!.first;
|
||||
digest.toString() == fm.tagMap['x']!.first;
|
||||
|
||||
if (isSha256Ok.value == false) {
|
||||
print('actual digest $digest');
|
||||
|
||||
+71
-24
@@ -5,18 +5,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a
|
||||
sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "61.0.0"
|
||||
version: "64.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562
|
||||
sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.13.0"
|
||||
version: "6.2.0"
|
||||
android_package_installer:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -50,7 +50,7 @@ packages:
|
||||
source: hosted
|
||||
version: "2.11.0"
|
||||
bech32:
|
||||
dependency: "direct main"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bech32
|
||||
sha256: "156cbace936f7720c79a79d16a03efad343b1ef17106716e04b8b8e39f99f7f7"
|
||||
@@ -274,7 +274,7 @@ packages:
|
||||
source: hosted
|
||||
version: "0.4.4"
|
||||
equatable:
|
||||
dependency: "direct main"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: equatable
|
||||
sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2
|
||||
@@ -329,11 +329,10 @@ packages:
|
||||
flutter_data:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_data
|
||||
sha256: c58ef6a0d1fdf5bc6a1594dde26b5d1ab74a81db59da779d94fc0d9e0995524f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.15"
|
||||
path: "../flutter_data"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.6.0"
|
||||
flutter_hooks:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -392,6 +391,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
go_router:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: go_router
|
||||
sha256: "170c46e237d6eb0e6e9f0e8b3f56101e14fb64f787016e42edd74c39cf8b176a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "13.2.0"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -504,6 +511,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.7.1"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.0"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -524,26 +555,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
|
||||
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.16"
|
||||
version: "0.12.16+1"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
|
||||
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
version: "0.8.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
|
||||
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
version: "1.11.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -571,10 +602,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
|
||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.8.3"
|
||||
version: "1.9.0"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -756,6 +787,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
sprintf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sprintf
|
||||
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -768,10 +807,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: state_notifier
|
||||
sha256: "8fe42610f179b843b12371e40db58c9444f8757f8b69d181c97e50787caed289"
|
||||
sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.2+1"
|
||||
version: "1.0.0"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -832,10 +871,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: uuid
|
||||
sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313"
|
||||
sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
version: "4.3.3"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -844,6 +883,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "13.0.0"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -910,4 +957,4 @@ packages:
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.2.0 <4.0.0"
|
||||
flutter: ">=3.10.0"
|
||||
flutter: ">=3.13.0"
|
||||
|
||||
+4
-3
@@ -32,7 +32,7 @@ dependencies:
|
||||
sdk: flutter
|
||||
cupertino_icons: ^1.0.2
|
||||
gtk_window: ^0.1.1
|
||||
flutter_data: ^1.5.15
|
||||
flutter_data: ^1.6.0
|
||||
hooks_riverpod: ^2.4.9
|
||||
json_annotation: ^4.8.1
|
||||
flutter_hooks: ^0.20.4
|
||||
@@ -41,16 +41,17 @@ dependencies:
|
||||
collection: ^1.18.0
|
||||
path_provider: ^2.1.2
|
||||
ndk: ^0.0.1
|
||||
bech32: ^0.2.2
|
||||
expansion_tile_card: ^3.0.0
|
||||
desktop_drop: ^0.4.4
|
||||
equatable: ^2.0.5
|
||||
crypto: ^3.0.3
|
||||
android_package_installer: ^0.0.2
|
||||
go_router: ^13.2.0
|
||||
|
||||
dependency_overrides:
|
||||
ndk:
|
||||
path: ../ndk
|
||||
flutter_data:
|
||||
path: ../flutter_data
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user