Routing, local storage, relationships

This commit is contained in:
franzap
2024-02-21 10:37:10 +00:00
parent dd48f8783d
commit 7fe914a4d7
20 changed files with 864 additions and 462 deletions
+2
View File
@@ -41,3 +41,5 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
/flutter_data
+20 -5
View File
@@ -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
+1 -1
View File
@@ -1,5 +1,5 @@
{
"dart.flutterSdkPath": "~/fvm/versions/3.16.0",
"dart.flutterSdkPath": "~/fvm/default",
// Remove .fvm files from search
"search.exclude": {
"**/.fvm": true
+1 -1
View File
@@ -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 -1
View File
@@ -1,5 +1,5 @@
buildscript {
ext.kotlin_version = '1.7.10'
ext.kotlin_version = '1.9.22'
repositories {
google()
mavenCentral()
+1 -1
View File
@@ -1,3 +1,3 @@
org.gradle.jvmargs=-Xmx4G
android.useAndroidX=true
android.enableJetifier=true
android.enableJetifier=true
+235 -117
View File
@@ -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
View File
@@ -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;
}
+14
View File
@@ -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();
}
+70
View File
@@ -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);
}
}
+57
View File
@@ -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);
// }
+68
View File
@@ -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
View File
@@ -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
View File
@@ -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,
};
+26
View File
@@ -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
View File
@@ -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;
}
+85 -93
View File
@@ -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
View File
@@ -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
View File
@@ -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: