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')),
 | 
						|
          ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 |