Compare commits
	
		
			2 Commits
		
	
	
		
			d7dcde898c
			...
			abf395ff9a
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| abf395ff9a | |||
| 4fdc8eb1d0 | 
| @@ -907,5 +907,12 @@ | ||||
|   "copyKeyHint": "Please copy this key and store it somewhere safe. You will not be able to see it again.", | ||||
|   "rotateKey": "Rotate Key", | ||||
|   "rotateBotKey": "Rotate Bot Key", | ||||
|   "rotateBotKeyHint": "Are you sure you want to rotate this key? The old key will become invalid immediately. This action cannot be undone." | ||||
|   "rotateBotKeyHint": "Are you sure you want to rotate this key? The old key will become invalid immediately. This action cannot be undone.", | ||||
|   "webFeedArticleCount": { | ||||
|     "zero": "No articles", | ||||
|     "one": "{} article", | ||||
|     "other": "{} articles" | ||||
|   }, | ||||
|   "webFeedSubscribed": "The feed has been subscribed", | ||||
|   "webFeedUnsubscribed": "The feed has been unsubscribed" | ||||
| } | ||||
| @@ -297,16 +297,24 @@ class EditAppScreen extends HookConsumerWidget { | ||||
|                 } | ||||
|                 : null, | ||||
|       }; | ||||
|       if (isNew) { | ||||
|         await client.post( | ||||
|           '/develop/developers/$publisherName/projects/$projectId/apps', | ||||
|           data: data, | ||||
|         ); | ||||
|       } else { | ||||
|         await client.patch( | ||||
|           '/develop/developers/$publisherName/projects/$projectId/apps/$id', | ||||
|           data: data, | ||||
|         ); | ||||
|       try { | ||||
|         showLoadingModal(context); | ||||
|         if (isNew) { | ||||
|           await client.post( | ||||
|             '/develop/developers/$publisherName/projects/$projectId/apps', | ||||
|             data: data, | ||||
|           ); | ||||
|         } else { | ||||
|           await client.patch( | ||||
|             '/develop/developers/$publisherName/projects/$projectId/apps/$id', | ||||
|             data: data, | ||||
|           ); | ||||
|         } | ||||
|       } catch (err) { | ||||
|         showErrorAlert(err); | ||||
|         return; | ||||
|       } finally { | ||||
|         if (context.mounted) hideLoadingModal(context); | ||||
|       } | ||||
|       ref.invalidate(customAppsProvider(publisherName, projectId)); | ||||
|       if (context.mounted) { | ||||
|   | ||||
| @@ -116,33 +116,89 @@ class SliverArticlesList extends ConsumerWidget { | ||||
|   } | ||||
| } | ||||
|  | ||||
| class ArticlesScreen extends ConsumerWidget { | ||||
|   final String? feedId; | ||||
|   final String? publisherId; | ||||
|   final String? title; | ||||
| @riverpod | ||||
| Future<List<SnWebFeed>> subscribedFeeds(Ref ref) async { | ||||
|   final client = ref.watch(apiClientProvider); | ||||
|   final response = await client.get('/sphere/feeds/subscribed'); | ||||
|   final data = response.data as List<dynamic>; | ||||
|   return data.map((json) => SnWebFeed.fromJson(json)).toList(); | ||||
| } | ||||
|  | ||||
|   const ArticlesScreen({super.key, this.feedId, this.publisherId, this.title}); | ||||
| class ArticlesScreen extends ConsumerWidget { | ||||
|   const ArticlesScreen({super.key}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar(title: Text(title ?? 'Articles')), | ||||
|       body: Center( | ||||
|         child: ConstrainedBox( | ||||
|           constraints: const BoxConstraints(maxWidth: 560), | ||||
|           child: CustomScrollView( | ||||
|             slivers: [ | ||||
|               SliverPadding( | ||||
|                 padding: const EdgeInsets.only(top: 8, left: 8, right: 8), | ||||
|                 sliver: SliverArticlesList( | ||||
|                   feedId: feedId, | ||||
|                   publisherId: publisherId, | ||||
|                 ), | ||||
|     final subscribedFeedsAsync = ref.watch(subscribedFeedsProvider); | ||||
|  | ||||
|     return subscribedFeedsAsync.when( | ||||
|       data: (feeds) { | ||||
|         return DefaultTabController( | ||||
|           length: feeds.length + 1, | ||||
|           child: AppScaffold( | ||||
|             appBar: AppBar( | ||||
|               title: const Text('Articles'), | ||||
|               bottom: TabBar( | ||||
|                 isScrollable: true, | ||||
|                 tabs: [ | ||||
|                   const Tab(text: 'All'), | ||||
|                   ...feeds.map((feed) => Tab(text: feed.title)), | ||||
|                 ], | ||||
|               ), | ||||
|             ], | ||||
|             ), | ||||
|             body: TabBarView( | ||||
|               children: [ | ||||
|                 Center( | ||||
|                   child: ConstrainedBox( | ||||
|                     constraints: const BoxConstraints(maxWidth: 560), | ||||
|                     child: CustomScrollView( | ||||
|                       slivers: [ | ||||
|                         SliverPadding( | ||||
|                           padding: const EdgeInsets.only( | ||||
|                             top: 8, | ||||
|                             left: 8, | ||||
|                             right: 8, | ||||
|                           ), | ||||
|                           sliver: SliverArticlesList(), | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|                 ...feeds.map((feed) { | ||||
|                   return Center( | ||||
|                     child: ConstrainedBox( | ||||
|                       constraints: const BoxConstraints(maxWidth: 560), | ||||
|                       child: CustomScrollView( | ||||
|                         slivers: [ | ||||
|                           SliverPadding( | ||||
|                             padding: const EdgeInsets.only( | ||||
|                               top: 8, | ||||
|                               left: 8, | ||||
|                               right: 8, | ||||
|                             ), | ||||
|                             sliver: SliverArticlesList(feedId: feed.id), | ||||
|                           ), | ||||
|                         ], | ||||
|                       ), | ||||
|                     ), | ||||
|                   ); | ||||
|                 }).toList(), | ||||
|               ], | ||||
|             ), | ||||
|           ), | ||||
|         ); | ||||
|       }, | ||||
|       loading: | ||||
|           () => AppScaffold( | ||||
|             appBar: AppBar(title: const Text('Articles')), | ||||
|             body: const Center(child: CircularProgressIndicator()), | ||||
|           ), | ||||
|       error: | ||||
|           (err, stack) => AppScaffold( | ||||
|             appBar: AppBar(title: const Text('Articles')), | ||||
|             body: Center(child: Text('Error: $err')), | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -6,6 +6,25 @@ part of 'articles.dart'; | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$subscribedFeedsHash() => r'cd2f5d7d4ea49ad00dc731f8fc2ed65450a3f0e4'; | ||||
|  | ||||
| /// See also [subscribedFeeds]. | ||||
| @ProviderFor(subscribedFeeds) | ||||
| final subscribedFeedsProvider = | ||||
|     AutoDisposeFutureProvider<List<SnWebFeed>>.internal( | ||||
|       subscribedFeeds, | ||||
|       name: r'subscribedFeedsProvider', | ||||
|       debugGetCreateSourceHash: | ||||
|           const bool.fromEnvironment('dart.vm.product') | ||||
|               ? null | ||||
|               : _$subscribedFeedsHash, | ||||
|       dependencies: null, | ||||
|       allTransitiveDependencies: null, | ||||
|     ); | ||||
|  | ||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||
| // ignore: unused_element | ||||
| typedef SubscribedFeedsRef = AutoDisposeFutureProviderRef<List<SnWebFeed>>; | ||||
| String _$articlesListNotifierHash() => | ||||
|     r'579741af4d90c7c81f2e2697e57c4895b7a9dabc'; | ||||
|  | ||||
|   | ||||
| @@ -1,27 +1,75 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.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:island/widgets/web_article_card.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
| import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| part 'feed_detail.g.dart'; | ||||
|  | ||||
| @riverpod | ||||
| Future<SnWebFeed> marketplaceWebFeed(Ref ref, String feedId) async { | ||||
|   final apiClient = ref.watch(apiClientProvider); | ||||
|   final resp = await apiClient.get('/sphere/feeds/$feedId'); | ||||
|   return SnWebFeed.fromJson(resp.data); | ||||
| } | ||||
|  | ||||
| /// 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(); | ||||
| class MarketplaceWebFeedContentNotifier | ||||
|     extends _$MarketplaceWebFeedContentNotifier | ||||
|     with CursorPagingNotifierMixin<SnWebArticle> { | ||||
|   static const int _pageSize = 20; | ||||
|  | ||||
|   @override | ||||
|   Future<CursorPagingData<SnWebArticle>> build(String feedId) async { | ||||
|     _feedId = feedId; | ||||
|     return fetch(cursor: null); | ||||
|   } | ||||
|  | ||||
|   late final String _feedId; | ||||
|   ValueNotifier<int> totalCount = ValueNotifier(0); | ||||
|  | ||||
|   @override | ||||
|   Future<CursorPagingData<SnWebArticle>> fetch({ | ||||
|     required String? cursor, | ||||
|   }) async { | ||||
|     final client = ref.read(apiClientProvider); | ||||
|     final offset = cursor == null ? 0 : int.parse(cursor); | ||||
|  | ||||
|     final queryParams = {'offset': offset, 'take': _pageSize}; | ||||
|  | ||||
|     final response = await client.get( | ||||
|       '/sphere/feeds/$_feedId/articles', | ||||
|       queryParameters: queryParams, | ||||
|     ); | ||||
|     final total = int.parse(response.headers.value('X-Total') ?? '0'); | ||||
|     totalCount.value = total; | ||||
|     final List<dynamic> data = response.data; | ||||
|     final articles = data.map((json) => SnWebArticle.fromJson(json)).toList(); | ||||
|  | ||||
|     final hasMore = offset + articles.length < total; | ||||
|     final nextCursor = hasMore ? (offset + articles.length).toString() : null; | ||||
|  | ||||
|     return CursorPagingData( | ||||
|       items: articles, | ||||
|       hasMore: hasMore, | ||||
|       nextCursor: nextCursor, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   void dispose() { | ||||
|     totalCount.dispose(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// Provider for web feed subscription status | ||||
| @@ -49,11 +97,7 @@ class MarketplaceWebFeedDetailScreen extends HookConsumerWidget { | ||||
|  | ||||
|   @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 feed = ref.watch(marketplaceWebFeedProvider(id)); | ||||
|     final subscribed = ref.watch( | ||||
|       marketplaceWebFeedSubscriptionProvider(feedId: id), | ||||
|     ); | ||||
| @@ -65,7 +109,7 @@ class MarketplaceWebFeedDetailScreen extends HookConsumerWidget { | ||||
|       HapticFeedback.selectionClick(); | ||||
|       ref.invalidate(marketplaceWebFeedSubscriptionProvider(feedId: id)); | ||||
|       if (!context.mounted) return; | ||||
|       showSnackBar('feedSubscribed'.tr()); | ||||
|       showSnackBar('webFeedSubscribed'.tr()); | ||||
|     } | ||||
|  | ||||
|     // Unsubscribe from web feed | ||||
| @@ -75,86 +119,94 @@ class MarketplaceWebFeedDetailScreen extends HookConsumerWidget { | ||||
|       HapticFeedback.selectionClick(); | ||||
|       ref.invalidate(marketplaceWebFeedSubscriptionProvider(feedId: id)); | ||||
|       if (!context.mounted) return; | ||||
|       showSnackBar('feedUnsubscribed'.tr()); | ||||
|       showSnackBar('webFeedUnsubscribed'.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(), | ||||
|     final feedNotifier = ref.watch( | ||||
|       marketplaceWebFeedContentNotifierProvider(id).notifier, | ||||
|     ); | ||||
|  | ||||
|     useEffect(() { | ||||
|       return feedNotifier.dispose; | ||||
|     }, []); | ||||
|  | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar(title: Text(dummyFeed.title)), | ||||
|       appBar: AppBar(title: Text(feed.value?.title ?? 'loading'.tr())), | ||||
|       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), | ||||
|           feed | ||||
|               .when( | ||||
|                 data: | ||||
|                     (data) => Column( | ||||
|                       crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|                       children: [ | ||||
|                         Text(data.description ?? 'descriptionNone'.tr()), | ||||
|                         Row( | ||||
|                           spacing: 4, | ||||
|                           children: [ | ||||
|                             const Icon(Symbols.rss_feed, size: 16), | ||||
|                             ListenableBuilder( | ||||
|                               listenable: feedNotifier.totalCount, | ||||
|                               builder: | ||||
|                                   (context, _) => Text( | ||||
|                                     'webFeedArticleCount'.plural( | ||||
|                                       feedNotifier.totalCount.value, | ||||
|                                     ), | ||||
|                                   ), | ||||
|                             ), | ||||
|                           ], | ||||
|                         ).opacity(0.85), | ||||
|                         Row( | ||||
|                           spacing: 4, | ||||
|                           children: [ | ||||
|                             const Icon(Symbols.link, size: 16), | ||||
|                             SelectableText(data.url), | ||||
|                           ], | ||||
|                         ).opacity(0.85), | ||||
|                       ], | ||||
|                     ), | ||||
|                 error: (err, _) => Text(err.toString()), | ||||
|                 loading: () => CircularProgressIndicator().center(), | ||||
|               ) | ||||
|               .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 | ||||
|                             }, | ||||
|                           ), | ||||
|                         ); | ||||
|                       }, | ||||
|             child: PagingHelperView( | ||||
|               provider: marketplaceWebFeedContentNotifierProvider(id), | ||||
|               futureRefreshable: | ||||
|                   marketplaceWebFeedContentNotifierProvider(id).future, | ||||
|               notifierRefreshable: | ||||
|                   marketplaceWebFeedContentNotifierProvider(id).notifier, | ||||
|               contentBuilder: | ||||
|                   (data, widgetCount, endItemView) => ListView.separated( | ||||
|                     padding: const EdgeInsets.symmetric( | ||||
|                       horizontal: 24, | ||||
|                       vertical: 20, | ||||
|                     ), | ||||
|                     itemCount: widgetCount, | ||||
|                     itemBuilder: (context, index) { | ||||
|                       if (index == widgetCount - 1) { | ||||
|                         return endItemView; | ||||
|                       } | ||||
|  | ||||
|                       final article = data.items[index]; | ||||
|                       return WebArticleCard(article: article); | ||||
|                     }, | ||||
|                     separatorBuilder: (context, index) => const Gap(12), | ||||
|                   ), | ||||
|               error: | ||||
|                   (err, _) => | ||||
|                       Text( | ||||
|                         'Error: $err', | ||||
|                       ).textAlignment(TextAlign.center).center(), | ||||
|               loading: () => const CircularProgressIndicator().center(), | ||||
|             ), | ||||
|           ), | ||||
|           Padding( | ||||
|             padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8), | ||||
|           Container( | ||||
|             padding: EdgeInsets.only( | ||||
|               bottom: 16 + MediaQuery.of(context).padding.bottom, | ||||
|               left: 24, | ||||
|               right: 24, | ||||
|               top: 16, | ||||
|             ), | ||||
|             color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|             child: subscribed.when( | ||||
|               data: | ||||
|                   (isSubscribed) => FilledButton.icon( | ||||
| @@ -181,7 +233,6 @@ class MarketplaceWebFeedDetailScreen extends HookConsumerWidget { | ||||
|                   ), | ||||
|             ), | ||||
|           ), | ||||
|           Gap(MediaQuery.of(context).padding.bottom), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   | ||||
| @@ -6,8 +6,8 @@ part of 'feed_detail.dart'; | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$marketplaceWebFeedContentHash() => | ||||
|     r'4e65350bff4055302e15ec14266cdebb1cd89bbe'; | ||||
| String _$marketplaceWebFeedHash() => | ||||
|     r'8383f94f1bc272b903c341b8d95000313b69d14c'; | ||||
|  | ||||
| /// Copied from Dart SDK | ||||
| class _SystemHash { | ||||
| @@ -30,34 +30,25 @@ class _SystemHash { | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// Provider for web feed articles content | ||||
| /// | ||||
| /// Copied from [marketplaceWebFeedContent]. | ||||
| @ProviderFor(marketplaceWebFeedContent) | ||||
| const marketplaceWebFeedContentProvider = MarketplaceWebFeedContentFamily(); | ||||
| /// See also [marketplaceWebFeed]. | ||||
| @ProviderFor(marketplaceWebFeed) | ||||
| const marketplaceWebFeedProvider = MarketplaceWebFeedFamily(); | ||||
|  | ||||
| /// 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(); | ||||
| /// See also [marketplaceWebFeed]. | ||||
| class MarketplaceWebFeedFamily extends Family<AsyncValue<SnWebFeed>> { | ||||
|   /// See also [marketplaceWebFeed]. | ||||
|   const MarketplaceWebFeedFamily(); | ||||
|  | ||||
|   /// Provider for web feed articles content | ||||
|   /// | ||||
|   /// Copied from [marketplaceWebFeedContent]. | ||||
|   MarketplaceWebFeedContentProvider call({required String feedId}) { | ||||
|     return MarketplaceWebFeedContentProvider(feedId: feedId); | ||||
|   /// See also [marketplaceWebFeed]. | ||||
|   MarketplaceWebFeedProvider call(String feedId) { | ||||
|     return MarketplaceWebFeedProvider(feedId); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   MarketplaceWebFeedContentProvider getProviderOverride( | ||||
|     covariant MarketplaceWebFeedContentProvider provider, | ||||
|   MarketplaceWebFeedProvider getProviderOverride( | ||||
|     covariant MarketplaceWebFeedProvider provider, | ||||
|   ) { | ||||
|     return call(feedId: provider.feedId); | ||||
|     return call(provider.feedId); | ||||
|   } | ||||
|  | ||||
|   static const Iterable<ProviderOrFamily>? _dependencies = null; | ||||
| @@ -72,36 +63,28 @@ class MarketplaceWebFeedContentFamily | ||||
|       _allTransitiveDependencies; | ||||
|  | ||||
|   @override | ||||
|   String? get name => r'marketplaceWebFeedContentProvider'; | ||||
|   String? get name => r'marketplaceWebFeedProvider'; | ||||
| } | ||||
|  | ||||
| /// 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}) | ||||
| /// See also [marketplaceWebFeed]. | ||||
| class MarketplaceWebFeedProvider extends AutoDisposeFutureProvider<SnWebFeed> { | ||||
|   /// See also [marketplaceWebFeed]. | ||||
|   MarketplaceWebFeedProvider(String feedId) | ||||
|     : this._internal( | ||||
|         (ref) => marketplaceWebFeedContent( | ||||
|           ref as MarketplaceWebFeedContentRef, | ||||
|           feedId: feedId, | ||||
|         ), | ||||
|         from: marketplaceWebFeedContentProvider, | ||||
|         name: r'marketplaceWebFeedContentProvider', | ||||
|         (ref) => marketplaceWebFeed(ref as MarketplaceWebFeedRef, feedId), | ||||
|         from: marketplaceWebFeedProvider, | ||||
|         name: r'marketplaceWebFeedProvider', | ||||
|         debugGetCreateSourceHash: | ||||
|             const bool.fromEnvironment('dart.vm.product') | ||||
|                 ? null | ||||
|                 : _$marketplaceWebFeedContentHash, | ||||
|         dependencies: MarketplaceWebFeedContentFamily._dependencies, | ||||
|                 : _$marketplaceWebFeedHash, | ||||
|         dependencies: MarketplaceWebFeedFamily._dependencies, | ||||
|         allTransitiveDependencies: | ||||
|             MarketplaceWebFeedContentFamily._allTransitiveDependencies, | ||||
|             MarketplaceWebFeedFamily._allTransitiveDependencies, | ||||
|         feedId: feedId, | ||||
|       ); | ||||
|  | ||||
|   MarketplaceWebFeedContentProvider._internal( | ||||
|   MarketplaceWebFeedProvider._internal( | ||||
|     super._createNotifier, { | ||||
|     required super.name, | ||||
|     required super.dependencies, | ||||
| @@ -115,13 +98,12 @@ class MarketplaceWebFeedContentProvider | ||||
|  | ||||
|   @override | ||||
|   Override overrideWith( | ||||
|     FutureOr<List<SnWebArticle>> Function(MarketplaceWebFeedContentRef provider) | ||||
|     create, | ||||
|     FutureOr<SnWebFeed> Function(MarketplaceWebFeedRef provider) create, | ||||
|   ) { | ||||
|     return ProviderOverride( | ||||
|       origin: this, | ||||
|       override: MarketplaceWebFeedContentProvider._internal( | ||||
|         (ref) => create(ref as MarketplaceWebFeedContentRef), | ||||
|       override: MarketplaceWebFeedProvider._internal( | ||||
|         (ref) => create(ref as MarketplaceWebFeedRef), | ||||
|         from: from, | ||||
|         name: null, | ||||
|         dependencies: null, | ||||
| @@ -133,13 +115,13 @@ class MarketplaceWebFeedContentProvider | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   AutoDisposeFutureProviderElement<List<SnWebArticle>> createElement() { | ||||
|     return _MarketplaceWebFeedContentProviderElement(this); | ||||
|   AutoDisposeFutureProviderElement<SnWebFeed> createElement() { | ||||
|     return _MarketplaceWebFeedProviderElement(this); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return other is MarketplaceWebFeedContentProvider && other.feedId == feedId; | ||||
|     return other is MarketplaceWebFeedProvider && other.feedId == feedId; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -153,19 +135,18 @@ class MarketplaceWebFeedContentProvider | ||||
|  | ||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||
| // ignore: unused_element | ||||
| mixin MarketplaceWebFeedContentRef | ||||
|     on AutoDisposeFutureProviderRef<List<SnWebArticle>> { | ||||
| mixin MarketplaceWebFeedRef on AutoDisposeFutureProviderRef<SnWebFeed> { | ||||
|   /// The parameter `feedId` of this provider. | ||||
|   String get feedId; | ||||
| } | ||||
|  | ||||
| class _MarketplaceWebFeedContentProviderElement | ||||
|     extends AutoDisposeFutureProviderElement<List<SnWebArticle>> | ||||
|     with MarketplaceWebFeedContentRef { | ||||
|   _MarketplaceWebFeedContentProviderElement(super.provider); | ||||
| class _MarketplaceWebFeedProviderElement | ||||
|     extends AutoDisposeFutureProviderElement<SnWebFeed> | ||||
|     with MarketplaceWebFeedRef { | ||||
|   _MarketplaceWebFeedProviderElement(super.provider); | ||||
|  | ||||
|   @override | ||||
|   String get feedId => (origin as MarketplaceWebFeedContentProvider).feedId; | ||||
|   String get feedId => (origin as MarketplaceWebFeedProvider).feedId; | ||||
| } | ||||
|  | ||||
| String _$marketplaceWebFeedSubscriptionHash() => | ||||
| @@ -309,5 +290,169 @@ class _MarketplaceWebFeedSubscriptionProviderElement | ||||
|       (origin as MarketplaceWebFeedSubscriptionProvider).feedId; | ||||
| } | ||||
|  | ||||
| String _$marketplaceWebFeedContentNotifierHash() => | ||||
|     r'25688082884cb824eeff300888ba38c9748295dc'; | ||||
|  | ||||
| abstract class _$MarketplaceWebFeedContentNotifier | ||||
|     extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnWebArticle>> { | ||||
|   late final String feedId; | ||||
|  | ||||
|   FutureOr<CursorPagingData<SnWebArticle>> build(String feedId); | ||||
| } | ||||
|  | ||||
| /// Provider for web feed articles content | ||||
| /// | ||||
| /// Copied from [MarketplaceWebFeedContentNotifier]. | ||||
| @ProviderFor(MarketplaceWebFeedContentNotifier) | ||||
| const marketplaceWebFeedContentNotifierProvider = | ||||
|     MarketplaceWebFeedContentNotifierFamily(); | ||||
|  | ||||
| /// Provider for web feed articles content | ||||
| /// | ||||
| /// Copied from [MarketplaceWebFeedContentNotifier]. | ||||
| class MarketplaceWebFeedContentNotifierFamily | ||||
|     extends Family<AsyncValue<CursorPagingData<SnWebArticle>>> { | ||||
|   /// Provider for web feed articles content | ||||
|   /// | ||||
|   /// Copied from [MarketplaceWebFeedContentNotifier]. | ||||
|   const MarketplaceWebFeedContentNotifierFamily(); | ||||
|  | ||||
|   /// Provider for web feed articles content | ||||
|   /// | ||||
|   /// Copied from [MarketplaceWebFeedContentNotifier]. | ||||
|   MarketplaceWebFeedContentNotifierProvider call(String feedId) { | ||||
|     return MarketplaceWebFeedContentNotifierProvider(feedId); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   MarketplaceWebFeedContentNotifierProvider getProviderOverride( | ||||
|     covariant MarketplaceWebFeedContentNotifierProvider provider, | ||||
|   ) { | ||||
|     return call(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'marketplaceWebFeedContentNotifierProvider'; | ||||
| } | ||||
|  | ||||
| /// Provider for web feed articles content | ||||
| /// | ||||
| /// Copied from [MarketplaceWebFeedContentNotifier]. | ||||
| class MarketplaceWebFeedContentNotifierProvider | ||||
|     extends | ||||
|         AutoDisposeAsyncNotifierProviderImpl< | ||||
|           MarketplaceWebFeedContentNotifier, | ||||
|           CursorPagingData<SnWebArticle> | ||||
|         > { | ||||
|   /// Provider for web feed articles content | ||||
|   /// | ||||
|   /// Copied from [MarketplaceWebFeedContentNotifier]. | ||||
|   MarketplaceWebFeedContentNotifierProvider(String feedId) | ||||
|     : this._internal( | ||||
|         () => MarketplaceWebFeedContentNotifier()..feedId = feedId, | ||||
|         from: marketplaceWebFeedContentNotifierProvider, | ||||
|         name: r'marketplaceWebFeedContentNotifierProvider', | ||||
|         debugGetCreateSourceHash: | ||||
|             const bool.fromEnvironment('dart.vm.product') | ||||
|                 ? null | ||||
|                 : _$marketplaceWebFeedContentNotifierHash, | ||||
|         dependencies: MarketplaceWebFeedContentNotifierFamily._dependencies, | ||||
|         allTransitiveDependencies: | ||||
|             MarketplaceWebFeedContentNotifierFamily._allTransitiveDependencies, | ||||
|         feedId: feedId, | ||||
|       ); | ||||
|  | ||||
|   MarketplaceWebFeedContentNotifierProvider._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 | ||||
|   FutureOr<CursorPagingData<SnWebArticle>> runNotifierBuild( | ||||
|     covariant MarketplaceWebFeedContentNotifier notifier, | ||||
|   ) { | ||||
|     return notifier.build(feedId); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Override overrideWith(MarketplaceWebFeedContentNotifier Function() create) { | ||||
|     return ProviderOverride( | ||||
|       origin: this, | ||||
|       override: MarketplaceWebFeedContentNotifierProvider._internal( | ||||
|         () => create()..feedId = feedId, | ||||
|         from: from, | ||||
|         name: null, | ||||
|         dependencies: null, | ||||
|         allTransitiveDependencies: null, | ||||
|         debugGetCreateSourceHash: null, | ||||
|         feedId: feedId, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   AutoDisposeAsyncNotifierProviderElement< | ||||
|     MarketplaceWebFeedContentNotifier, | ||||
|     CursorPagingData<SnWebArticle> | ||||
|   > | ||||
|   createElement() { | ||||
|     return _MarketplaceWebFeedContentNotifierProviderElement(this); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return other is MarketplaceWebFeedContentNotifierProvider && | ||||
|         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 MarketplaceWebFeedContentNotifierRef | ||||
|     on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnWebArticle>> { | ||||
|   /// The parameter `feedId` of this provider. | ||||
|   String get feedId; | ||||
| } | ||||
|  | ||||
| class _MarketplaceWebFeedContentNotifierProviderElement | ||||
|     extends | ||||
|         AutoDisposeAsyncNotifierProviderElement< | ||||
|           MarketplaceWebFeedContentNotifier, | ||||
|           CursorPagingData<SnWebArticle> | ||||
|         > | ||||
|     with MarketplaceWebFeedContentNotifierRef { | ||||
|   _MarketplaceWebFeedContentNotifierProviderElement(super.provider); | ||||
|  | ||||
|   @override | ||||
|   String get feedId => | ||||
|       (origin as MarketplaceWebFeedContentNotifierProvider).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 | ||||
|   | ||||
| @@ -7,7 +7,7 @@ part of 'feed_marketplace.dart'; | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$marketplaceWebFeedsNotifierHash() => | ||||
|     r'dbf885d95570ca9c2259a58998975db813b18cbb'; | ||||
|     r'774b2985f2f7d61fe958f534f84e39f814327c4e'; | ||||
|  | ||||
| /// Copied from Dart SDK | ||||
| class _SystemHash { | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:island/screens/chat/chat.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:island/models/chat.dart'; | ||||
| @@ -520,9 +521,11 @@ class _RealmActionMenu extends HookConsumerWidget { | ||||
| class RealmMemberListNotifier extends _$RealmMemberListNotifier | ||||
|     with CursorPagingNotifierMixin<SnRealmMember> { | ||||
|   static const int _pageSize = 20; | ||||
|   ValueNotifier<int> totalCount = ValueNotifier(0); | ||||
|  | ||||
|   @override | ||||
|   Future<CursorPagingData<SnRealmMember>> build(String realmSlug) async { | ||||
|     totalCount.value = 0; | ||||
|     return fetch(); | ||||
|   } | ||||
|  | ||||
| @@ -541,6 +544,7 @@ class RealmMemberListNotifier extends _$RealmMemberListNotifier | ||||
|     ); | ||||
|  | ||||
|     final total = int.parse(response.headers.value('X-Total') ?? '0'); | ||||
|     totalCount.value = total; | ||||
|     final List<dynamic> data = response.data; | ||||
|     final members = data.map((e) => SnRealmMember.fromJson(e)).toList(); | ||||
|  | ||||
| @@ -553,52 +557,9 @@ class RealmMemberListNotifier extends _$RealmMemberListNotifier | ||||
|       nextCursor: nextCursor, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Keep the old provider for backward compatibility | ||||
| final realmMemberStateProvider = | ||||
|     StateNotifierProvider.family<RealmMemberNotifier, RealmMemberState, String>( | ||||
|       (ref, realmSlug) { | ||||
|         final apiClient = ref.watch(apiClientProvider); | ||||
|         return RealmMemberNotifier(apiClient, realmSlug); | ||||
|       }, | ||||
|     ); | ||||
|  | ||||
| class RealmMemberNotifier extends StateNotifier<RealmMemberState> { | ||||
|   final String realmSlug; | ||||
|   final Dio _apiClient; | ||||
|  | ||||
|   RealmMemberNotifier(this._apiClient, this.realmSlug) | ||||
|     : super(const RealmMemberState(members: [], isLoading: false, total: 0)); | ||||
|  | ||||
|   Future<void> loadMore({int offset = 0, int take = 20}) async { | ||||
|     if (state.isLoading) return; | ||||
|     if (state.total > 0 && state.members.length >= state.total) return; | ||||
|  | ||||
|     state = state.copyWith(isLoading: true, error: null); | ||||
|  | ||||
|     try { | ||||
|       final response = await _apiClient.get( | ||||
|         '/sphere/realms/$realmSlug/members', | ||||
|         queryParameters: {'offset': offset, 'take': take, 'withStatus': true}, | ||||
|       ); | ||||
|  | ||||
|       final total = int.parse(response.headers.value('X-Total') ?? '0'); | ||||
|       final List<dynamic> data = response.data; | ||||
|       final members = data.map((e) => SnRealmMember.fromJson(e)).toList(); | ||||
|  | ||||
|       state = state.copyWith( | ||||
|         members: [...state.members, ...members], | ||||
|         total: total, | ||||
|         isLoading: false, | ||||
|       ); | ||||
|     } catch (e) { | ||||
|       state = state.copyWith(error: e.toString(), isLoading: false); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void reset() { | ||||
|     state = const RealmMemberState(members: [], isLoading: false, total: 0); | ||||
|   void dispose() { | ||||
|     totalCount.dispose(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -610,18 +571,10 @@ class _RealmMemberListSheet extends HookConsumerWidget { | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final realmIdentity = ref.watch(realmIdentityProvider(realmSlug)); | ||||
|     final memberListProvider = realmMemberListNotifierProvider(realmSlug); | ||||
|  | ||||
|     // For backward compatibility and to show total count in the header | ||||
|     final memberState = ref.watch(realmMemberStateProvider(realmSlug)); | ||||
|     final memberNotifier = ref.read( | ||||
|       realmMemberStateProvider(realmSlug).notifier, | ||||
|     ); | ||||
|     final memberListNotifier = ref.watch(memberListProvider.notifier); | ||||
|  | ||||
|     useEffect(() { | ||||
|       Future(() { | ||||
|         memberNotifier.loadMore(); | ||||
|       }); | ||||
|       return null; | ||||
|       return memberListNotifier.dispose; | ||||
|     }, []); | ||||
|  | ||||
|     Future<void> invitePerson() async { | ||||
| @@ -638,9 +591,7 @@ class _RealmMemberListSheet extends HookConsumerWidget { | ||||
|           '/sphere/realms/invites/$realmSlug', | ||||
|           data: {'related_user_id': result.id, 'role': 0}, | ||||
|         ); | ||||
|         // Refresh both providers | ||||
|         memberNotifier.reset(); | ||||
|         await memberNotifier.loadMore(); | ||||
|         // Refresh the provider | ||||
|         ref.invalidate(memberListProvider); | ||||
|       } catch (err) { | ||||
|         showErrorAlert(err); | ||||
| @@ -652,12 +603,17 @@ class _RealmMemberListSheet extends HookConsumerWidget { | ||||
|         padding: EdgeInsets.only(top: 16, left: 20, right: 16, bottom: 12), | ||||
|         child: Row( | ||||
|           children: [ | ||||
|             Text( | ||||
|               'members'.plural(memberState.total), | ||||
|               style: Theme.of(context).textTheme.headlineSmall?.copyWith( | ||||
|                 fontWeight: FontWeight.w600, | ||||
|                 letterSpacing: -0.5, | ||||
|               ), | ||||
|             ListenableBuilder( | ||||
|               listenable: memberListNotifier.totalCount, | ||||
|               builder: | ||||
|                   (context, _) => Text( | ||||
|                     'members'.plural(memberListNotifier.totalCount.value), | ||||
|                     key: ValueKey(memberListNotifier), | ||||
|                     style: Theme.of(context).textTheme.headlineSmall?.copyWith( | ||||
|                       fontWeight: FontWeight.w600, | ||||
|                       letterSpacing: -0.5, | ||||
|                     ), | ||||
|                   ), | ||||
|             ), | ||||
|             const Spacer(), | ||||
|             IconButton( | ||||
| @@ -668,9 +624,7 @@ class _RealmMemberListSheet extends HookConsumerWidget { | ||||
|             IconButton( | ||||
|               icon: const Icon(Symbols.refresh), | ||||
|               onPressed: () { | ||||
|                 // Refresh both providers | ||||
|                 memberNotifier.reset(); | ||||
|                 memberNotifier.loadMore(); | ||||
|                 // Refresh the provider | ||||
|                 ref.invalidate(memberListProvider); | ||||
|               }, | ||||
|             ), | ||||
| @@ -744,9 +698,7 @@ class _RealmMemberListSheet extends HookConsumerWidget { | ||||
|                                   ), | ||||
|                             ).then((value) { | ||||
|                               if (value != null) { | ||||
|                                 // Refresh both providers | ||||
|                                 memberNotifier.reset(); | ||||
|                                 memberNotifier.loadMore(); | ||||
|                                 // Refresh the provider | ||||
|                                 ref.invalidate(memberListProvider); | ||||
|                               } | ||||
|                             }); | ||||
| @@ -766,9 +718,7 @@ class _RealmMemberListSheet extends HookConsumerWidget { | ||||
|                                 await apiClient.delete( | ||||
|                                   '/sphere/realms/$realmSlug/members/${member.accountId}', | ||||
|                                 ); | ||||
|                                 // Refresh both providers | ||||
|                                 memberNotifier.reset(); | ||||
|                                 memberNotifier.loadMore(); | ||||
|                                 // Refresh the provider | ||||
|                                 ref.invalidate(memberListProvider); | ||||
|                               } catch (err) { | ||||
|                                 showErrorAlert(err); | ||||
| @@ -801,34 +751,6 @@ class _RealmMemberListSheet extends HookConsumerWidget { | ||||
|   } | ||||
| } | ||||
|  | ||||
| class RealmMemberState { | ||||
|   final List<SnRealmMember> members; | ||||
|   final bool isLoading; | ||||
|   final int total; | ||||
|   final String? error; | ||||
|  | ||||
|   const RealmMemberState({ | ||||
|     required this.members, | ||||
|     required this.isLoading, | ||||
|     required this.total, | ||||
|     this.error, | ||||
|   }); | ||||
|  | ||||
|   RealmMemberState copyWith({ | ||||
|     List<SnRealmMember>? members, | ||||
|     bool? isLoading, | ||||
|     int? total, | ||||
|     String? error, | ||||
|   }) { | ||||
|     return RealmMemberState( | ||||
|       members: members ?? this.members, | ||||
|       isLoading: isLoading ?? this.isLoading, | ||||
|       total: total ?? this.total, | ||||
|       error: error ?? this.error, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _RealmMemberRoleSheet extends HookConsumerWidget { | ||||
|   final String realmSlug; | ||||
|   final SnRealmMember member; | ||||
|   | ||||
| @@ -399,7 +399,7 @@ class _RealmChatRoomsProviderElement | ||||
| } | ||||
|  | ||||
| String _$realmMemberListNotifierHash() => | ||||
|     r'2f88f803b2e61e7287ed8a43025173e56ff6ca3b'; | ||||
|     r'db1fd8a6741dfb3d5bb921d5d965f0cfdc0e7bcc'; | ||||
|  | ||||
| abstract class _$RealmMemberListNotifier | ||||
|     extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnRealmMember>> { | ||||
|   | ||||
							
								
								
									
										1169
									
								
								swagger.json
									
									
									
									
									
								
							
							
						
						
									
										1169
									
								
								swagger.json
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Reference in New Issue
	
	Block a user