227 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			227 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'package:flutter/material.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:island/widgets/web_article_card.dart';
 | |
| import 'package:riverpod_annotation/riverpod_annotation.dart';
 | |
| import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
 | |
| 
 | |
| part 'articles.g.dart';
 | |
| 
 | |
| @riverpod
 | |
| class ArticlesListNotifier extends _$ArticlesListNotifier
 | |
|     with CursorPagingNotifierMixin<SnWebArticle> {
 | |
|   static const int _pageSize = 20;
 | |
| 
 | |
|   Map<String, dynamic> _params = {};
 | |
| 
 | |
|   @override
 | |
|   Future<CursorPagingData<SnWebArticle>> build({
 | |
|     String? feedId,
 | |
|     String? publisherId,
 | |
|   }) async {
 | |
|     _params = {
 | |
|       if (feedId != null) 'feedId': feedId,
 | |
|       if (publisherId != null) 'publisherId': publisherId,
 | |
|     };
 | |
|     return fetch(cursor: null);
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Future<CursorPagingData<SnWebArticle>> fetch({
 | |
|     required String? cursor,
 | |
|   }) async {
 | |
|     final client = ref.read(apiClientProvider);
 | |
|     final offset = cursor == null ? 0 : int.parse(cursor);
 | |
| 
 | |
|     final queryParams = {'limit': _pageSize, 'offset': offset, ..._params};
 | |
| 
 | |
|     try {
 | |
|       final response = await client.get(
 | |
|         '/sphere/feeds/articles',
 | |
|         queryParameters: queryParams,
 | |
|       );
 | |
| 
 | |
|       final List<dynamic> data = response.data;
 | |
|       final articles =
 | |
|           data
 | |
|               .map(
 | |
|                 (json) => SnWebArticle.fromJson(json as Map<String, dynamic>),
 | |
|               )
 | |
|               .toList();
 | |
| 
 | |
|       final total = int.tryParse(response.headers.value('X-Total') ?? '0') ?? 0;
 | |
|       final hasMore = offset + articles.length < total;
 | |
|       final nextCursor = hasMore ? (offset + articles.length).toString() : null;
 | |
| 
 | |
|       return CursorPagingData(
 | |
|         items: articles,
 | |
|         hasMore: hasMore,
 | |
|         nextCursor: nextCursor,
 | |
|       );
 | |
|     } catch (e) {
 | |
|       debugPrint('Error fetching articles: $e');
 | |
|       rethrow;
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| class SliverArticlesList extends ConsumerWidget {
 | |
|   final String? feedId;
 | |
|   final String? publisherId;
 | |
|   final Color? backgroundColor;
 | |
|   final EdgeInsets? padding;
 | |
|   final Function? onRefresh;
 | |
| 
 | |
|   const SliverArticlesList({
 | |
|     super.key,
 | |
|     this.feedId,
 | |
|     this.publisherId,
 | |
|     this.backgroundColor,
 | |
|     this.padding,
 | |
|     this.onRefresh,
 | |
|   });
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context, WidgetRef ref) {
 | |
|     return PagingHelperSliverView(
 | |
|       provider: articlesListNotifierProvider(
 | |
|         feedId: feedId,
 | |
|         publisherId: publisherId,
 | |
|       ),
 | |
|       futureRefreshable:
 | |
|           articlesListNotifierProvider(
 | |
|             feedId: feedId,
 | |
|             publisherId: publisherId,
 | |
|           ).future,
 | |
|       notifierRefreshable:
 | |
|           articlesListNotifierProvider(
 | |
|             feedId: feedId,
 | |
|             publisherId: publisherId,
 | |
|           ).notifier,
 | |
|       contentBuilder:
 | |
|           (data, widgetCount, endItemView) => SliverList.separated(
 | |
|             itemCount: widgetCount,
 | |
|             itemBuilder: (context, index) {
 | |
|               if (index == widgetCount - 1) {
 | |
|                 return endItemView;
 | |
|               }
 | |
| 
 | |
|               final article = data.items[index];
 | |
|               return WebArticleCard(article: article, showDetails: true);
 | |
|             },
 | |
|             separatorBuilder: (context, index) => const SizedBox(height: 12),
 | |
|           ),
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| @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();
 | |
| }
 | |
| 
 | |
| class ArticlesScreen extends ConsumerWidget {
 | |
|   const ArticlesScreen({super.key});
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context, WidgetRef ref) {
 | |
|     final subscribedFeedsAsync = ref.watch(subscribedFeedsProvider);
 | |
| 
 | |
|     return subscribedFeedsAsync.when(
 | |
|       data: (feeds) {
 | |
|         return DefaultTabController(
 | |
|           length: feeds.length + 1,
 | |
|           child: AppScaffold(
 | |
|             isNoBackground: false,
 | |
|             appBar: AppBar(
 | |
|               title: const Text('Articles'),
 | |
|               bottom: TabBar(
 | |
|                 isScrollable: true,
 | |
|                 tabs: [
 | |
|                   Tab(
 | |
|                     child: Text(
 | |
|                       'All',
 | |
|                       textAlign: TextAlign.center,
 | |
|                       style: TextStyle(
 | |
|                         color: Theme.of(context).appBarTheme.foregroundColor!,
 | |
|                       ),
 | |
|                     ),
 | |
|                   ),
 | |
|                   ...feeds.map(
 | |
|                     (feed) => Tab(
 | |
|                       child: Text(
 | |
|                         feed.title,
 | |
|                         textAlign: TextAlign.center,
 | |
|                         style: TextStyle(
 | |
|                           color: Theme.of(context).appBarTheme.foregroundColor!,
 | |
|                         ),
 | |
|                       ),
 | |
|                     ),
 | |
|                   ),
 | |
|                 ],
 | |
|               ),
 | |
|             ),
 | |
|             body: TabBarView(
 | |
|               children: [
 | |
|                 Center(
 | |
|                   child: ConstrainedBox(
 | |
|                     constraints: const BoxConstraints(maxWidth: 560),
 | |
|                     child: CustomScrollView(
 | |
|                       slivers: [
 | |
|                         SliverPadding(
 | |
|                           padding: const EdgeInsets.only(
 | |
|                             top: 12,
 | |
|                             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),
 | |
|                           ),
 | |
|                         ],
 | |
|                       ),
 | |
|                     ),
 | |
|                   );
 | |
|                 }),
 | |
|               ],
 | |
|             ),
 | |
|           ),
 | |
|         );
 | |
|       },
 | |
|       loading:
 | |
|           () => AppScaffold(
 | |
|             isNoBackground: false,
 | |
|             appBar: AppBar(title: const Text('Articles')),
 | |
|             body: const Center(child: CircularProgressIndicator()),
 | |
|           ),
 | |
|       error:
 | |
|           (err, stack) => AppScaffold(
 | |
|             isNoBackground: false,
 | |
|             appBar: AppBar(title: const Text('Articles')),
 | |
|             body: Center(child: Text('Error: $err')),
 | |
|           ),
 | |
|     );
 | |
|   }
 | |
| }
 |