diff --git a/analysis_options.yaml b/analysis_options.yaml index dff2f71..fb1fb95 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -24,7 +24,9 @@ linter: # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule prefer_const_constructors: false + prefer_const_constructors_in_immutables: false avoid_print: false + prefer_const_literals_to_create_immutables: false # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index c2fe2f6..ab46def 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -53,6 +53,7 @@ + diff --git a/assets/fonts/Inter-Bold.ttf b/assets/fonts/Inter-Bold.ttf new file mode 100644 index 0000000..15e908f Binary files /dev/null and b/assets/fonts/Inter-Bold.ttf differ diff --git a/assets/fonts/Inter-Italic.ttf b/assets/fonts/Inter-Italic.ttf new file mode 100644 index 0000000..e0421ee Binary files /dev/null and b/assets/fonts/Inter-Italic.ttf differ diff --git a/assets/fonts/Inter-Light.ttf b/assets/fonts/Inter-Light.ttf new file mode 100644 index 0000000..31ac788 Binary files /dev/null and b/assets/fonts/Inter-Light.ttf differ diff --git a/assets/fonts/Inter-Regular.ttf b/assets/fonts/Inter-Regular.ttf new file mode 100644 index 0000000..c544be4 Binary files /dev/null and b/assets/fonts/Inter-Regular.ttf differ diff --git a/build.yaml b/build.yaml new file mode 100644 index 0000000..e69de29 diff --git a/lib/main.dart b/lib/main.dart index ec6d847..7204ce0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,28 +1,39 @@ -// ignore_for_file: prefer_const_literals_to_create_immutables, prefer_const_constructors - import 'package:flutter/material.dart'; import 'package:flutter_data/flutter_data.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:ndk/ndk.dart' as ndk; +import 'package:path_provider/path_provider.dart'; import 'package:zapstore/main.data.dart'; -import 'package:zapstore/models/release.dart'; +import 'package:zapstore/models/app.dart'; import 'package:zapstore/screens/app_detail_screen.dart'; -import 'package:zapstore/screens/profile_screen.dart'; +import 'package:zapstore/screens/settings_screen.dart'; +import 'package:zapstore/screens/updates_screen.dart'; +import 'package:zapstore/utils/extensions.dart'; +import 'package:zapstore/widgets/app_drawer.dart'; import 'package:zapstore/screens/search_screen.dart'; void main() { runApp( ProviderScope( overrides: [ - configureRepositoryLocalStorage( - clear: LocalStorageClearStrategy.always), + localStorageProvider.overrideWithValue( + LocalStorage( + baseDirFn: () async { + final path = (await getApplicationSupportDirectory()).path; + print('initializing local storage at $path'); + return path; + }, + clear: LocalStorageClearStrategy.whenError, + ), + ) ], child: const ZapstoreApp(), ), ); } +const kBackgroundColor = Color.fromARGB(255, 6, 6, 6); + class ZapstoreApp extends StatelessWidget { const ZapstoreApp({super.key}); @@ -31,11 +42,17 @@ class ZapstoreApp extends StatelessWidget { return MaterialApp.router( routerConfig: goRouter, debugShowCheckedModeBanner: false, - darkTheme: ThemeData( - primarySwatch: Colors.purple, + theme: ThemeData( + primarySwatch: Colors.lightBlue, brightness: Brightness.dark, + fontFamily: 'Inter', + useMaterial3: true, + scaffoldBackgroundColor: kBackgroundColor, + visualDensity: VisualDensity.adaptivePlatformDensity, + // textTheme: context.theme.textTheme.copyWith( + // bodyLarge: + // TextStyle(color: Colors.white, fontWeight: FontWeight.w300)) ), - theme: ThemeData.dark(useMaterial3: true), ); } } @@ -44,21 +61,16 @@ class ZapstoreApp extends StatelessWidget { final _rootNavigatorKey = GlobalKey(); final _searchNavigatorKey = GlobalKey(debugLabel: 'search'); final _updatesNavigatorKey = GlobalKey(debugLabel: 'updates'); -final _profileNavigatorKey = GlobalKey(debugLabel: 'profile'); -final _notificationsNavigatorKey = - GlobalKey(debugLabel: 'notifications'); +final _settingsNavigatorKey = GlobalKey(debugLabel: 'settings'); final goRouter = GoRouter( - initialLocation: '/search', + initialLocation: '/', // * 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); @@ -68,15 +80,19 @@ final goRouter = GoRouter( navigatorKey: _searchNavigatorKey, routes: [ GoRoute( - path: '/search', - pageBuilder: (context, state) => NoTransitionPage( - child: SearchScreen(), - ), + path: '/', + builder: (context, state) => SearchScreen(), routes: [ GoRoute( path: 'details', builder: (context, state) => - AppDetailScreen(release: state.extra as Release), + AppDetailScreen(app: state.extra as App), + // pageBuilder: (context, state) => + // buildWithSlideTransition( + // context: context, + // state: state, + // child: AppDetailScreen(app: state.extra as App), + // ), ), ], ), @@ -87,41 +103,18 @@ final goRouter = GoRouter( routes: [ GoRoute( path: '/updates', - pageBuilder: (context, state) => NoTransitionPage( - child: Center( - child: ElevatedButton( - onPressed: () => {}, - child: const Text('Updates coming soon!'), - ), - ), - ), + builder: (context, state) => UpdatesScreen(), ), ], ), StatefulShellBranch( - navigatorKey: _profileNavigatorKey, + navigatorKey: _settingsNavigatorKey, routes: [ GoRoute( - path: '/profile', + path: '/settings', pageBuilder: (context, state) => NoTransitionPage( child: Center( - child: ProfileScreen(), - ), - ), - ), - ], - ), - StatefulShellBranch( - navigatorKey: _notificationsNavigatorKey, - routes: [ - GoRoute( - path: '/notifications', - pageBuilder: (context, state) => NoTransitionPage( - child: Center( - child: ElevatedButton( - onPressed: () => {}, - child: const Text('Notifications coming soon!'), - ), + child: SettingsScreen(), ), ), ), @@ -133,12 +126,12 @@ final goRouter = GoRouter( ); final newInitializer = FutureProvider((ref) async { - await ref.read(repositoryInitializerProvider.future); - await ref.read(ndk.frameProvider.notifier).initialize('wss://relay.damus.io'); + await ref.read(initializeFlutterData(adapterProvidersMap).future); + // ref + // .read(p.relayMessageNotifierProvider.notifier) + // .initialize(['wss://relay.zap.store', 'wss://relay.nostr.band']); }); -// 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, @@ -163,7 +156,7 @@ class ScaffoldWithNestedNavigation extends HookConsumerWidget { return SafeArea( child: LayoutBuilder( builder: (context, constraints) { - if (constraints.maxWidth < 450) { + if (constraints.maxWidth < 550) { return ScaffoldWithNavigationBar( body: navigationShell, selectedIndex: navigationShell.currentIndex, @@ -197,34 +190,48 @@ class ScaffoldWithNavigationBar extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final initializer = ref.watch(newInitializer); return Scaffold( - body: initializer.when( - data: (_) => body, - error: (e, _) => const Text('error'), - loading: () => const Center(child: CircularProgressIndicator()), + body: Padding( + padding: const EdgeInsets.fromLTRB(16, 20, 16, 0), + child: initializer.when( + data: (_) => body, + error: (e, _) => const Text('error'), + loading: () => const Center(child: CircularProgressIndicator()), + ), ), bottomNavigationBar: NavigationBar( + height: 60, + backgroundColor: kBackgroundColor, + indicatorColor: Colors.transparent, selectedIndex: selectedIndex, labelBehavior: NavigationDestinationLabelBehavior.alwaysHide, destinations: const [ NavigationDestination( - label: 'Search', - icon: Icon(Icons.search_outlined), + label: 'Home', + icon: Icon(Icons.home_outlined), + selectedIcon: Icon(Icons.home_filled), ), NavigationDestination( label: 'Updates', icon: Icon(Icons.download_for_offline_outlined), + selectedIcon: Icon(Icons.download_for_offline), ), NavigationDestination( - label: 'Profile', - icon: Icon(Icons.person_outline), - ), - NavigationDestination( - label: 'Notifications', - icon: Icon(Icons.notifications_outlined), + label: 'Settings', + icon: Icon(Icons.settings_outlined), + selectedIcon: Icon(Icons.settings), ), ], onDestinationSelected: onDestinationSelected, ), + drawer: Drawer( + child: Column( + children: [ + DrawerHeader( + child: AppDrawer(), + ), + ], + ), + ), ); } } @@ -262,10 +269,6 @@ class ScaffoldWithNavigationRail extends StatelessWidget { label: Text('Profile'), icon: Icon(Icons.person_outline), ), - const NavigationRailDestination( - label: Text('Notifications'), - icon: Icon(Icons.notifications_outlined), - ), ], ), body, @@ -275,37 +278,34 @@ class ScaffoldWithNavigationRail extends StatelessWidget { } } - -// const borderColor = Color(0xFF805306); - -// const sidebarColor = Color(0xFFF6A00C); -// const backgroundStartColor = Color(0xFFFFD500); -// const backgroundEndColor = Color(0xFFF6A00C); - -// final buttonColors = WindowButtonColors( -// iconNormal: const Color(0xFF805306), -// mouseOver: const Color(0xFFF6A00C), -// mouseDown: const Color(0xFF805306), -// iconMouseOver: const Color(0xFF805306), -// iconMouseDown: const Color(0xFFFFD500)); - -// final closeButtonColors = WindowButtonColors( -// mouseOver: const Color(0xFFD32F2F), -// mouseDown: const Color(0xFFB71C1C), -// iconNormal: const Color(0xFF805306), -// iconMouseOver: Colors.white); - -// 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), -// ], -// ); -// } -// } +CustomTransitionPage buildWithSlideTransition({ + required BuildContext context, + required GoRouterState state, + required Widget child, +}) { + return CustomTransitionPage( + key: state.pageKey, + child: child, + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return SlideTransition( + position: + Tween(begin: const Offset(1, 0), end: Offset.zero).animate( + CurvedAnimation( + parent: animation, + curve: Curves.fastOutSlowIn, + ), + ), + child: AnimatedBuilder( + animation: animation, + builder: (context, child) { + return Container( + color: context.theme.colorScheme.background, + child: child, + ); + }, + child: child, + ), + ); + }, + ); +} diff --git a/lib/main.data.dart b/lib/main.data.dart index 3275b85..de65e8e 100644 --- a/lib/main.data.dart +++ b/lib/main.data.dart @@ -4,70 +4,31 @@ // ignore_for_file: directives_ordering, top_level_function_literal_block, depend_on_referenced_packages import 'package:flutter_data/flutter_data.dart'; -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/app.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 -ConfigureRepositoryLocalStorage configureRepositoryLocalStorage = ({FutureFn? baseDirFn, List? encryptionKey, LocalStorageClearStrategy? clear}) { - if (!kIsWeb) { - baseDirFn ??= () => getApplicationDocumentsDirectory().then((dir) => dir.path); - } else { - baseDirFn ??= () => ''; - } - - return hiveLocalStorageProvider.overrideWith( - (ref) => HiveLocalStorage( - hive: ref.read(hiveProvider), - baseDirFn: baseDirFn, - encryptionKey: encryptionKey, - clear: clear, - ), - ); +final adapterProvidersMap = >>{ + 'apps': appsAdapterProvider, +'fileMetadata': fileMetadataAdapterProvider, +'releases': releasesAdapterProvider, +'users': usersAdapterProvider }; -final repositoryProviders = >>{ - 'fileMetadata': fileMetadataRepositoryProvider, -'releases': releasesRepositoryProvider, -'users': usersRepositoryProvider -}; - -final repositoryInitializerProvider = - FutureProvider((ref) async { - DataHelpers.setInternalType('fileMetadata'); - DataHelpers.setInternalType('releases'); - DataHelpers.setInternalType('users'); - 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(); - - // initialize and register - for (final type in repositoryProviders.keys) { - final repository = ref.read(repositoryProviders[type]!); - repository.dispose(); - await repository.initialize( - remote: remotes[type], - adapters: adapters, - ); - internalRepositories[type] = repository; - } - - 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 AdapterWidgetRefX on WidgetRef { + Adapter get apps => watch(appsAdapterProvider)..internalWatch = watch; + Adapter get fileMetadata => watch(fileMetadataAdapterProvider)..internalWatch = watch; + Adapter get releases => watch(releasesAdapterProvider)..internalWatch = watch; + Adapter get users => watch(usersAdapterProvider)..internalWatch = watch; } -extension RepositoryRefX on Ref { +extension AdapterRefX 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; + Adapter get apps => watch(appsAdapterProvider)..internalWatch = watch as Watcher; + Adapter get fileMetadata => watch(fileMetadataAdapterProvider)..internalWatch = watch as Watcher; + Adapter get releases => watch(releasesAdapterProvider)..internalWatch = watch as Watcher; + Adapter get users => watch(usersAdapterProvider)..internalWatch = watch as Watcher; } \ No newline at end of file diff --git a/lib/models/app.dart b/lib/models/app.dart new file mode 100644 index 0000000..c05bee3 --- /dev/null +++ b/lib/models/app.dart @@ -0,0 +1,38 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_data/flutter_data.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:purplebase/purplebase.dart'; +import 'package:zapstore/models/release.dart'; +import 'package:zapstore/models/user.dart'; + +part 'app.g.dart'; + +@JsonSerializable() +@DataAdapter([NostrAdapter, CoolAdapter]) +class App extends ZapstoreEvent with BaseApp, EquatableMixin { + late final HasMany releases; + App(); + factory App.fromJson(Map json) => _$AppFromJson(json); + @override + List get props => [id]; +} + +mixin CoolAdapter on Adapter { + @override + Future onInitialized() async { + // db.execute(''' + // CREATE TABLE IF NOT EXISTS $internalType ( + // key INTEGER PRIMARY KEY AUTOINCREMENT, + // data TEXT + // ); + // '''); + super.onInitialized(); + } + + Future> query(int kind) async { + final result = db.select( + "SELECT data FROM $internalType WHERE json_extract(data, '\$.kind') = ?", + [kind]); + return deserializeFromResult(result); + } +} diff --git a/lib/models/app.g.dart b/lib/models/app.g.dart new file mode 100644 index 0000000..adde420 --- /dev/null +++ b/lib/models/app.g.dart @@ -0,0 +1,86 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'app.dart'; + +// ************************************************************************** +// AdapterGenerator +// ************************************************************************** + +// ignore_for_file: non_constant_identifier_names, duplicate_ignore + +mixin _$AppAdapter on Adapter { + static final Map _kAppRelationshipMetas = { + 'releases': RelationshipMeta( + name: 'releases', + inverseName: 'app', + type: 'releases', + kind: 'HasMany', + instance: (_) => (_ as App).releases, + ) + }; + + @override + Map get relationshipMetas => _kAppRelationshipMetas; + + @override + App deserializeLocal(map, {String? key}) { + map = transformDeserialize(map); + return internalWrapStopInit(() => App.fromJson(map), key: key); + } + + @override + Map serializeLocal(model, {bool withRelationships = true}) { + final map = _$AppToJson(model); + return transformSerialize(map, withRelationships: withRelationships); + } +} + +final _appsFinders = {}; + +class $AppAdapter = Adapter + with _$AppAdapter, NostrAdapter, CoolAdapter; + +final appsAdapterProvider = Provider>( + (ref) => $AppAdapter(ref, InternalHolder(_appsFinders))); + +extension AppAdapterX on Adapter { + NostrAdapter get nostrAdapter => this as NostrAdapter; + CoolAdapter get coolAdapter => this as CoolAdapter; +} + +extension AppRelationshipGraphNodeX on RelationshipGraphNode { + RelationshipGraphNode get releases { + final meta = _$AppAdapter._kAppRelationshipMetas['releases'] + as RelationshipMeta; + return meta.clone( + parent: this is RelationshipMeta ? this as RelationshipMeta : null); + } +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +App _$AppFromJson(Map json) => App() + ..id = json['id'] + ..pubkey = json['pubkey'] as String + ..createdAt = DateTime.parse(json['createdAt'] as String) + ..content = json['content'] as String + ..kind = json['kind'] as int + ..tags = (json['tags'] as List) + .map((e) => (e as List).map((e) => e as String).toList()) + .toList() + ..signature = json['signature'] as String? + ..releases = + HasMany.fromJson(json['releases'] as Map); + +Map _$AppToJson(App instance) => { + 'id': instance.id, + 'pubkey': instance.pubkey, + 'createdAt': instance.createdAt.toIso8601String(), + 'content': instance.content, + 'kind': instance.kind, + 'tags': instance.tags, + 'signature': instance.signature, + 'releases': instance.releases, + }; diff --git a/lib/models/file_metadata.dart b/lib/models/file_metadata.dart index a00f8cf..8e0c1b4 100644 --- a/lib/models/file_metadata.dart +++ b/lib/models/file_metadata.dart @@ -4,7 +4,8 @@ import 'package:android_package_installer/android_package_installer.dart'; import 'package:android_package_manager/android_package_manager.dart'; import 'package:crypto/crypto.dart'; import 'package:flutter_data/flutter_data.dart'; -import 'package:ndk/ndk.dart' as ndk; +import 'package:json_annotation/json_annotation.dart'; +import 'package:purplebase/purplebase.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:zapstore/models/release.dart'; @@ -16,17 +17,9 @@ part 'file_metadata.g.dart'; final packageManager = AndroidPackageManager(); -@DataRepository([NostrAdapter, FileMetadataAdapter], - fromJson: 'FileMetadata.fromMapFactory(map)', toJson: 'model.toMap()') -class FileMetadata extends BaseEvent with ndk.FileMetadata { - FileMetadata.fromMap(super.map) : super.fromMap(); - - factory FileMetadata.fromMapFactory(Map map) { - final m = FileMetadata.fromMap(map); - m.author = BelongsTo.fromJson(map['author']); - return m; - } - +@JsonSerializable() +@DataAdapter([NostrAdapter, FileMetadataAdapter]) +class FileMetadata extends ZapstoreEvent with BaseFileMetadata { late final BelongsTo author; late final BelongsTo release = BelongsTo(); @@ -66,16 +59,14 @@ class FileMetadata extends BaseEvent with ndk.FileMetadata { } } -mixin FileMetadataAdapter on RemoteAdapter { +mixin FileMetadataAdapter on Adapter { @override - Future> deserialize(Object? data) async { + DeserializedData deserialize(Object? data, {String? key}) { final list = data is Iterable ? data : [data as Map]; for (final e in list) { final map = e as Map; map['author'] = map['pubkey']; } - final result = await super.deserialize(data); - print(result); - return result; + return super.deserialize(data); } } diff --git a/lib/models/file_metadata.g.dart b/lib/models/file_metadata.g.dart index adeafe6..0f4be5c 100644 --- a/lib/models/file_metadata.g.dart +++ b/lib/models/file_metadata.g.dart @@ -3,12 +3,12 @@ part of 'file_metadata.dart'; // ************************************************************************** -// RepositoryGenerator +// AdapterGenerator // ************************************************************************** // ignore_for_file: non_constant_identifier_names, duplicate_ignore -mixin $FileMetadataLocalAdapter on LocalAdapter { +mixin _$FileMetadataAdapter on Adapter { static final Map _kFileMetadataRelationshipMetas = { 'author': RelationshipMeta( name: 'author', @@ -30,56 +30,74 @@ mixin $FileMetadataLocalAdapter on LocalAdapter { _kFileMetadataRelationshipMetas; @override - FileMetadata deserialize(map) { + FileMetadata deserializeLocal(map, {String? key}) { map = transformDeserialize(map); - return FileMetadata.fromMapFactory(map); + return internalWrapStopInit(() => _$FileMetadataFromJson(map), key: key); } @override - Map serialize(model, {bool withRelationships = true}) { - final map = model.toMap(); + Map serializeLocal(model, {bool withRelationships = true}) { + final map = _$FileMetadataToJson(model); return transformSerialize(map, withRelationships: withRelationships); } } final _fileMetadataFinders = {}; -// ignore: must_be_immutable -class $FileMetadataHiveLocalAdapter = HiveLocalAdapter - with $FileMetadataLocalAdapter; +class $FileMetadataAdapter = Adapter + with _$FileMetadataAdapter, NostrAdapter, FileMetadataAdapter; -class $FileMetadataRemoteAdapter = RemoteAdapter - with NostrAdapter, FileMetadataAdapter; +final fileMetadataAdapterProvider = Provider>( + (ref) => $FileMetadataAdapter(ref, InternalHolder(_fileMetadataFinders))); -final internalFileMetadataRemoteAdapterProvider = - Provider>((ref) => $FileMetadataRemoteAdapter( - $FileMetadataHiveLocalAdapter(ref), - InternalHolder(_fileMetadataFinders))); - -final fileMetadataRepositoryProvider = - Provider>((ref) => Repository(ref)); - -extension FileMetadataDataRepositoryX on Repository { +extension FileMetadataAdapterX on Adapter { NostrAdapter get nostrAdapter => - remoteAdapter as NostrAdapter; - FileMetadataAdapter get fileMetadataAdapter => - remoteAdapter as FileMetadataAdapter; + this as NostrAdapter; + FileMetadataAdapter get fileMetadataAdapter => this as FileMetadataAdapter; } extension FileMetadataRelationshipGraphNodeX on RelationshipGraphNode { RelationshipGraphNode get author { - final meta = $FileMetadataLocalAdapter - ._kFileMetadataRelationshipMetas['author'] as RelationshipMeta; + final meta = _$FileMetadataAdapter._kFileMetadataRelationshipMetas['author'] + as RelationshipMeta; return meta.clone( parent: this is RelationshipMeta ? this as RelationshipMeta : null); } RelationshipGraphNode get release { final meta = - $FileMetadataLocalAdapter._kFileMetadataRelationshipMetas['release'] + _$FileMetadataAdapter._kFileMetadataRelationshipMetas['release'] as RelationshipMeta; return meta.clone( parent: this is RelationshipMeta ? this as RelationshipMeta : null); } } + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +FileMetadata _$FileMetadataFromJson(Map json) => FileMetadata() + ..id = json['id'] + ..pubkey = json['pubkey'] as String + ..createdAt = DateTime.parse(json['createdAt'] as String) + ..content = json['content'] as String + ..kind = json['kind'] as int + ..tags = (json['tags'] as List) + .map((e) => (e as List).map((e) => e as String).toList()) + .toList() + ..signature = json['signature'] as String? + ..author = BelongsTo.fromJson(json['author'] as Map); + +Map _$FileMetadataToJson(FileMetadata instance) => + { + 'id': instance.id, + 'pubkey': instance.pubkey, + 'createdAt': instance.createdAt.toIso8601String(), + 'content': instance.content, + 'kind': instance.kind, + 'tags': instance.tags, + 'signature': instance.signature, + 'author': instance.author, + }; diff --git a/lib/models/release.dart b/lib/models/release.dart index 506e6a1..8de2e58 100644 --- a/lib/models/release.dart +++ b/lib/models/release.dart @@ -1,29 +1,22 @@ import 'package:flutter_data/flutter_data.dart'; -import 'package:ndk/ndk.dart' as ndk; +import 'package:json_annotation/json_annotation.dart'; +import 'package:purplebase/purplebase.dart'; +import 'package:zapstore/models/app.dart'; 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(); - - String get identifier => tagMap['i']!.first; - - factory Release.fromMapFactory(Map map) { - final m = Release.fromMap(map); - m.artifacts = HasMany.fromJson(map['artifacts']); - return m; - } - +@JsonSerializable() +@DataAdapter([NostrAdapter, ReleaseAdapter]) +class Release extends ZapstoreEvent with BaseRelease { late final HasMany artifacts; + late final BelongsTo app; } -mixin ReleaseAdapter on RemoteAdapter { +mixin ReleaseAdapter on Adapter { @override - Future> deserialize(Object? data) { + DeserializedData deserialize(Object? data, {String? key}) { final list = data is Iterable ? data : [data as Map]; for (final e in list) { final map = e as Map; @@ -31,9 +24,15 @@ mixin ReleaseAdapter on RemoteAdapter { .where((e) => e.first == 'e') .map((e) => e[1].toString()); map['artifacts'] = eTags.toList(); + final appIdentifier = + map['tags'].firstWhere((t) => t.first == 'd')[1].split('@').first; + map['app'] = (adapters['apps'] as Adapter) + .findAllLocal() + .where((a) => a.identifier == appIdentifier) + // ignore: invalid_use_of_visible_for_testing_member + .safeFirst + ?.id; } - final result = super.deserialize(data); - - return result; + return super.deserialize(data); } } diff --git a/lib/models/release.g.dart b/lib/models/release.g.dart index 1dcab1b..14c7ae3 100644 --- a/lib/models/release.g.dart +++ b/lib/models/release.g.dart @@ -3,12 +3,12 @@ part of 'release.dart'; // ************************************************************************** -// RepositoryGenerator +// AdapterGenerator // ************************************************************************** // ignore_for_file: non_constant_identifier_names, duplicate_ignore -mixin $ReleaseLocalAdapter on LocalAdapter { +mixin _$ReleaseAdapter on Adapter { static final Map _kReleaseRelationshipMetas = { 'artifacts': RelationshipMeta( name: 'artifacts', @@ -16,6 +16,13 @@ mixin $ReleaseLocalAdapter on LocalAdapter { type: 'fileMetadata', kind: 'HasMany', instance: (_) => (_ as Release).artifacts, + ), + 'app': RelationshipMeta( + name: 'app', + inverseName: 'releases', + type: 'apps', + kind: 'BelongsTo', + instance: (_) => (_ as Release).app, ) }; @@ -24,45 +31,73 @@ mixin $ReleaseLocalAdapter on LocalAdapter { _kReleaseRelationshipMetas; @override - Release deserialize(map) { + Release deserializeLocal(map, {String? key}) { map = transformDeserialize(map); - return Release.fromMapFactory(map); + return internalWrapStopInit(() => _$ReleaseFromJson(map), key: key); } @override - Map serialize(model, {bool withRelationships = true}) { - final map = model.toMap(); + Map serializeLocal(model, {bool withRelationships = true}) { + final map = _$ReleaseToJson(model); return transformSerialize(map, withRelationships: withRelationships); } } final _releasesFinders = {}; -// ignore: must_be_immutable -class $ReleaseHiveLocalAdapter = HiveLocalAdapter - with $ReleaseLocalAdapter; +class $ReleaseAdapter = Adapter + with _$ReleaseAdapter, NostrAdapter, ReleaseAdapter; -class $ReleaseRemoteAdapter = RemoteAdapter - with NostrAdapter, ReleaseAdapter; +final releasesAdapterProvider = Provider>( + (ref) => $ReleaseAdapter(ref, InternalHolder(_releasesFinders))); -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 ReleaseAdapterX on Adapter { + NostrAdapter get nostrAdapter => this as NostrAdapter; + ReleaseAdapter get releaseAdapter => this as ReleaseAdapter; } extension ReleaseRelationshipGraphNodeX on RelationshipGraphNode { RelationshipGraphNode get artifacts { - final meta = $ReleaseLocalAdapter._kReleaseRelationshipMetas['artifacts'] + final meta = _$ReleaseAdapter._kReleaseRelationshipMetas['artifacts'] as RelationshipMeta; return meta.clone( parent: this is RelationshipMeta ? this as RelationshipMeta : null); } + + RelationshipGraphNode get app { + final meta = _$ReleaseAdapter._kReleaseRelationshipMetas['app'] + as RelationshipMeta; + return meta.clone( + parent: this is RelationshipMeta ? this as RelationshipMeta : null); + } } + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Release _$ReleaseFromJson(Map json) => Release() + ..id = json['id'] + ..pubkey = json['pubkey'] as String + ..createdAt = DateTime.parse(json['createdAt'] as String) + ..content = json['content'] as String + ..kind = json['kind'] as int + ..tags = (json['tags'] as List) + .map((e) => (e as List).map((e) => e as String).toList()) + .toList() + ..signature = json['signature'] as String? + ..artifacts = + HasMany.fromJson(json['artifacts'] as Map) + ..app = BelongsTo.fromJson(json['app'] as Map); + +Map _$ReleaseToJson(Release instance) => { + 'id': instance.id, + 'pubkey': instance.pubkey, + 'createdAt': instance.createdAt.toIso8601String(), + 'content': instance.content, + 'kind': instance.kind, + 'tags': instance.tags, + 'signature': instance.signature, + 'artifacts': instance.artifacts, + 'app': instance.app, + }; diff --git a/lib/models/user.dart b/lib/models/user.dart index 826cdda..7868626 100644 --- a/lib/models/user.dart +++ b/lib/models/user.dart @@ -1,23 +1,19 @@ import 'package:equatable/equatable.dart'; import 'package:flutter_data/flutter_data.dart'; -import 'package:ndk/ndk.dart' as ndk; +import 'package:json_annotation/json_annotation.dart'; +import 'package:purplebase/purplebase.dart'; part 'user.g.dart'; -abstract class BaseEvent> = ndk.Event +abstract class ZapstoreEvent> = BaseEvent with DataModelMixin; -@DataRepository([NostrAdapter, UserAdapter], - fromJson: 'User.fromMapFactory(map)', toJson: 'model.toMap()') -class User extends BaseEvent with ndk.User, EquatableMixin { +@JsonSerializable() +@DataAdapter([NostrAdapter, UserAdapter]) +class User extends ZapstoreEvent with BaseUser, EquatableMixin { + User(); User.fromMap(super.map) : super.fromMap(); - factory User.fromMapFactory(Map map) { - final user = User.fromMap(map); - user.following = HasMany.fromJson(map['following']); - return user; - } - @DataRelationship(inverse: 'followers') late final HasMany following; @DataRelationship(inverse: 'following') @@ -29,14 +25,14 @@ class User extends BaseEvent with ndk.User, EquatableMixin { mixin UserAdapter on NostrAdapter { @override - Future> deserialize(Object? data) async { + DeserializedData deserialize(Object? data, {String? key}) { final users = []; final list = data is Iterable ? data : [data as Map]; for (final e in list) { final map = e as Map; if (map['kind'] == 3) { for (final [_, id, ..._] in map['tags'] as Iterable) { - users.add(User.fromMapFactory({ + users.add(User.fromMap({ 'id': id, 'content': '', 'pubkey': id, @@ -57,7 +53,7 @@ mixin UserAdapter on NostrAdapter { if (users.isNotEmpty) { map['following'] = users.map((e) => e.id).toList(); } - final data0 = await super.deserialize(map); + final data0 = super.deserialize(map); final user = data0.model; if (user != null) { users.add(user); @@ -70,86 +66,80 @@ mixin UserAdapter on NostrAdapter { @override Future findOne(Object id, - {bool? remote, - bool? background, + {bool remote = true, + bool background = false, Map? params, Map? headers, OnSuccessOne? onSuccess, OnErrorOne? onError, DataRequestLabel? label}) async { - if (remote == false) { - final key = graph.getKeyForId(internalType, id.toString()); - return localAdapter.findOne(key!); + String? publicKey; + try { + publicKey = '$id'.hexKey; + } catch (_) { + String username; + String domain; + try { + [username, domain] = id.toString().split('@'); + } catch (_) { + return null; + } + publicKey = await sendRequest( + Uri.parse('https://$domain/.well-known/nostr.json?name=$username'), + onSuccess: (response, label) { + return (response.body as Map)['names']?[username]; + }, + ); } - final req = ndk.Req( - authors: {id.toString()}, + if (publicKey == null) { + return null; + } + + final req = RelayRequest( + authors: {publicKey}, kinds: {kind, ...?params?.remove('kinds')}, tags: params ?? {}, ); - print('[findOne] req $req'); - final result = await notifier.query(req); - final data = await deserialize(result); - - for (final m in data.models) { - print('saving model of kind ${m.kind}: ${m.content}'); - m.init().saveLocal(); - } - return data.models.firstWhere((e) => e.id == id); + final result = + await notifier.query(req, relayUrls: ['wss://relay.nostr.band']); + final data = deserialize(result); + return data.model?..saveLocal(); } } -mixin NostrAdapter> on RemoteAdapter { - // void Function()? _closeSub; - late final ndk.FrameNotifier notifier; - - @override - 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(); - // } +mixin NostrAdapter> on Adapter { + late final RelayMessageNotifier notifier = + ref.read(relayMessageNotifierProvider.notifier); Map kindType = { 0: 'users', 3: 'users', 1063: 'fileMetadata', 30063: 'releases', + 32267: 'apps' }; @override - Future> deserialize(Object? data) async { + DeserializedData deserialize(Object? data, {String? key}) { final list = data is Iterable ? data : [data as Map]; final models = []; final included = []; for (final e in list) { final map = e as Map; + map['createdAt'] = + DateTime.fromMillisecondsSinceEpoch(map['created_at'] * 1000) + .toIso8601String(); final kind = map['kind'] as int; final xType = kindType[kind]; if (xType != null) { if (xType == internalType) { - final newData = await super.deserialize(map); + final newData = super.deserialize(map); models.addAll(newData.models as Iterable); } else { - final newData = await adapters[xType]!.deserialize(map); + final newData = adapters[xType]!.deserialize(map); included.addAll(newData.models as Iterable); } } @@ -171,22 +161,16 @@ mixin NostrAdapter> on RemoteAdapter { OnErrorAll? onError, DataRequestLabel? label}) async { if (remote == false) { - return localAdapter.findAll(); + return findAllLocal(); } - final req = ndk.Req( + final req = RelayRequest( kinds: {kind, ...?params?.remove('kinds')}, tags: params ?? {}, ); - print('req $req'); final result = await notifier.query(req); - final data = await deserialize(result); - print('req result ${data.models.length}'); - for (final m in data.models) { - print('saving model of kind ${m.kind}: ${m.content}'); - m.init().saveLocal(); - } - return data.models; + final deserialized = await deserializeAsync(result, save: true); + return deserialized.models; } } diff --git a/lib/models/user.g.dart b/lib/models/user.g.dart index 86385c9..0d1c1ea 100644 --- a/lib/models/user.g.dart +++ b/lib/models/user.g.dart @@ -3,12 +3,12 @@ part of 'user.dart'; // ************************************************************************** -// RepositoryGenerator +// AdapterGenerator // ************************************************************************** // ignore_for_file: non_constant_identifier_names, duplicate_ignore -mixin $UserLocalAdapter on LocalAdapter { +mixin _$UserAdapter on Adapter { static final Map _kUserRelationshipMetas = { 'following': RelationshipMeta( name: 'following', @@ -31,50 +31,71 @@ mixin $UserLocalAdapter on LocalAdapter { _kUserRelationshipMetas; @override - User deserialize(map) { + User deserializeLocal(map, {String? key}) { map = transformDeserialize(map); - return User.fromMapFactory(map); + return internalWrapStopInit(() => _$UserFromJson(map), key: key); } @override - Map serialize(model, {bool withRelationships = true}) { - final map = model.toMap(); + Map serializeLocal(model, {bool withRelationships = true}) { + final map = _$UserToJson(model); return transformSerialize(map, withRelationships: withRelationships); } } final _usersFinders = {}; -// ignore: must_be_immutable -class $UserHiveLocalAdapter = HiveLocalAdapter with $UserLocalAdapter; +class $UserAdapter = Adapter + with _$UserAdapter, NostrAdapter, UserAdapter; -class $UserRemoteAdapter = RemoteAdapter - with NostrAdapter, UserAdapter; +final usersAdapterProvider = Provider>( + (ref) => $UserAdapter(ref, InternalHolder(_usersFinders))); -final internalUsersRemoteAdapterProvider = Provider>( - (ref) => $UserRemoteAdapter( - $UserHiveLocalAdapter(ref), InternalHolder(_usersFinders))); - -final usersRepositoryProvider = - Provider>((ref) => Repository(ref)); - -extension UserDataRepositoryX on Repository { - NostrAdapter get nostrAdapter => remoteAdapter as NostrAdapter; - UserAdapter get userAdapter => remoteAdapter as UserAdapter; +extension UserAdapterX on Adapter { + NostrAdapter get nostrAdapter => this as NostrAdapter; + UserAdapter get userAdapter => this as UserAdapter; } extension UserRelationshipGraphNodeX on RelationshipGraphNode { RelationshipGraphNode get following { - final meta = $UserLocalAdapter._kUserRelationshipMetas['following'] + final meta = _$UserAdapter._kUserRelationshipMetas['following'] as RelationshipMeta; return meta.clone( parent: this is RelationshipMeta ? this as RelationshipMeta : null); } RelationshipGraphNode get followers { - final meta = $UserLocalAdapter._kUserRelationshipMetas['followers'] + final meta = _$UserAdapter._kUserRelationshipMetas['followers'] as RelationshipMeta; return meta.clone( parent: this is RelationshipMeta ? this as RelationshipMeta : null); } } + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +User _$UserFromJson(Map json) => User() + ..id = json['id'] + ..pubkey = json['pubkey'] as String + ..createdAt = DateTime.parse(json['createdAt'] as String) + ..content = json['content'] as String + ..kind = json['kind'] as int + ..tags = (json['tags'] as List) + .map((e) => (e as List).map((e) => e as String).toList()) + .toList() + ..signature = json['signature'] as String? + ..following = + HasMany.fromJson(json['following'] as Map); + +Map _$UserToJson(User instance) => { + 'id': instance.id, + 'pubkey': instance.pubkey, + 'createdAt': instance.createdAt.toIso8601String(), + 'content': instance.content, + 'kind': instance.kind, + 'tags': instance.tags, + 'signature': instance.signature, + 'following': instance.following, + }; diff --git a/lib/screens/app_detail_screen.dart b/lib/screens/app_detail_screen.dart index 740723a..4fa1a45 100644 --- a/lib/screens/app_detail_screen.dart +++ b/lib/screens/app_detail_screen.dart @@ -1,212 +1,172 @@ -import 'package:async_button_builder/async_button_builder.dart'; -import 'package:expansion_tile_card/expansion_tile_card.dart'; +// ignore_for_file: prefer_const_literals_to_create_immutables + +import 'dart:convert'; + +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:carousel_slider/carousel_slider.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:flutter_styled_toast/flutter_styled_toast.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:ndk/ndk.dart' as ndk; -import 'package:url_launcher/url_launcher_string.dart'; -import 'package:zapstore/main.data.dart'; -import 'package:zapstore/models/file_metadata.dart'; -import 'package:zapstore/models/release.dart'; +import 'package:zapstore/models/app.dart'; import 'package:zapstore/models/user.dart'; -import 'package:zapstore/screens/profile_screen.dart'; +import 'package:zapstore/widgets/card.dart'; +import 'package:zapstore/widgets/pill_widget.dart'; + +final List imgList = [ + 'https://images.unsplash.com/photo-1520342868574-5fa3804e551c?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=6ff92caffcdd63681a35134a6770ed3b&auto=format&fit=crop&w=1951&q=80', + 'https://images.unsplash.com/photo-1522205408450-add114ad53fe?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=368f45b0888aeb0b7b08e3a1084d3ede&auto=format&fit=crop&w=1950&q=80', + 'https://images.unsplash.com/photo-1519125323398-675f0ddb6308?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=94a1e718d89ca60a6337a6008341ca50&auto=format&fit=crop&w=1950&q=80', + 'https://images.unsplash.com/photo-1523205771623-e0faa4d2813d?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=89719a0d55dd05e2deae4120227e6efc&auto=format&fit=crop&w=1953&q=80', + 'https://images.unsplash.com/photo-1508704019882-f9cf40e475b4?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=8c6e5e3aba713b17aa1fe71ab4f0ae5b&auto=format&fit=crop&w=1352&q=80', + 'https://images.unsplash.com/photo-1519985176271-adb1088fa94c?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=a0c8d632e977f94e5d312d9893258f59&auto=format&fit=crop&w=1355&q=80' +]; class AppDetailScreen extends HookConsumerWidget { - final Release release; + final App app; const AppDetailScreen({ - required this.release, + required this.app, super.key, }); @override Widget build(BuildContext context, WidgetRef ref) { - final artifacts = release.artifacts.toList(); - return ListView.builder( - shrinkWrap: true, - itemCount: release.artifacts.length, - itemBuilder: (context, index) { - final event = artifacts[index]; - return CardWidget(fileMetadata: event); - }, - ); - } -} + final user = User.fromMap({ + 'kind': 0, + 'created_at': 112899202, + 'pubkey': + 'a9e95a4eb32b55441b222ae5674f063949bfd0759b82deb03d7cd262e82d5626', + 'content': jsonEncode( + {'name': 'test', 'picture': 'https://picsum.photos/200/300'}), + 'tags': [], + }); -class CardWidget extends HookConsumerWidget { - final FileMetadata fileMetadata; - - const CardWidget({super.key, required this.fileMetadata}); - - User? get author => fileMetadata.author.value; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final currentUser = ref.read(loggedInUser); - final fetchUser = useMemoized(() => ref.users.findOne(author!.id!)); - useFuture(fetchUser); - - final isWebOfTrust = [...?currentUser?.following.toList()] - .contains(fileMetadata.author.value); - - return ExpansionTileCard( - leading: ClipRRect( - borderRadius: BorderRadius.circular(15.0), - child: Image.network( - fileMetadata.release.value!.image, - width: 80, - height: 80, - fit: BoxFit.cover, - errorBuilder: (context, err, _) => Text(err.toString()), - ), - ), - onExpansionChanged: (_) { - ref.users.findOne(author!.id!); - }, - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - fileMetadata.content, - overflow: TextOverflow.ellipsis, - maxLines: 1, - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 26), - ), - SizedBox(height: 10), - Row( - children: [ - CircleAvatar( - backgroundImage: NetworkImage(author?.profilePicture ?? ''), - maxRadius: 10, - ), - SizedBox(width: 10), - Text( - '${author?.name ?? author?.id}', - style: TextStyle(fontSize: 14), - ), - ], - ) - ], - ), + return Column( children: [ - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (isWebOfTrust) - Text( - 'You are following this author!', - style: TextStyle(color: Colors.green), - ), - if (!isWebOfTrust) + Expanded( + child: CustomScrollView( + slivers: [ + // SliverAppBar( + // pinned: true, + // leading: IconButton( + // icon: Icon(Icons.arrow_back), + // onPressed: () { + // context.pop(); + // }, + // ), + // ), + SliverList( + delegate: SliverChildListDelegate([ Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, children: [ - Text('Author is not in your web of trust', - style: TextStyle(color: Colors.red)), - GestureDetector( - onTap: () => launchUrlString( - 'https://primal.net/p/${author?.id.toString().npub}', + CircularImage( + url: 'https://picsum.photos/200/200', + size: 80, + radius: 25, + ), + Gap(16), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AutoSizeText( + 'Mutiny Wallet', + minFontSize: 16, + style: TextStyle( + fontSize: 28, fontWeight: FontWeight.bold), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Gap(8), + PillWidget(text: '0.5.6'), + ], ), - child: Text( - 'see profile', - style: TextStyle( - color: Colors.blue, - decoration: TextDecoration.underline, - ), - ), - ) + ), ], ), - AsyncButtonBuilder( - loadingWidget: const Padding( - padding: EdgeInsets.all(8.0), - child: SizedBox( - height: 16.0, - width: 16.0, - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(Colors.white), - ), + Gap(16), + CarouselSlider( + options: CarouselOptions( + enableInfiniteScroll: false, ), + items: imgList + .map((item) => Padding( + padding: const EdgeInsets.only(right: 10), + child: Image.network(item, + fit: BoxFit.cover, width: 1000), + )) + .toList(), ), - successWidget: const Padding( - padding: EdgeInsets.all(4.0), - child: Icon( - Icons.check, - color: Colors.purpleAccent, + Divider(height: 24), + MarkdownBody( + styleSheet: MarkdownStyleSheet( + h1: TextStyle(fontWeight: FontWeight.bold), + h2: TextStyle(fontWeight: FontWeight.bold), + p: TextStyle(fontSize: 16, fontWeight: FontWeight.w300), ), + selectable: false, + data: + 'Lorem ipsum dolor sit amet\n - consectetur adipisicing elit\n - sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, ', ), - onPressed: () async { - try { - await fileMetadata.install(); - showToastWidget( - Text( - 'Success!', - style: TextStyle(fontSize: 20), + Gap(10), + AuthorContainer(user: user, text: 'Built by', oneLine: false), + Gap(10), + Divider(height: 24), + Container( + padding: EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.grey[900], + borderRadius: BorderRadius.circular(10), + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [Text('Source'), Text('github link')], ), - // ignore: use_build_context_synchronously - context: context, - position: StyledToastPosition.center, - animation: StyledToastAnimation.scale, - reverseAnimation: StyledToastAnimation.fade, - duration: Duration(seconds: 4), - animDuration: Duration(seconds: 1), - curve: Curves.elasticOut, - reverseCurve: Curves.linear); - } on Exception catch (e) { - showToastWidget( - Text( - 'Error! $e', - style: TextStyle(fontSize: 20), + ), + Padding( + padding: const EdgeInsets.all(8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [Text('Github stars'), Text('456')], ), - // ignore: use_build_context_synchronously - context: context, - position: StyledToastPosition.center, - animation: StyledToastAnimation.scale, - reverseAnimation: StyledToastAnimation.fade, - duration: Duration(seconds: 4), - animDuration: Duration(seconds: 1), - curve: Curves.elasticOut, - reverseCurve: Curves.linear); - } - }, - loadingSwitchInCurve: Curves.bounceInOut, - loadingTransitionBuilder: (child, animation) { - return SlideTransition( - position: Tween( - begin: const Offset(0, 1.0), - end: const Offset(0, 0), - ).animate(animation), - child: child, - ); - }, - builder: (context, child, callback, state) { - return Material( - color: state.maybeWhen( - success: () => Colors.purple[100], - orElse: () => Colors.black12, - ), - // This prevents the loading indicator showing below the - // button - clipBehavior: Clip.hardEdge, - shape: const StadiumBorder(), - child: InkWell( - onTap: callback, - child: child, - ), - ); - }, - child: Padding( - padding: const EdgeInsets.all(12.0), - child: const Icon(Icons.download), - ), + ), + Padding( + padding: const EdgeInsets.all(8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [Text('Github forks'), Text('3')], + ), + ) + ], + ), + ) + ]), + ), + ], + ), + ), + Container( + height: 50.0, + padding: EdgeInsets.all(8), + // color: Colors.blue, + child: Center( + child: Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + minimumSize: const Size.fromHeight(50), ), - ], + onPressed: () {}, + child: const Text('Install'), + ), ), ), ), diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index bade243..60dc1d5 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -1,68 +1,109 @@ +// ignore_for_file: prefer_const_literals_to_create_immutables + import 'package:flutter/material.dart'; import 'package:flutter_data/flutter_data.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:zapstore/main.data.dart'; +import 'package:zapstore/models/app.dart'; import 'package:zapstore/screens/app_detail_screen.dart'; - -import '../main.data.dart'; +import 'package:zapstore/widgets/card.dart'; +import 'package:zapstore/widgets/user_avatar.dart'; const kAndroidMimeType = 'application/vnd.android.package-archive'; +final searchStateProvider = + StateProvider>>((_) => DataState([])); + +final screenInfoProvider = StateProvider( + (ref) => 'Welcome to zap.store!\nUse the search bar to find apps'); + class SearchScreen extends HookConsumerWidget { const SearchScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final searchState = useState(null); - final labelState = useState(null); + final state = ref.watch(searchStateProvider); - final value = ref.fileMetadata.watchAll(remote: false); - final artifacts = value.model - .where((a) => a.release.isPresent && a.mimeType == kAndroidMimeType) - .where((a) => searchState.value != null - ? a.release.value!.title - .toLowerCase() - .contains(searchState.value!.toLowerCase()) - : true) - .toList(); + if (state.isLoading) { + return Center(child: CircularProgressIndicator()); + } - return Padding( - padding: const EdgeInsets.all(18.0), - child: Column( - children: [ - SearchBar( - elevation: const MaterialStatePropertyAll(2.2), - onSubmitted: (query) async { - searchState.value = query; + final apps = state.model; - await ref.releases.findAll(params: { - 'since': 1708658457, - }); - await ref.fileMetadata.findAll( - params: { - // 'kinds': {1063}, - // 'limit': 20, - // 'search': query, - '#m': [kAndroidMimeType], - 'since': 1708658457, + return Column( + children: [ + Row( + children: [ + GestureDetector( + onTap: () => Scaffold.of(context).openDrawer(), + child: UserAvatar(), + ), + Gap(20), + Expanded( + child: SearchBar( + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(28), + ), + ), + leading: Padding( + padding: const EdgeInsets.fromLTRB(10, 8, 6, 6), + child: Icon( + Icons.search, + color: Colors.blueGrey, + size: 20, + ), + ), + hintText: 'Search for apps', + hintStyle: MaterialStateProperty.all( + TextStyle(color: Colors.grey[600])), + autoFocus: true, + elevation: MaterialStatePropertyAll(2.2), + onSubmitted: (query) async { + ref.read(screenInfoProvider.notifier).state = 'No items'; + final apps = [App(), App(), App(), App()]; + // await ref.apps.findAll(params: {'search': query}); + + // final releases = await ref.releases.findAll( + // params: {'#i': apps.map((a) => a.identifier).toSet()}); + + // await ref.fileMetadata.findAll( + // params: { + // 'ids': + // releases.map((r) => r.tagMap['e']!.first).toSet(), + // '#m': [kAndroidMimeType], + // }, + // ); + ref.read(searchStateProvider.notifier).state = + DataState(apps); }, - label: labelState.value, - ); - }, + ), + ), + ], + ), + Gap(10), + if (apps.isEmpty) + Expanded( + child: Center( + child: Text( + ref.read(screenInfoProvider), + textAlign: TextAlign.center, + ), + ), ), - const SizedBox(height: 20), + if (apps.isNotEmpty) Expanded( child: ListView.builder( shrinkWrap: true, - itemCount: artifacts.length, + itemCount: apps.length, itemBuilder: (context, index) { - final event = artifacts[index]; - return CardWidget(fileMetadata: event); + final app = apps[index]; + return CardWidget(app: app); }, ), ), - ], - ), + ], ); } } diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart new file mode 100644 index 0000000..125d430 --- /dev/null +++ b/lib/screens/settings_screen.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class SettingsScreen extends HookConsumerWidget { + const SettingsScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final controller = useTextEditingController(); + return Expanded( + child: TextField( + controller: controller, + autofocus: true, + maxLines: 10, + ), + ); + } +} diff --git a/lib/screens/updates_screen.dart b/lib/screens/updates_screen.dart new file mode 100644 index 0000000..5c49aff --- /dev/null +++ b/lib/screens/updates_screen.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class UpdatesScreen extends HookConsumerWidget { + const UpdatesScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final controller = useTextEditingController(); + return Expanded( + child: TextField( + controller: controller, + autofocus: true, + maxLines: 10, + ), + ); + } +} diff --git a/lib/services/session_service.dart b/lib/services/session_service.dart new file mode 100644 index 0000000..49f2342 --- /dev/null +++ b/lib/services/session_service.dart @@ -0,0 +1,4 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:zapstore/models/user.dart'; + +final loggedInUser = StateProvider((ref) => null); diff --git a/lib/utils/extensions.dart b/lib/utils/extensions.dart new file mode 100644 index 0000000..c7ca9e4 --- /dev/null +++ b/lib/utils/extensions.dart @@ -0,0 +1,5 @@ +import 'package:flutter/material.dart'; + +extension ContextX on BuildContext { + ThemeData get theme => Theme.of(this); +} diff --git a/lib/screens/profile_screen.dart b/lib/widgets/app_drawer.dart similarity index 50% rename from lib/screens/profile_screen.dart rename to lib/widgets/app_drawer.dart index 9f520f4..ec6cc61 100644 --- a/lib/screens/profile_screen.dart +++ b/lib/widgets/app_drawer.dart @@ -1,14 +1,16 @@ +import 'package:android_package_manager/android_package_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:ndk/ndk.dart' as ndk; import 'package:zapstore/main.data.dart'; -import 'package:zapstore/models/user.dart'; +import 'package:zapstore/services/session_service.dart'; +import 'package:zapstore/widgets/card.dart'; -final loggedInUser = StateProvider((ref) => null); +class AppDrawer extends HookConsumerWidget { + AppDrawer({super.key}); -class ProfileScreen extends HookConsumerWidget { - const ProfileScreen({super.key}); + // final packageManager = AndroidPackageManager(); @override Widget build(BuildContext context, WidgetRef ref) { @@ -19,19 +21,30 @@ class ProfileScreen extends HookConsumerWidget { padding: const EdgeInsets.all(12.0), child: Column( children: [ - if (u2 == null) TextField(controller: controller), - SizedBox(height: 10), + // Text('npub or NIP-05'), + if (u2 == null) TextField(autocorrect: false, controller: controller), + Gap(5), if (u2 == null) ElevatedButton( onPressed: () async { - final pubkey = controller.text.hexKey; ref.read(loggedInUser.notifier).state = - await ref.users.findOne(pubkey, params: { - 'kinds': {3} - }); + await ref.users.findOne(controller.text); }, - child: Text('Log in with npub'), + child: Text('Log in'), ), + ElevatedButton( + onPressed: () async { + ref.read(loggedInUser.notifier).state = null; + controller.clear(); + // final as = await packageManager.getInstalledApplications(); + // for (final a in as!) { + // if (!a.packageName!.contains('android')) { + // print(a.packageName); + // } + // } + }, + child: Text('check'), + ), if (u2 != null) Expanded( child: Column( @@ -39,17 +52,18 @@ class ProfileScreen extends HookConsumerWidget { Card( child: ListTile( dense: true, - leading: CircleAvatar( - backgroundImage: NetworkImage(u2.profilePicture ?? ''), + leading: CircularImage( + size: 20, + url: u2.avatarUrl, ), title: Text( u2.name ?? u2.id?.toString() ?? '', ), - subtitle: Text('following ${u2.following.length}'), + // subtitle: Text('following ${u2.following.length}'), ), ), ElevatedButton( - onPressed: () { + onPressed: () async { ref.read(loggedInUser.notifier).state = null; controller.clear(); }, diff --git a/lib/widgets/card.dart b/lib/widgets/card.dart new file mode 100644 index 0000000..d9714f5 --- /dev/null +++ b/lib/widgets/card.dart @@ -0,0 +1,170 @@ +import 'dart:convert'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:zapstore/models/app.dart'; +import 'package:zapstore/models/user.dart'; +import 'package:zapstore/widgets/pill_widget.dart'; + +class CardWidget extends HookConsumerWidget { + final App app; + + const CardWidget({super.key, required this.app}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final user = User.fromMap({ + 'kind': 0, + 'created_at': 112899202, + 'pubkey': + 'a9e95a4eb32b55441b222ae5674f063949bfd0759b82deb03d7cd262e82d5626', + 'content': jsonEncode( + {'name': 'test', 'picture': 'https://picsum.photos/200/300'}), + 'tags': [], + }); + + return Card( + margin: EdgeInsets.only(top: 20), + child: GestureDetector( + onTap: () => context.go('/details', extra: app), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + CircularImage( + url: 'https://picsum.photos/200/200', + size: 80, + radius: 25, + ), + Gap(16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Mutiny Wallet - Mutiny Wallet - Muti', + // '${app.name} - ${app.name}', + style: + TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis, + maxLines: 1, + softWrap: true, + ), + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + 'Best web-based Lightning and Ecash wallet web-based Lightning and Ecash wallet', + style: TextStyle( + fontSize: 14, fontWeight: FontWeight.w300), + overflow: TextOverflow.ellipsis, + maxLines: 2, + softWrap: true, + ), + ), + Gap(10), + AuthorContainer(user: user), + AuthorContainer(user: user), + Gap(16), + TagsContainer(), + ], + ), + ), + ], + ), + ), + ), + ); + } +} + +class TagsContainer extends StatelessWidget { + const TagsContainer({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: const [ + PillWidget(text: 'nostr'), + Gap(6), + PillWidget(text: 'wallet'), + ], + ); + } +} + +class AuthorContainer extends StatelessWidget { + final User user; + final String text; + final bool oneLine; + const AuthorContainer({ + super.key, + required this.user, + this.text = 'Signed by', + this.oneLine = true, + }); + + String get name => user.name ?? user.npub; + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only(top: 4, bottom: 4), + child: Row( + children: [ + CircularImage(url: user.avatarUrl, size: oneLine ? 22 : 46), + Gap(10), + if (oneLine) Text('$text $name'), + if (!oneLine) + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(text), + Padding( + padding: const EdgeInsets.only(left: 1), + child: + Text(name, style: TextStyle(fontWeight: FontWeight.bold)), + ), + ], + ) + ], + ), + ); + } +} + +class CircularImage extends StatelessWidget { + const CircularImage({ + super.key, + this.url, + this.size = 22, + this.radius = 60, + }); + + final String? url; + final int size; + final int radius; + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(radius.toDouble()), + child: Container( + width: size.toDouble(), + height: size.toDouble(), + color: Colors.grey[850], + child: CachedNetworkImage( + imageUrl: url ?? '', + fit: BoxFit.cover, + ), + ), + ); + } +} diff --git a/lib/widgets/pill_widget.dart b/lib/widgets/pill_widget.dart new file mode 100644 index 0000000..9796dbd --- /dev/null +++ b/lib/widgets/pill_widget.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +class PillWidget extends StatelessWidget { + final String text; + const PillWidget({super.key, required this.text}); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.blue, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + text, + style: TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.bold, + ), + ), + ); + } +} diff --git a/lib/widgets/user_avatar.dart b/lib/widgets/user_avatar.dart new file mode 100644 index 0000000..e5252a2 --- /dev/null +++ b/lib/widgets/user_avatar.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:zapstore/services/session_service.dart'; + +class UserAvatar extends HookConsumerWidget { + const UserAvatar({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final user = ref.watch(loggedInUser); + return CircleAvatar( + radius: 20, + // backgroundImage: AssetImage('assets/images/logo.png'), + backgroundColor: Colors.grey[850], + foregroundImage: user != null ? NetworkImage(user.avatarUrl!) : null, + ); + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index de7c877..854280a 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -18,6 +19,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) screen_retriever_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); + g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); + sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index ea8c597..ab7d522 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST handy_window screen_retriever + sqlite3_flutter_libs url_launcher_linux window_manager ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 5730c07..92d0ff2 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,12 +7,16 @@ import Foundation import path_provider_foundation import screen_retriever +import sqflite +import sqlite3_flutter_libs import url_launcher_macos import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 3a96bf1..85db780 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -5,6 +5,26 @@ PODS: - FlutterMacOS - screen_retriever (0.0.1): - FlutterMacOS + - sqflite (0.0.3): + - Flutter + - FlutterMacOS + - "sqlite3 (3.45.3+1)": + - "sqlite3/common (= 3.45.3+1)" + - "sqlite3/common (3.45.3+1)" + - "sqlite3/fts5 (3.45.3+1)": + - sqlite3/common + - "sqlite3/perf-threadsafe (3.45.3+1)": + - sqlite3/common + - "sqlite3/rtree (3.45.3+1)": + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): + - FlutterMacOS + - "sqlite3 (~> 3.45.3+1)" + - sqlite3/fts5 + - sqlite3/perf-threadsafe + - sqlite3/rtree + - url_launcher_macos (0.0.1): + - FlutterMacOS - window_manager (0.2.0): - FlutterMacOS @@ -12,8 +32,15 @@ DEPENDENCIES: - FlutterMacOS (from `Flutter/ephemeral`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) + - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`) + - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) +SPEC REPOS: + trunk: + - sqlite3 + EXTERNAL SOURCES: FlutterMacOS: :path: Flutter/ephemeral @@ -21,6 +48,12 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin screen_retriever: :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos + sqflite: + :path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin + sqlite3_flutter_libs: + :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos window_manager: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos @@ -28,8 +61,12 @@ SPEC CHECKSUMS: FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec + sqlite3: 02d1f07eaaa01f80a1c16b4b31dfcbb3345ee01a + sqlite3_flutter_libs: 8d204ef443cf0d5c1c8b058044eab53f3943a9c5 + url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.2 diff --git a/pubspec.lock b/pubspec.lock index ed386e1..2d6254a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -38,18 +38,18 @@ packages: dependency: transitive description: name: archive - sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" + sha256: "0763b45fa9294197a2885c8567927e2830ade852e5c896fd4ab7e0e348d0f373" url: "https://pub.dev" source: hosted - version: "3.4.10" + version: "3.5.0" args: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.5.0" async: dependency: transitive description: @@ -66,6 +66,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0+1" + auto_size_text: + dependency: "direct main" + description: + name: auto_size_text + sha256: "3f5261cd3fb5f2a9ab4e2fc3fba84fd9fcaac8821f20a1d4e71f557521b22599" + url: "https://pub.dev" + source: hosted + version: "3.0.0" bech32: dependency: transitive description: @@ -126,10 +134,10 @@ packages: dependency: "direct main" description: name: build_runner - sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" + sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.9" build_runner_core: dependency: transitive description: @@ -150,10 +158,42 @@ packages: dependency: transitive description: name: built_value - sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.9.1" + version: "8.9.2" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f" + url: "https://pub.dev" + source: hosted + version: "3.3.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "205d6a9f1862de34b93184f22b9d2d94586b2f05c581d546695e3d8f6a805cd7" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + carousel_slider: + dependency: "direct main" + description: + name: carousel_slider + sha256: "9c695cc963bf1d04a47bd6021f68befce8970bcd61d24938e1fb0918cf5d9c42" + url: "https://pub.dev" + source: hosted + version: "4.2.1" characters: dependency: transitive description: @@ -222,18 +262,26 @@ packages: dependency: "direct main" description: name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.8" dart_style: dependency: transitive description: name: dart_style - sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.6" + device_apps: + dependency: "direct main" + description: + name: device_apps + sha256: e84dc74d55749993fd671148cc0bd53096e1be0c268fc364285511b1d8a4c19b + url: "https://pub.dev" + source: hosted + version: "2.2.0" equatable: dependency: "direct main" description: @@ -287,14 +335,21 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "395d6b7831f21f3b989ebedbb785545932adb9afe2622c1ffacf7f4b53a7e544" + url: "https://pub.dev" + source: hosted + version: "3.3.2" flutter_data: dependency: "direct main" description: - name: flutter_data - sha256: ac6aba32a7c67d4f8e1b979ddb33eb3bdad3612378e54446780ca1bfc3003cab - url: "https://pub.dev" - source: hosted - version: "1.6.0" + path: "../flutter_data" + relative: true + source: path + version: "2.0.0-rc3" flutter_hooks: dependency: "direct main" description: @@ -324,14 +379,22 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_markdown: + dependency: "direct main" + description: + name: flutter_markdown + sha256: "9921f9deda326f8a885e202b1e35237eadfc1345239a0f6f0f1ff287e047547f" + url: "https://pub.dev" + source: hosted + version: "0.7.1" flutter_riverpod: dependency: transitive description: name: flutter_riverpod - sha256: "4bce556b7ecbfea26109638d5237684538d4abc509d253e6c5c4c5733b360098" + sha256: "0f1974eff5bbe774bf1d870e406fc6f29e3d6f1c46bd9c58e7172ff68a785d7d" url: "https://pub.dev" source: hosted - version: "2.4.10" + version: "2.5.1" flutter_styled_toast: dependency: "direct main" description: @@ -362,10 +425,18 @@ packages: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" + gap: + dependency: "direct main" + description: + name: gap + sha256: f19387d4e32f849394758b91377f9153a1b41d79513ef7668c088c77dbc6955d + url: "https://pub.dev" + source: hosted + version: "3.0.1" glob: dependency: transitive description: @@ -378,10 +449,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "170c46e237d6eb0e6e9f0e8b3f56101e14fb64f787016e42edd74c39cf8b176a" + sha256: b465e99ce64ba75e61c8c0ce3d87b66d8ac07f0b35d0a7e0263fcfc10f99e836 url: "https://pub.dev" source: hosted - version: "13.2.0" + version: "13.2.5" graphs: dependency: transitive description: @@ -406,22 +477,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.1" - hive: - dependency: transitive - description: - name: hive - sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" - url: "https://pub.dev" - source: hosted - version: "2.2.3" hooks_riverpod: dependency: "direct main" description: name: hooks_riverpod - sha256: "758b07eba336e3cbacbd81dba481f2228a14102083fdde07045e8514e8054c49" + sha256: "45b2030a18bcd6dbd680c2c91bc3b33e3fe7c323e3acb5ecec93a613e2fbaa8a" url: "https://pub.dev" source: hosted - version: "2.4.10" + version: "2.5.1" http: dependency: "direct main" description: @@ -490,18 +553,18 @@ packages: dependency: "direct main" description: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "4.9.0" json_serializable: dependency: "direct dev" description: name: json_serializable - sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 + sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b url: "https://pub.dev" source: hosted - version: "6.7.1" + version: "6.8.0" leak_tracker: dependency: transitive description: @@ -542,6 +605,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + markdown: + dependency: transitive + description: + name: markdown + sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051 + url: "https://pub.dev" + source: hosted + version: "7.2.2" matcher: dependency: transitive description: @@ -574,13 +645,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" - ndk: - dependency: "direct main" + octo_image: + dependency: transitive description: - path: "../ndk" - relative: true - source: path - version: "0.1.0" + name: octo_image + sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" + url: "https://pub.dev" + source: hosted + version: "2.0.0" package_config: dependency: transitive description: @@ -601,18 +673,18 @@ packages: dependency: "direct main" description: name: path_provider - sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" + sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.4" path_provider_foundation: dependency: transitive description: @@ -649,10 +721,10 @@ packages: dependency: "direct main" description: name: permission_handler - sha256: "74e962b7fad7ff75959161bb2c0ad8fe7f2568ee82621c9c2660b751146bfe44" + sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb" url: "https://pub.dev" source: hosted - version: "11.3.0" + version: "11.3.1" permission_handler_android: dependency: transitive description: @@ -665,10 +737,10 @@ packages: dependency: transitive description: name: permission_handler_apple - sha256: bdafc6db74253abb63907f4e357302e6bb786ab41465e8635f362ee71fd8707b + sha256: e9ad66020b89ff1b63908f247c2c6f931c6e62699b756ef8b3c4569350cd8662 url: "https://pub.dev" source: hosted - version: "9.4.0" + version: "9.4.4" permission_handler_html: dependency: transitive description: @@ -681,10 +753,10 @@ packages: dependency: transitive description: name: permission_handler_platform_interface - sha256: "23dfba8447c076ab5be3dee9ceb66aad345c4a648f0cac292c77b1eb0e800b78" + sha256: "48d4fcf201a1dad93ee869ab0d4101d084f49136ec82a8a06ed9cfeacab9fd20" url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.2.1" permission_handler_windows: dependency: transitive description: @@ -721,10 +793,10 @@ packages: dependency: transitive description: name: pointycastle - sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" + sha256: "79fbafed02cfdbe85ef3fd06c7f4bc2cbcba0177e61b765264853d4253b21744" url: "https://pub.dev" source: hosted - version: "3.7.4" + version: "3.9.0" pool: dependency: transitive description: @@ -749,30 +821,29 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.3" - recase: - dependency: transitive + purplebase: + dependency: "direct main" description: - name: recase - sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 - url: "https://pub.dev" - source: hosted - version: "4.1.0" - retry: - dependency: transitive - description: - name: retry - sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc" - url: "https://pub.dev" - source: hosted - version: "3.1.2" + path: "../purplebase" + relative: true + source: path + version: "0.1.0" riverpod: dependency: transitive description: name: riverpod - sha256: "548e2192eb7aeb826eb89387f814edb76594f3363e2c0bb99dd733d795ba3589" + sha256: f21b32ffd26a36555e501b04f4a5dca43ed59e16343f1a30c13632b2351dfa4d url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.5.1" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" + source: hosted + version: "0.27.7" screen_retriever: dependency: transitive description: @@ -834,6 +905,38 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: "5ce2e1a15e822c3b4bfb5400455775e421da7098eed8adc8f26298ada7c9308c" + url: "https://pub.dev" + source: hosted + version: "2.3.3" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: "1abbeb84bf2b1a10e5e1138c913123c8aa9d83cd64e5f9a0dd847b3c83063202" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqlite3_flutter_libs: + dependency: "direct main" + description: + name: sqlite3_flutter_libs + sha256: fb2a106a2ea6042fe57de2c47074cc31539a941819c91e105b864744605da3f5 + url: "https://pub.dev" + source: hosted + version: "0.5.21" stack_trace: dependency: transitive description: @@ -874,6 +977,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + url: "https://pub.dev" + source: hosted + version: "3.1.0+1" term_glyph: dependency: transitive description: @@ -910,26 +1021,26 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e" + sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e" url: "https://pub.dev" source: hosted - version: "6.2.5" + version: "6.2.6" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745 + sha256: "360a6ed2027f18b73c8d98e159dda67a61b7f2e0f6ec26e86c3ada33b0621775" url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.1" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" + sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5" url: "https://pub.dev" source: hosted - version: "6.2.4" + version: "6.2.5" url_launcher_linux: dependency: transitive description: @@ -958,10 +1069,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "3692a459204a33e04bc94f5fb91158faf4f2c8903281ddd82915adecdb1a901d" + sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.1" url_launcher_windows: dependency: transitive description: @@ -974,10 +1085,10 @@ packages: dependency: transitive description: name: uuid - sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8 + sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" url: "https://pub.dev" source: hosted - version: "4.3.3" + version: "4.4.0" vector_math: dependency: transitive description: @@ -1006,26 +1117,26 @@ packages: dependency: transitive description: name: web - sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad" + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.5.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.5" win32: dependency: transitive description: name: win32 - sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.5.0" window_manager: dependency: transitive description: @@ -1034,6 +1145,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.8" + ws: + dependency: transitive + description: + name: ws + sha256: bb40a7b9219324ac285133c6b3c11bc16015d9cd5889e6555b76bd3e3a64b3e7 + url: "https://pub.dev" + source: hosted + version: "1.0.0" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2e5ca07..8e1c8ec 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,6 +20,7 @@ version: 0.0.1 environment: sdk: '>=3.3.0 <4.0.0' + dependencies: flutter: sdk: flutter @@ -32,7 +33,6 @@ dependencies: build_runner: ^2.4.8 collection: ^1.18.0 path_provider: ^2.1.2 - ndk: ^0.0.1 expansion_tile_card: ^3.0.0 crypto: ^3.0.3 android_package_installer: ^0.0.2 @@ -45,68 +45,47 @@ dependencies: flutter_styled_toast: ^2.2.1 equatable: ^2.0.5 url_launcher: ^6.2.5 + device_apps: ^2.2.0 + purplebase: + sqlite3_flutter_libs: ^0.5.20 + carousel_slider: ^4.2.1 + flutter_markdown: ^0.7.1 + cached_network_image: ^3.3.1 + gap: ^3.0.1 + auto_size_text: ^3.0.0 dependency_overrides: android_package_installer: git: url: https://github.com/ImranR98/android_package_installer ref: main - ndk: - path: ../ndk + purplebase: + path: ../purplebase + flutter_data: + path: ../flutter_data dev_dependencies: flutter_test: sdk: flutter json_serializable: ^6.7.1 - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. flutter_lints: ^2.0.0 flutter_launcher_icons: "^0.13.1" -# The following section is specific to Flutter packages. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true assets: - assets/images/logo.png - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages + fonts: + - family: Inter + fonts: + - asset: assets/fonts/Inter-Light.ttf + weight: 300 + - asset: assets/fonts/Inter-Regular.ttf + - asset: assets/fonts/Inter-Italic.ttf + style: italic + - asset: assets/fonts/Inter-Bold.ttf + weight: 700 flutter_launcher_icons: android: "launcher_icon"