From 7fe914a4d7f240e54c5a34b9f435bb23347def54 Mon Sep 17 00:00:00 2001 From: franzap <_@franzap.com> Date: Wed, 21 Feb 2024 10:37:10 +0000 Subject: [PATCH] Routing, local storage, relationships --- .gitignore | 2 + .metadata | 25 +- .vscode/settings.json | 2 +- android/app/build.gradle | 2 +- .../{testy => zapstore}/MainActivity.kt | 0 android/build.gradle | 2 +- android/gradle.properties | 2 +- lib/main.dart | 352 ++++++++++++------ lib/main.data.dart | 16 +- lib/models/file_metadata.dart | 14 + lib/models/file_metadata.g.dart | 70 ++++ lib/models/release.dart | 57 +++ lib/models/release.g.dart | 68 ++++ lib/models/user.dart | 184 +++++++-- lib/models/user.g.dart | 34 +- lib/screens/app_detail_screen.dart | 26 ++ lib/screens/profile_screen.dart | 190 ++-------- lib/screens/search_screen.dart | 178 +++++---- pubspec.lock | 95 +++-- pubspec.yaml | 7 +- 20 files changed, 864 insertions(+), 462 deletions(-) rename android/app/src/main/kotlin/com/example/{testy => zapstore}/MainActivity.kt (100%) create mode 100644 lib/models/file_metadata.dart create mode 100644 lib/models/file_metadata.g.dart create mode 100644 lib/models/release.dart create mode 100644 lib/models/release.g.dart create mode 100644 lib/screens/app_detail_screen.dart diff --git a/.gitignore b/.gitignore index 29a3a50..3819c4b 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,5 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +/flutter_data diff --git a/.metadata b/.metadata index b060b6a..b5c370f 100644 --- a/.metadata +++ b/.metadata @@ -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 diff --git a/.vscode/settings.json b/.vscode/settings.json index 891146e..b734807 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { - "dart.flutterSdkPath": "~/fvm/versions/3.16.0", + "dart.flutterSdkPath": "~/fvm/default", // Remove .fvm files from search "search.exclude": { "**/.fvm": true diff --git a/android/app/build.gradle b/android/app/build.gradle index aa36916..1f7da28 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -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 diff --git a/android/app/src/main/kotlin/com/example/testy/MainActivity.kt b/android/app/src/main/kotlin/com/example/zapstore/MainActivity.kt similarity index 100% rename from android/app/src/main/kotlin/com/example/testy/MainActivity.kt rename to android/app/src/main/kotlin/com/example/zapstore/MainActivity.kt diff --git a/android/build.gradle b/android/build.gradle index e83fb5d..802640d 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.7.10' + ext.kotlin_version = '1.9.22' repositories { google() mavenCentral() diff --git a/android/gradle.properties b/android/gradle.properties index 598d13f..8a922dd 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,3 +1,3 @@ org.gradle.jvmargs=-Xmx4G android.useAndroidX=true -android.enableJetifier=true +android.enableJetifier=true \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 21b9501..8dba38c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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((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(); +final _searchNavigatorKey = GlobalKey(debugLabel: 'search'); +final _updatesNavigatorKey = GlobalKey(debugLabel: 'updates'); +final _profileNavigatorKey = GlobalKey(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((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('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 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 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), +// ], +// ); +// } +// } diff --git a/lib/main.data.dart b/lib/main.data.dart index 96fa480..3275b85 100644 --- a/lib/main.data.dart +++ b/lib/main.data.dart @@ -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>>{ - 'users': usersRepositoryProvider + 'fileMetadata': fileMetadataRepositoryProvider, +'releases': releasesRepositoryProvider, +'users': usersRepositoryProvider }; final repositoryInitializerProvider = FutureProvider((ref) async { + DataHelpers.setInternalType('fileMetadata'); + DataHelpers.setInternalType('releases'); DataHelpers.setInternalType('users'); - final adapters = {'users': ref.watch(internalUsersRemoteAdapterProvider)}; - final remotes = {'users': true}; + final adapters = {'fileMetadata': ref.watch(internalFileMetadataRemoteAdapterProvider), 'releases': ref.watch(internalReleasesRemoteAdapterProvider), 'users': ref.watch(internalUsersRemoteAdapterProvider)}; + final remotes = {'fileMetadata': true, 'releases': true, 'users': true}; await ref.watch(graphNotifierProvider).initialize(); @@ -54,10 +60,14 @@ final repositoryInitializerProvider = return RepositoryInitializer(); }); extension RepositoryWidgetRefX on WidgetRef { + Repository get fileMetadata => watch(fileMetadataRepositoryProvider)..remoteAdapter.internalWatch = watch; + Repository get releases => watch(releasesRepositoryProvider)..remoteAdapter.internalWatch = watch; Repository get users => watch(usersRepositoryProvider)..remoteAdapter.internalWatch = watch; } extension RepositoryRefX on Ref { + Repository get fileMetadata => watch(fileMetadataRepositoryProvider)..remoteAdapter.internalWatch = watch as Watcher; + Repository get releases => watch(releasesRepositoryProvider)..remoteAdapter.internalWatch = watch as Watcher; Repository get users => watch(usersRepositoryProvider)..remoteAdapter.internalWatch = watch as Watcher; } \ No newline at end of file diff --git a/lib/models/file_metadata.dart b/lib/models/file_metadata.dart new file mode 100644 index 0000000..62f1335 --- /dev/null +++ b/lib/models/file_metadata.dart @@ -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 with ndk.FileMetadata { + FileMetadata.fromMap(super.map) : super.fromMap(); + + late final BelongsTo release = BelongsTo(); +} diff --git a/lib/models/file_metadata.g.dart b/lib/models/file_metadata.g.dart new file mode 100644 index 0000000..477c62e --- /dev/null +++ b/lib/models/file_metadata.g.dart @@ -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 { + static final Map _kFileMetadataRelationshipMetas = { + 'release': RelationshipMeta( + name: 'release', + inverseName: 'artifacts', + type: 'releases', + kind: 'BelongsTo', + instance: (_) => (_ as FileMetadata).release, + ) + }; + + @override + Map get relationshipMetas => + _kFileMetadataRelationshipMetas; + + @override + FileMetadata deserialize(map) { + map = transformDeserialize(map); + return FileMetadata.fromMap(map); + } + + @override + Map serialize(model, {bool withRelationships = true}) { + final map = model.toMap(); + return transformSerialize(map, withRelationships: withRelationships); + } +} + +final _fileMetadataFinders = {}; + +// ignore: must_be_immutable +class $FileMetadataHiveLocalAdapter = HiveLocalAdapter + with $FileMetadataLocalAdapter; + +class $FileMetadataRemoteAdapter = RemoteAdapter + with NostrAdapter; + +final internalFileMetadataRemoteAdapterProvider = + Provider>((ref) => $FileMetadataRemoteAdapter( + $FileMetadataHiveLocalAdapter(ref), + InternalHolder(_fileMetadataFinders))); + +final fileMetadataRepositoryProvider = + Provider>((ref) => Repository(ref)); + +extension FileMetadataDataRepositoryX on Repository { + NostrAdapter get nostrAdapter => + remoteAdapter as NostrAdapter; +} + +extension FileMetadataRelationshipGraphNodeX + on RelationshipGraphNode { + RelationshipGraphNode get release { + final meta = + $FileMetadataLocalAdapter._kFileMetadataRelationshipMetas['release'] + as RelationshipMeta; + return meta.clone( + parent: this is RelationshipMeta ? this as RelationshipMeta : null); + } +} diff --git a/lib/models/release.dart b/lib/models/release.dart new file mode 100644 index 0000000..dbb0fdc --- /dev/null +++ b/lib/models/release.dart @@ -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 with ndk.Release { + Release.fromMap(super.map) : super.fromMap(); + + factory Release.fromMapFactory(Map map) { + final m = Release.fromMap(map); + m.artifacts = HasMany.fromJson(map['artifacts']); + return m; + } + + late final HasMany artifacts; +} + +mixin ReleaseAdapter on RemoteAdapter { + @override + Future> deserialize(Object? data) { + final list = data is Iterable ? data : [data as Map]; + for (final e in list) { + final map = e as Map; + final eTags = (map['tags'] as Iterable) + .where((e) => e.first == 'e') + .map((e) => e[1].toString()); + map['artifacts'] = eTags.toList(); + } + final 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); +// } diff --git a/lib/models/release.g.dart b/lib/models/release.g.dart new file mode 100644 index 0000000..1dcab1b --- /dev/null +++ b/lib/models/release.g.dart @@ -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 { + static final Map _kReleaseRelationshipMetas = { + 'artifacts': RelationshipMeta( + name: 'artifacts', + inverseName: 'release', + type: 'fileMetadata', + kind: 'HasMany', + instance: (_) => (_ as Release).artifacts, + ) + }; + + @override + Map get relationshipMetas => + _kReleaseRelationshipMetas; + + @override + Release deserialize(map) { + map = transformDeserialize(map); + return Release.fromMapFactory(map); + } + + @override + Map serialize(model, {bool withRelationships = true}) { + final map = model.toMap(); + return transformSerialize(map, withRelationships: withRelationships); + } +} + +final _releasesFinders = {}; + +// ignore: must_be_immutable +class $ReleaseHiveLocalAdapter = HiveLocalAdapter + with $ReleaseLocalAdapter; + +class $ReleaseRemoteAdapter = RemoteAdapter + with NostrAdapter, ReleaseAdapter; + +final internalReleasesRemoteAdapterProvider = Provider>( + (ref) => $ReleaseRemoteAdapter( + $ReleaseHiveLocalAdapter(ref), InternalHolder(_releasesFinders))); + +final releasesRepositoryProvider = + Provider>((ref) => Repository(ref)); + +extension ReleaseDataRepositoryX on Repository { + NostrAdapter get nostrAdapter => + remoteAdapter as NostrAdapter; + ReleaseAdapter get releaseAdapter => remoteAdapter as ReleaseAdapter; +} + +extension ReleaseRelationshipGraphNodeX on RelationshipGraphNode { + RelationshipGraphNode get artifacts { + final meta = $ReleaseLocalAdapter._kReleaseRelationshipMetas['artifacts'] + as RelationshipMeta; + return meta.clone( + parent: this is RelationshipMeta ? this as RelationshipMeta : null); + } +} diff --git a/lib/models/user.dart b/lib/models/user.dart index e093dcf..d66ceaf 100644 --- a/lib/models/user.dart +++ b/lib/models/user.dart @@ -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 with EquatableMixin { - final String id; - String? name; - String? pictureUrl; - String? nip05; +abstract class BaseEvent> = ndk.Event + with DataModelMixin; + +@DataRepository([NostrAdapter], + fromJson: 'User.fromMapFactory(map)', toJson: 'model.toMap()') +class User extends BaseEvent with ndk.User { + User.fromMap(super.map) : super.fromMap(); + + factory User.fromMapFactory(Map map) { + final user = User.fromMap(map); + user.following = HasMany.fromJson({}); + return user; + } + @DataRelationship(inverse: 'followers') - final HasMany following; + late final HasMany following; @DataRelationship(inverse: 'following') - final HasMany followers; - - User({required this.id, HasMany? following, HasMany? followers}) - : this.following = following ?? HasMany(), - this.followers = followers ?? HasMany(); - - @override - List get props => [id]; + late final HasMany followers = HasMany(); } -mixin UserAdapter on RemoteAdapter { +mixin NostrAdapter> on RemoteAdapter { + // void Function()? _closeSub; + late final ndk.FrameNotifier notifier; + @override - DataStateNotifier> watchAllNotifier( + Future 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 typeKind = { + 'fileMetadata': 1063, + 'releases': 30063, + }; + + @override + Future> deserialize(Object? data) async { + final list = data is Iterable ? data : [data as Map]; + final models = []; + final included = []; + + for (final e in list) { + final map = e as Map; + 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); + } else { + final newData = await adapters[xType]!.deserialize(map); + included.addAll(newData.models as Iterable); + } + } + } + return DeserializedData(models, included: included); + } + + @override + Future> findAll( {bool? remote, + bool? background, Map? params, Map? headers, bool? syncLocal, - String? finder, - DataRequestLabel? label}) { - return super.watchAllNotifier( - remote: remote, - params: params, - headers: headers, - syncLocal: syncLocal, - finder: finder, - label: label); + OnSuccessAll? onSuccess, + OnErrorAll? 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((ref, record) async { +// final (pubkey, loadContacts) = record; +// final completer = Completer(); +// 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? 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; +// }); \ No newline at end of file diff --git a/lib/models/user.g.dart b/lib/models/user.g.dart index ec4f338..0d798f6 100644 --- a/lib/models/user.g.dart +++ b/lib/models/user.g.dart @@ -33,12 +33,12 @@ mixin $UserLocalAdapter on LocalAdapter { @override User deserialize(map) { map = transformDeserialize(map); - return _$UserFromJson(map); + return User.fromMapFactory(map); } @override Map serialize(model, {bool withRelationships = true}) { - final map = _$UserToJson(model); + final map = model.toMap(); return transformSerialize(map, withRelationships: withRelationships); } } @@ -48,7 +48,7 @@ final _usersFinders = {}; // ignore: must_be_immutable class $UserHiveLocalAdapter = HiveLocalAdapter with $UserLocalAdapter; -class $UserRemoteAdapter = RemoteAdapter with UserAdapter; +class $UserRemoteAdapter = RemoteAdapter with NostrAdapter; final internalUsersRemoteAdapterProvider = Provider>( (ref) => $UserRemoteAdapter( @@ -58,7 +58,7 @@ final usersRepositoryProvider = Provider>((ref) => Repository(ref)); extension UserDataRepositoryX on Repository { - UserAdapter get userAdapter => remoteAdapter as UserAdapter; + NostrAdapter get nostrAdapter => remoteAdapter as NostrAdapter; } extension UserRelationshipGraphNodeX on RelationshipGraphNode { @@ -76,29 +76,3 @@ extension UserRelationshipGraphNodeX on RelationshipGraphNode { parent: this is RelationshipMeta ? this as RelationshipMeta : null); } } - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -User _$UserFromJson(Map json) => User( - id: json['id'] as String, - following: json['following'] == null - ? null - : HasMany.fromJson(json['following'] as Map), - followers: json['followers'] == null - ? null - : HasMany.fromJson(json['followers'] as Map), - ) - ..name = json['name'] as String? - ..pictureUrl = json['pictureUrl'] as String? - ..nip05 = json['nip05'] as String?; - -Map _$UserToJson(User instance) => { - 'id': instance.id, - 'name': instance.name, - 'pictureUrl': instance.pictureUrl, - 'nip05': instance.nip05, - 'following': instance.following, - 'followers': instance.followers, - }; diff --git a/lib/screens/app_detail_screen.dart b/lib/screens/app_detail_screen.dart new file mode 100644 index 0000000..1fff83e --- /dev/null +++ b/lib/screens/app_detail_screen.dart @@ -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}'), + ), + ), + ], + ); + } +} diff --git a/lib/screens/profile_screen.dart b/lib/screens/profile_screen.dart index 66ff554..649663f 100644 --- a/lib/screens/profile_screen.dart +++ b/lib/screens/profile_screen.dart @@ -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((ref, record) async { - final (pubkey, loadContacts) = record; - final completer = Completer(); - 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? 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((ref) => null); final downloadsPath = StateProvider((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 convertBits(List data, int fromBits, int toBits, bool pad) { - var acc = 0; - var bits = 0; - final maxv = (1 << toBits) - 1; - final result = []; - - 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; -} diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index bc3593a..910dfaf 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -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(null); - final subscriptionIdState = useState(null); - final value = ref - .watch(subscriptionFrameProvider(subscriptionIdState.value)) - .where((e) => searchState.value != null - ? e.content.contains(searchState.value!) - : true) - .toList(); + final labelState = useState(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'); diff --git a/pubspec.lock b/pubspec.lock index a10c6d1..f312293 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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" diff --git a/pubspec.yaml b/pubspec.yaml index 60de5e2..b9784a6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: