Compare commits
	
		
			3 Commits
		
	
	
		
			1fe4889460
			...
			1232318a5d
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 1232318a5d | |||
|  | 56f41b6c0e | ||
|  | 3ea717d25a | 
| @@ -1,5 +1,6 @@ | ||||
| import 'dart:convert'; | ||||
| import 'dart:developer'; | ||||
| import 'dart:io' show Platform; | ||||
|  | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| @@ -28,7 +29,10 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> { | ||||
|       final response = await client.get('/id/accounts/me'); | ||||
|       final user = SnAccount.fromJson(response.data); | ||||
|       state = AsyncValue.data(user); | ||||
|       FirebaseAnalytics.instance.setUserId(id: user.id); | ||||
|  | ||||
|       if (kIsWeb || !Platform.isLinux) { | ||||
|         FirebaseAnalytics.instance.setUserId(id: user.id); | ||||
|       } | ||||
|     } catch (error, stackTrace) { | ||||
|       if (!kIsWeb) { | ||||
|         if (error is DioException) { | ||||
| @@ -83,7 +87,9 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> { | ||||
|     final prefs = _ref.read(sharedPreferencesProvider); | ||||
|     await prefs.remove(kTokenPairStoreKey); | ||||
|     _ref.invalidate(tokenProvider); | ||||
|     FirebaseAnalytics.instance.setUserId(id: null); | ||||
|     if (kIsWeb || !Platform.isLinux) { | ||||
|       FirebaseAnalytics.instance.setUserId(id: null); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -37,6 +37,8 @@ import 'package:island/screens/creators/stickers/stickers.dart'; | ||||
| import 'package:island/screens/creators/stickers/pack_detail.dart'; | ||||
| import 'package:island/screens/stickers/sticker_marketplace.dart'; | ||||
| import 'package:island/screens/stickers/pack_detail.dart'; | ||||
| import 'package:island/screens/discovery/feeds/feed_marketplace.dart'; | ||||
| import 'package:island/screens/discovery/feeds/feed_detail.dart'; | ||||
| import 'package:island/screens/creators/poll/poll_list.dart'; | ||||
| import 'package:island/screens/creators/publishers.dart'; | ||||
| import 'package:island/screens/creators/webfeed/webfeed_list.dart'; | ||||
| @@ -546,6 +548,22 @@ final routerProvider = Provider<GoRouter>((ref) { | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     name: 'webFeedMarketplace', | ||||
|                     path: '/feeds', | ||||
|                     builder: | ||||
|                         (context, state) => const MarketplaceWebFeedsScreen(), | ||||
|                     routes: [ | ||||
|                       GoRoute( | ||||
|                         name: 'webFeedDetail', | ||||
|                         path: ':feedId', | ||||
|                         builder: (context, state) { | ||||
|                           final feedId = state.pathParameters['feedId']!; | ||||
|                           return MarketplaceWebFeedDetailScreen(id: feedId); | ||||
|                         }, | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     name: 'notifications', | ||||
|                     path: '/account/notifications', | ||||
|   | ||||
| @@ -236,6 +236,16 @@ class AccountScreen extends HookConsumerWidget { | ||||
|                 context.pushNamed('stickerMarketplace'); | ||||
|               }, | ||||
|             ), | ||||
|             ListTile( | ||||
|               minTileHeight: 48, | ||||
|               leading: const Icon(Symbols.rss_feed), | ||||
|               trailing: const Icon(Symbols.chevron_right), | ||||
|               contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|               title: Text('webFeeds').tr(), | ||||
|               onTap: () { | ||||
|                 context.pushNamed('webFeedMarketplace'); | ||||
|               }, | ||||
|             ), | ||||
|             ListTile( | ||||
|               minTileHeight: 48, | ||||
|               leading: const Icon(Symbols.star), | ||||
|   | ||||
							
								
								
									
										189
									
								
								lib/screens/discovery/feeds/feed_detail.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								lib/screens/discovery/feeds/feed_detail.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,189 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/webfeed.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| part 'feed_detail.g.dart'; | ||||
|  | ||||
| /// Provider for web feed articles content | ||||
| @riverpod | ||||
| Future<List<SnWebArticle>> marketplaceWebFeedContent( | ||||
|   Ref ref, { | ||||
|   required String feedId, | ||||
| }) async { | ||||
|   final apiClient = ref.watch(apiClientProvider); | ||||
|   final resp = await apiClient.get('/sphere/feeds/$feedId/articles'); | ||||
|   return (resp.data as List).map((e) => SnWebArticle.fromJson(e)).toList(); | ||||
| } | ||||
|  | ||||
| /// Provider for web feed subscription status | ||||
| @riverpod | ||||
| Future<bool> marketplaceWebFeedSubscription( | ||||
|   Ref ref, { | ||||
|   required String feedId, | ||||
| }) async { | ||||
|   final api = ref.watch(apiClientProvider); | ||||
|   try { | ||||
|     await api.get('/sphere/feeds/$feedId/subscription'); | ||||
|     // If not 404, consider subscribed | ||||
|     return true; | ||||
|   } on Object catch (e) { | ||||
|     // Dio error handling agnostic: treat 404 as not-subscribed, rethrow others | ||||
|     final msg = e.toString(); | ||||
|     if (msg.contains('404')) return false; | ||||
|     rethrow; | ||||
|   } | ||||
| } | ||||
|  | ||||
| class MarketplaceWebFeedDetailScreen extends HookConsumerWidget { | ||||
|   final String id; | ||||
|   const MarketplaceWebFeedDetailScreen({super.key, required this.id}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     // TODO: Need to create a web feed provider similar to stickerPackProvider | ||||
|     // For now, we'll fetch the feed directly | ||||
|     final feedContent = ref.watch( | ||||
|       marketplaceWebFeedContentProvider(feedId: id), | ||||
|     ); | ||||
|     final subscribed = ref.watch( | ||||
|       marketplaceWebFeedSubscriptionProvider(feedId: id), | ||||
|     ); | ||||
|  | ||||
|     // Subscribe to web feed | ||||
|     Future<void> subscribeToFeed() async { | ||||
|       final apiClient = ref.watch(apiClientProvider); | ||||
|       await apiClient.post('/sphere/feeds/$id/subscribe'); | ||||
|       HapticFeedback.selectionClick(); | ||||
|       ref.invalidate(marketplaceWebFeedSubscriptionProvider(feedId: id)); | ||||
|       if (!context.mounted) return; | ||||
|       showSnackBar('feedSubscribed'.tr()); | ||||
|     } | ||||
|  | ||||
|     // Unsubscribe from web feed | ||||
|     Future<void> unsubscribeFromFeed() async { | ||||
|       final apiClient = ref.watch(apiClientProvider); | ||||
|       await apiClient.delete('/sphere/feeds/$id/subscribe'); | ||||
|       HapticFeedback.selectionClick(); | ||||
|       ref.invalidate(marketplaceWebFeedSubscriptionProvider(feedId: id)); | ||||
|       if (!context.mounted) return; | ||||
|       showSnackBar('feedUnsubscribed'.tr()); | ||||
|     } | ||||
|  | ||||
|     // TODO: Replace with actual feed data provider once created | ||||
|     final dummyFeed = SnWebFeed( | ||||
|       id: id, | ||||
|       url: 'https://example.com', | ||||
|       title: 'Loading...', | ||||
|       publisherId: 'publisher-id', | ||||
|       createdAt: DateTime.now(), | ||||
|       updatedAt: DateTime.now(), | ||||
|     ); | ||||
|  | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar(title: Text(dummyFeed.title)), | ||||
|       body: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|         children: [ | ||||
|           // Feed meta | ||||
|           Column( | ||||
|             crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|             children: [ | ||||
|               Text(dummyFeed.description ?? ''), | ||||
|               Row( | ||||
|                 spacing: 4, | ||||
|                 children: [ | ||||
|                   const Icon(Symbols.rss_feed, size: 16), | ||||
|                   Text('${feedContent.value?.length ?? 0} articles'), | ||||
|                 ], | ||||
|               ).opacity(0.85), | ||||
|               Row( | ||||
|                 spacing: 4, | ||||
|                 children: [ | ||||
|                   const Icon(Symbols.link, size: 16), | ||||
|                   SelectableText(dummyFeed.url), | ||||
|                 ], | ||||
|               ).opacity(0.85), | ||||
|             ], | ||||
|           ).padding(horizontal: 24, vertical: 24), | ||||
|           const Divider(height: 1), | ||||
|           // Articles list | ||||
|           Expanded( | ||||
|             child: feedContent.when( | ||||
|               data: | ||||
|                   (articles) => RefreshIndicator( | ||||
|                     onRefresh: | ||||
|                         () => ref.refresh( | ||||
|                           marketplaceWebFeedContentProvider(feedId: id).future, | ||||
|                         ), | ||||
|                     child: ListView.builder( | ||||
|                       padding: const EdgeInsets.symmetric( | ||||
|                         horizontal: 24, | ||||
|                         vertical: 20, | ||||
|                       ), | ||||
|                       itemCount: articles.length, | ||||
|                       itemBuilder: (context, index) { | ||||
|                         final article = articles[index]; | ||||
|                         return Card( | ||||
|                           child: ListTile( | ||||
|                             title: Text(article.title), | ||||
|                             subtitle: Text(article.author ?? ''), | ||||
|                             trailing: const Icon(Symbols.open_in_new), | ||||
|                             onTap: () { | ||||
|                               // TODO: Navigate to article detail or open URL | ||||
|                             }, | ||||
|                           ), | ||||
|                         ); | ||||
|                       }, | ||||
|                     ), | ||||
|                   ), | ||||
|               error: | ||||
|                   (err, _) => | ||||
|                       Text( | ||||
|                         'Error: $err', | ||||
|                       ).textAlignment(TextAlign.center).center(), | ||||
|               loading: () => const CircularProgressIndicator().center(), | ||||
|             ), | ||||
|           ), | ||||
|           Padding( | ||||
|             padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8), | ||||
|             child: subscribed.when( | ||||
|               data: | ||||
|                   (isSubscribed) => FilledButton.icon( | ||||
|                     onPressed: | ||||
|                         isSubscribed ? unsubscribeFromFeed : subscribeToFeed, | ||||
|                     icon: Icon( | ||||
|                       isSubscribed ? Symbols.remove_circle : Symbols.add_circle, | ||||
|                     ), | ||||
|                     label: Text( | ||||
|                       isSubscribed ? 'unsubscribe'.tr() : 'subscribe'.tr(), | ||||
|                     ), | ||||
|                   ), | ||||
|               loading: | ||||
|                   () => const SizedBox( | ||||
|                     height: 32, | ||||
|                     width: 32, | ||||
|                     child: CircularProgressIndicator(strokeWidth: 2), | ||||
|                   ), | ||||
|               error: | ||||
|                   (_, _) => OutlinedButton.icon( | ||||
|                     onPressed: subscribeToFeed, | ||||
|                     icon: const Icon(Symbols.add_circle), | ||||
|                     label: Text('subscribe').tr(), | ||||
|                   ), | ||||
|             ), | ||||
|           ), | ||||
|           Gap(MediaQuery.of(context).padding.bottom), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										313
									
								
								lib/screens/discovery/feeds/feed_detail.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										313
									
								
								lib/screens/discovery/feeds/feed_detail.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,313 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'feed_detail.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$marketplaceWebFeedContentHash() => | ||||
|     r'4e65350bff4055302e15ec14266cdebb1cd89bbe'; | ||||
|  | ||||
| /// Copied from Dart SDK | ||||
| class _SystemHash { | ||||
|   _SystemHash._(); | ||||
|  | ||||
|   static int combine(int hash, int value) { | ||||
|     // ignore: parameter_assignments | ||||
|     hash = 0x1fffffff & (hash + value); | ||||
|     // ignore: parameter_assignments | ||||
|     hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); | ||||
|     return hash ^ (hash >> 6); | ||||
|   } | ||||
|  | ||||
|   static int finish(int hash) { | ||||
|     // ignore: parameter_assignments | ||||
|     hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); | ||||
|     // ignore: parameter_assignments | ||||
|     hash = hash ^ (hash >> 11); | ||||
|     return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// Provider for web feed articles content | ||||
| /// | ||||
| /// Copied from [marketplaceWebFeedContent]. | ||||
| @ProviderFor(marketplaceWebFeedContent) | ||||
| const marketplaceWebFeedContentProvider = MarketplaceWebFeedContentFamily(); | ||||
|  | ||||
| /// Provider for web feed articles content | ||||
| /// | ||||
| /// Copied from [marketplaceWebFeedContent]. | ||||
| class MarketplaceWebFeedContentFamily | ||||
|     extends Family<AsyncValue<List<SnWebArticle>>> { | ||||
|   /// Provider for web feed articles content | ||||
|   /// | ||||
|   /// Copied from [marketplaceWebFeedContent]. | ||||
|   const MarketplaceWebFeedContentFamily(); | ||||
|  | ||||
|   /// Provider for web feed articles content | ||||
|   /// | ||||
|   /// Copied from [marketplaceWebFeedContent]. | ||||
|   MarketplaceWebFeedContentProvider call({required String feedId}) { | ||||
|     return MarketplaceWebFeedContentProvider(feedId: feedId); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   MarketplaceWebFeedContentProvider getProviderOverride( | ||||
|     covariant MarketplaceWebFeedContentProvider provider, | ||||
|   ) { | ||||
|     return call(feedId: provider.feedId); | ||||
|   } | ||||
|  | ||||
|   static const Iterable<ProviderOrFamily>? _dependencies = null; | ||||
|  | ||||
|   @override | ||||
|   Iterable<ProviderOrFamily>? get dependencies => _dependencies; | ||||
|  | ||||
|   static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null; | ||||
|  | ||||
|   @override | ||||
|   Iterable<ProviderOrFamily>? get allTransitiveDependencies => | ||||
|       _allTransitiveDependencies; | ||||
|  | ||||
|   @override | ||||
|   String? get name => r'marketplaceWebFeedContentProvider'; | ||||
| } | ||||
|  | ||||
| /// Provider for web feed articles content | ||||
| /// | ||||
| /// Copied from [marketplaceWebFeedContent]. | ||||
| class MarketplaceWebFeedContentProvider | ||||
|     extends AutoDisposeFutureProvider<List<SnWebArticle>> { | ||||
|   /// Provider for web feed articles content | ||||
|   /// | ||||
|   /// Copied from [marketplaceWebFeedContent]. | ||||
|   MarketplaceWebFeedContentProvider({required String feedId}) | ||||
|     : this._internal( | ||||
|         (ref) => marketplaceWebFeedContent( | ||||
|           ref as MarketplaceWebFeedContentRef, | ||||
|           feedId: feedId, | ||||
|         ), | ||||
|         from: marketplaceWebFeedContentProvider, | ||||
|         name: r'marketplaceWebFeedContentProvider', | ||||
|         debugGetCreateSourceHash: | ||||
|             const bool.fromEnvironment('dart.vm.product') | ||||
|                 ? null | ||||
|                 : _$marketplaceWebFeedContentHash, | ||||
|         dependencies: MarketplaceWebFeedContentFamily._dependencies, | ||||
|         allTransitiveDependencies: | ||||
|             MarketplaceWebFeedContentFamily._allTransitiveDependencies, | ||||
|         feedId: feedId, | ||||
|       ); | ||||
|  | ||||
|   MarketplaceWebFeedContentProvider._internal( | ||||
|     super._createNotifier, { | ||||
|     required super.name, | ||||
|     required super.dependencies, | ||||
|     required super.allTransitiveDependencies, | ||||
|     required super.debugGetCreateSourceHash, | ||||
|     required super.from, | ||||
|     required this.feedId, | ||||
|   }) : super.internal(); | ||||
|  | ||||
|   final String feedId; | ||||
|  | ||||
|   @override | ||||
|   Override overrideWith( | ||||
|     FutureOr<List<SnWebArticle>> Function(MarketplaceWebFeedContentRef provider) | ||||
|     create, | ||||
|   ) { | ||||
|     return ProviderOverride( | ||||
|       origin: this, | ||||
|       override: MarketplaceWebFeedContentProvider._internal( | ||||
|         (ref) => create(ref as MarketplaceWebFeedContentRef), | ||||
|         from: from, | ||||
|         name: null, | ||||
|         dependencies: null, | ||||
|         allTransitiveDependencies: null, | ||||
|         debugGetCreateSourceHash: null, | ||||
|         feedId: feedId, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   AutoDisposeFutureProviderElement<List<SnWebArticle>> createElement() { | ||||
|     return _MarketplaceWebFeedContentProviderElement(this); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return other is MarketplaceWebFeedContentProvider && other.feedId == feedId; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode { | ||||
|     var hash = _SystemHash.combine(0, runtimeType.hashCode); | ||||
|     hash = _SystemHash.combine(hash, feedId.hashCode); | ||||
|  | ||||
|     return _SystemHash.finish(hash); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||
| // ignore: unused_element | ||||
| mixin MarketplaceWebFeedContentRef | ||||
|     on AutoDisposeFutureProviderRef<List<SnWebArticle>> { | ||||
|   /// The parameter `feedId` of this provider. | ||||
|   String get feedId; | ||||
| } | ||||
|  | ||||
| class _MarketplaceWebFeedContentProviderElement | ||||
|     extends AutoDisposeFutureProviderElement<List<SnWebArticle>> | ||||
|     with MarketplaceWebFeedContentRef { | ||||
|   _MarketplaceWebFeedContentProviderElement(super.provider); | ||||
|  | ||||
|   @override | ||||
|   String get feedId => (origin as MarketplaceWebFeedContentProvider).feedId; | ||||
| } | ||||
|  | ||||
| String _$marketplaceWebFeedSubscriptionHash() => | ||||
|     r'2ff06a48ed7d4236b57412ecca55e94c0a0b6330'; | ||||
|  | ||||
| /// Provider for web feed subscription status | ||||
| /// | ||||
| /// Copied from [marketplaceWebFeedSubscription]. | ||||
| @ProviderFor(marketplaceWebFeedSubscription) | ||||
| const marketplaceWebFeedSubscriptionProvider = | ||||
|     MarketplaceWebFeedSubscriptionFamily(); | ||||
|  | ||||
| /// Provider for web feed subscription status | ||||
| /// | ||||
| /// Copied from [marketplaceWebFeedSubscription]. | ||||
| class MarketplaceWebFeedSubscriptionFamily extends Family<AsyncValue<bool>> { | ||||
|   /// Provider for web feed subscription status | ||||
|   /// | ||||
|   /// Copied from [marketplaceWebFeedSubscription]. | ||||
|   const MarketplaceWebFeedSubscriptionFamily(); | ||||
|  | ||||
|   /// Provider for web feed subscription status | ||||
|   /// | ||||
|   /// Copied from [marketplaceWebFeedSubscription]. | ||||
|   MarketplaceWebFeedSubscriptionProvider call({required String feedId}) { | ||||
|     return MarketplaceWebFeedSubscriptionProvider(feedId: feedId); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   MarketplaceWebFeedSubscriptionProvider getProviderOverride( | ||||
|     covariant MarketplaceWebFeedSubscriptionProvider provider, | ||||
|   ) { | ||||
|     return call(feedId: provider.feedId); | ||||
|   } | ||||
|  | ||||
|   static const Iterable<ProviderOrFamily>? _dependencies = null; | ||||
|  | ||||
|   @override | ||||
|   Iterable<ProviderOrFamily>? get dependencies => _dependencies; | ||||
|  | ||||
|   static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null; | ||||
|  | ||||
|   @override | ||||
|   Iterable<ProviderOrFamily>? get allTransitiveDependencies => | ||||
|       _allTransitiveDependencies; | ||||
|  | ||||
|   @override | ||||
|   String? get name => r'marketplaceWebFeedSubscriptionProvider'; | ||||
| } | ||||
|  | ||||
| /// Provider for web feed subscription status | ||||
| /// | ||||
| /// Copied from [marketplaceWebFeedSubscription]. | ||||
| class MarketplaceWebFeedSubscriptionProvider | ||||
|     extends AutoDisposeFutureProvider<bool> { | ||||
|   /// Provider for web feed subscription status | ||||
|   /// | ||||
|   /// Copied from [marketplaceWebFeedSubscription]. | ||||
|   MarketplaceWebFeedSubscriptionProvider({required String feedId}) | ||||
|     : this._internal( | ||||
|         (ref) => marketplaceWebFeedSubscription( | ||||
|           ref as MarketplaceWebFeedSubscriptionRef, | ||||
|           feedId: feedId, | ||||
|         ), | ||||
|         from: marketplaceWebFeedSubscriptionProvider, | ||||
|         name: r'marketplaceWebFeedSubscriptionProvider', | ||||
|         debugGetCreateSourceHash: | ||||
|             const bool.fromEnvironment('dart.vm.product') | ||||
|                 ? null | ||||
|                 : _$marketplaceWebFeedSubscriptionHash, | ||||
|         dependencies: MarketplaceWebFeedSubscriptionFamily._dependencies, | ||||
|         allTransitiveDependencies: | ||||
|             MarketplaceWebFeedSubscriptionFamily._allTransitiveDependencies, | ||||
|         feedId: feedId, | ||||
|       ); | ||||
|  | ||||
|   MarketplaceWebFeedSubscriptionProvider._internal( | ||||
|     super._createNotifier, { | ||||
|     required super.name, | ||||
|     required super.dependencies, | ||||
|     required super.allTransitiveDependencies, | ||||
|     required super.debugGetCreateSourceHash, | ||||
|     required super.from, | ||||
|     required this.feedId, | ||||
|   }) : super.internal(); | ||||
|  | ||||
|   final String feedId; | ||||
|  | ||||
|   @override | ||||
|   Override overrideWith( | ||||
|     FutureOr<bool> Function(MarketplaceWebFeedSubscriptionRef provider) create, | ||||
|   ) { | ||||
|     return ProviderOverride( | ||||
|       origin: this, | ||||
|       override: MarketplaceWebFeedSubscriptionProvider._internal( | ||||
|         (ref) => create(ref as MarketplaceWebFeedSubscriptionRef), | ||||
|         from: from, | ||||
|         name: null, | ||||
|         dependencies: null, | ||||
|         allTransitiveDependencies: null, | ||||
|         debugGetCreateSourceHash: null, | ||||
|         feedId: feedId, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   AutoDisposeFutureProviderElement<bool> createElement() { | ||||
|     return _MarketplaceWebFeedSubscriptionProviderElement(this); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return other is MarketplaceWebFeedSubscriptionProvider && | ||||
|         other.feedId == feedId; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode { | ||||
|     var hash = _SystemHash.combine(0, runtimeType.hashCode); | ||||
|     hash = _SystemHash.combine(hash, feedId.hashCode); | ||||
|  | ||||
|     return _SystemHash.finish(hash); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||
| // ignore: unused_element | ||||
| mixin MarketplaceWebFeedSubscriptionRef on AutoDisposeFutureProviderRef<bool> { | ||||
|   /// The parameter `feedId` of this provider. | ||||
|   String get feedId; | ||||
| } | ||||
|  | ||||
| class _MarketplaceWebFeedSubscriptionProviderElement | ||||
|     extends AutoDisposeFutureProviderElement<bool> | ||||
|     with MarketplaceWebFeedSubscriptionRef { | ||||
|   _MarketplaceWebFeedSubscriptionProviderElement(super.provider); | ||||
|  | ||||
|   @override | ||||
|   String get feedId => | ||||
|       (origin as MarketplaceWebFeedSubscriptionProvider).feedId; | ||||
| } | ||||
|  | ||||
| // ignore_for_file: type=lint | ||||
| // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package | ||||
							
								
								
									
										169
									
								
								lib/screens/discovery/feeds/feed_marketplace.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								lib/screens/discovery/feeds/feed_marketplace.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,169 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/webfeed.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'dart:async'; | ||||
|  | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
| import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; | ||||
|  | ||||
| part 'feed_marketplace.g.dart'; | ||||
|  | ||||
| @riverpod | ||||
| class MarketplaceWebFeedsNotifier extends _$MarketplaceWebFeedsNotifier | ||||
|     with CursorPagingNotifierMixin<SnWebFeed> { | ||||
|   String? _query; | ||||
|  | ||||
|   @override | ||||
|   Future<CursorPagingData<SnWebFeed>> build({required String? query}) { | ||||
|     _query = query; | ||||
|     return fetch(cursor: null); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<CursorPagingData<SnWebFeed>> fetch({required String? cursor}) async { | ||||
|     final client = ref.read(apiClientProvider); | ||||
|     final offset = cursor == null ? 0 : int.parse(cursor); | ||||
|  | ||||
|     final response = await client.get( | ||||
|       '/sphere/feeds', | ||||
|       queryParameters: { | ||||
|         'offset': offset, | ||||
|         'take': 20, | ||||
|         if (_query != null && _query!.isNotEmpty) 'query': _query, | ||||
|       }, | ||||
|     ); | ||||
|  | ||||
|     final total = int.parse(response.headers.value('X-Total') ?? '0'); | ||||
|     final List<dynamic> data = response.data; | ||||
|     final feeds = data.map((e) => SnWebFeed.fromJson(e)).toList(); | ||||
|  | ||||
|     final hasMore = offset + feeds.length < total; | ||||
|     final nextCursor = hasMore ? (offset + feeds.length).toString() : null; | ||||
|  | ||||
|     return CursorPagingData( | ||||
|       items: feeds, | ||||
|       hasMore: hasMore, | ||||
|       nextCursor: nextCursor, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// Marketplace screen for browsing web feeds. | ||||
| class MarketplaceWebFeedsScreen extends HookConsumerWidget { | ||||
|   const MarketplaceWebFeedsScreen({super.key}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final query = useState<String?>(null); | ||||
|     final searchController = useTextEditingController(); | ||||
|     final focusNode = useFocusNode(); | ||||
|     final debounceTimer = useState<Timer?>(null); | ||||
|  | ||||
|     // Clear search when query is cleared | ||||
|     useEffect(() { | ||||
|       if (query.value == null || query.value!.isEmpty) { | ||||
|         searchController.clear(); | ||||
|       } | ||||
|       return null; | ||||
|     }, [query.value]); | ||||
|  | ||||
|     // Clean up timer on dispose | ||||
|     useEffect(() { | ||||
|       return () { | ||||
|         debounceTimer.value?.cancel(); | ||||
|       }; | ||||
|     }, []); | ||||
|  | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         title: const Text('webFeeds').tr(), | ||||
|         actions: const [Gap(8)], | ||||
|       ), | ||||
|       body: PagingHelperView( | ||||
|         provider: marketplaceWebFeedsNotifierProvider(query: query.value), | ||||
|         futureRefreshable: | ||||
|             marketplaceWebFeedsNotifierProvider(query: query.value).future, | ||||
|         notifierRefreshable: | ||||
|             marketplaceWebFeedsNotifierProvider(query: query.value).notifier, | ||||
|         contentBuilder: | ||||
|             (data, widgetCount, endItemView) => Column( | ||||
|               children: [ | ||||
|                 // Search bar above the list | ||||
|                 Padding( | ||||
|                   padding: const EdgeInsets.all(16), | ||||
|                   child: SearchBar( | ||||
|                     elevation: WidgetStateProperty.all(4), | ||||
|                     controller: searchController, | ||||
|                     focusNode: focusNode, | ||||
|                     hintText: 'search'.tr(), | ||||
|                     leading: const Icon(Symbols.search), | ||||
|                     padding: WidgetStateProperty.all( | ||||
|                       const EdgeInsets.symmetric(horizontal: 24), | ||||
|                     ), | ||||
|                     onTapOutside: | ||||
|                         (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                     trailing: [ | ||||
|                       if (query.value != null && query.value!.isNotEmpty) | ||||
|                         IconButton( | ||||
|                           icon: const Icon(Symbols.close), | ||||
|                           onPressed: () { | ||||
|                             query.value = null; | ||||
|                             searchController.clear(); | ||||
|                             focusNode.unfocus(); | ||||
|                           }, | ||||
|                         ), | ||||
|                     ], | ||||
|                     onChanged: (value) { | ||||
|                       // Debounce search to avoid excessive API calls | ||||
|                       debounceTimer.value?.cancel(); | ||||
|                       debounceTimer.value = Timer( | ||||
|                         const Duration(milliseconds: 500), | ||||
|                         () { | ||||
|                           query.value = value.isEmpty ? null : value; | ||||
|                         }, | ||||
|                       ); | ||||
|                     }, | ||||
|                     onSubmitted: (value) { | ||||
|                       query.value = value.isEmpty ? null : value; | ||||
|                       focusNode.unfocus(); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ), | ||||
|                 Expanded( | ||||
|                   child: ListView.builder( | ||||
|                     padding: EdgeInsets.zero, | ||||
|                     itemCount: widgetCount, | ||||
|                     itemBuilder: (context, index) { | ||||
|                       if (index == widgetCount - 1) { | ||||
|                         return endItemView; | ||||
|                       } | ||||
|  | ||||
|                       final feed = data.items[index]; | ||||
|                       return ListTile( | ||||
|                         title: Text(feed.title), | ||||
|                         subtitle: Text(feed.description ?? ''), | ||||
|                         trailing: const Icon(Symbols.chevron_right), | ||||
|                         onTap: () { | ||||
|                           // Navigate to web feed detail page | ||||
|                           context.pushNamed( | ||||
|                             'webFeedDetail', | ||||
|                             pathParameters: {'feedId': feed.id}, | ||||
|                           ); | ||||
|                         }, | ||||
|                       ); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										180
									
								
								lib/screens/discovery/feeds/feed_marketplace.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								lib/screens/discovery/feeds/feed_marketplace.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,180 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'feed_marketplace.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$marketplaceWebFeedsNotifierHash() => | ||||
|     r'dbf885d95570ca9c2259a58998975db813b18cbb'; | ||||
|  | ||||
| /// Copied from Dart SDK | ||||
| class _SystemHash { | ||||
|   _SystemHash._(); | ||||
|  | ||||
|   static int combine(int hash, int value) { | ||||
|     // ignore: parameter_assignments | ||||
|     hash = 0x1fffffff & (hash + value); | ||||
|     // ignore: parameter_assignments | ||||
|     hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); | ||||
|     return hash ^ (hash >> 6); | ||||
|   } | ||||
|  | ||||
|   static int finish(int hash) { | ||||
|     // ignore: parameter_assignments | ||||
|     hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); | ||||
|     // ignore: parameter_assignments | ||||
|     hash = hash ^ (hash >> 11); | ||||
|     return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); | ||||
|   } | ||||
| } | ||||
|  | ||||
| abstract class _$MarketplaceWebFeedsNotifier | ||||
|     extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnWebFeed>> { | ||||
|   late final String? query; | ||||
|  | ||||
|   FutureOr<CursorPagingData<SnWebFeed>> build({required String? query}); | ||||
| } | ||||
|  | ||||
| /// See also [MarketplaceWebFeedsNotifier]. | ||||
| @ProviderFor(MarketplaceWebFeedsNotifier) | ||||
| const marketplaceWebFeedsNotifierProvider = MarketplaceWebFeedsNotifierFamily(); | ||||
|  | ||||
| /// See also [MarketplaceWebFeedsNotifier]. | ||||
| class MarketplaceWebFeedsNotifierFamily | ||||
|     extends Family<AsyncValue<CursorPagingData<SnWebFeed>>> { | ||||
|   /// See also [MarketplaceWebFeedsNotifier]. | ||||
|   const MarketplaceWebFeedsNotifierFamily(); | ||||
|  | ||||
|   /// See also [MarketplaceWebFeedsNotifier]. | ||||
|   MarketplaceWebFeedsNotifierProvider call({required String? query}) { | ||||
|     return MarketplaceWebFeedsNotifierProvider(query: query); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   MarketplaceWebFeedsNotifierProvider getProviderOverride( | ||||
|     covariant MarketplaceWebFeedsNotifierProvider provider, | ||||
|   ) { | ||||
|     return call(query: provider.query); | ||||
|   } | ||||
|  | ||||
|   static const Iterable<ProviderOrFamily>? _dependencies = null; | ||||
|  | ||||
|   @override | ||||
|   Iterable<ProviderOrFamily>? get dependencies => _dependencies; | ||||
|  | ||||
|   static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null; | ||||
|  | ||||
|   @override | ||||
|   Iterable<ProviderOrFamily>? get allTransitiveDependencies => | ||||
|       _allTransitiveDependencies; | ||||
|  | ||||
|   @override | ||||
|   String? get name => r'marketplaceWebFeedsNotifierProvider'; | ||||
| } | ||||
|  | ||||
| /// See also [MarketplaceWebFeedsNotifier]. | ||||
| class MarketplaceWebFeedsNotifierProvider | ||||
|     extends | ||||
|         AutoDisposeAsyncNotifierProviderImpl< | ||||
|           MarketplaceWebFeedsNotifier, | ||||
|           CursorPagingData<SnWebFeed> | ||||
|         > { | ||||
|   /// See also [MarketplaceWebFeedsNotifier]. | ||||
|   MarketplaceWebFeedsNotifierProvider({required String? query}) | ||||
|     : this._internal( | ||||
|         () => MarketplaceWebFeedsNotifier()..query = query, | ||||
|         from: marketplaceWebFeedsNotifierProvider, | ||||
|         name: r'marketplaceWebFeedsNotifierProvider', | ||||
|         debugGetCreateSourceHash: | ||||
|             const bool.fromEnvironment('dart.vm.product') | ||||
|                 ? null | ||||
|                 : _$marketplaceWebFeedsNotifierHash, | ||||
|         dependencies: MarketplaceWebFeedsNotifierFamily._dependencies, | ||||
|         allTransitiveDependencies: | ||||
|             MarketplaceWebFeedsNotifierFamily._allTransitiveDependencies, | ||||
|         query: query, | ||||
|       ); | ||||
|  | ||||
|   MarketplaceWebFeedsNotifierProvider._internal( | ||||
|     super._createNotifier, { | ||||
|     required super.name, | ||||
|     required super.dependencies, | ||||
|     required super.allTransitiveDependencies, | ||||
|     required super.debugGetCreateSourceHash, | ||||
|     required super.from, | ||||
|     required this.query, | ||||
|   }) : super.internal(); | ||||
|  | ||||
|   final String? query; | ||||
|  | ||||
|   @override | ||||
|   FutureOr<CursorPagingData<SnWebFeed>> runNotifierBuild( | ||||
|     covariant MarketplaceWebFeedsNotifier notifier, | ||||
|   ) { | ||||
|     return notifier.build(query: query); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Override overrideWith(MarketplaceWebFeedsNotifier Function() create) { | ||||
|     return ProviderOverride( | ||||
|       origin: this, | ||||
|       override: MarketplaceWebFeedsNotifierProvider._internal( | ||||
|         () => create()..query = query, | ||||
|         from: from, | ||||
|         name: null, | ||||
|         dependencies: null, | ||||
|         allTransitiveDependencies: null, | ||||
|         debugGetCreateSourceHash: null, | ||||
|         query: query, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   AutoDisposeAsyncNotifierProviderElement< | ||||
|     MarketplaceWebFeedsNotifier, | ||||
|     CursorPagingData<SnWebFeed> | ||||
|   > | ||||
|   createElement() { | ||||
|     return _MarketplaceWebFeedsNotifierProviderElement(this); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return other is MarketplaceWebFeedsNotifierProvider && other.query == query; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode { | ||||
|     var hash = _SystemHash.combine(0, runtimeType.hashCode); | ||||
|     hash = _SystemHash.combine(hash, query.hashCode); | ||||
|  | ||||
|     return _SystemHash.finish(hash); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||
| // ignore: unused_element | ||||
| mixin MarketplaceWebFeedsNotifierRef | ||||
|     on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnWebFeed>> { | ||||
|   /// The parameter `query` of this provider. | ||||
|   String? get query; | ||||
| } | ||||
|  | ||||
| class _MarketplaceWebFeedsNotifierProviderElement | ||||
|     extends | ||||
|         AutoDisposeAsyncNotifierProviderElement< | ||||
|           MarketplaceWebFeedsNotifier, | ||||
|           CursorPagingData<SnWebFeed> | ||||
|         > | ||||
|     with MarketplaceWebFeedsNotifierRef { | ||||
|   _MarketplaceWebFeedsNotifierProviderElement(super.provider); | ||||
|  | ||||
|   @override | ||||
|   String? get query => (origin as MarketplaceWebFeedsNotifierProvider).query; | ||||
| } | ||||
|  | ||||
| // ignore_for_file: type=lint | ||||
| // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package | ||||
| @@ -7,7 +7,7 @@ part of 'sticker_marketplace.dart'; | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$marketplaceStickerPacksNotifierHash() => | ||||
|     r'711eafeadf488485521563d0831676c51772d13c'; | ||||
|     r'3bde76e18bb024f45ff6261fe735cdba97b02808'; | ||||
|  | ||||
| /// Copied from Dart SDK | ||||
| class _SystemHash { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user