From 25f23f7f93f6cf1f91c5d7fa7b5c267a1d702ac5 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 6 Dec 2025 21:05:29 +0800 Subject: [PATCH] :bug: Fix serval bugs during the changes --- lib/screens/discovery/articles.dart | 41 +++--- lib/screens/discovery/feeds/feed_detail.dart | 100 +++++++------ .../discovery/feeds/feed_marketplace.dart | 14 +- lib/widgets/article/article_list.dart | 1 - lib/widgets/paging/pagination_list.dart | 37 ++++- lib/widgets/web_article_card.dart | 135 +++++------------- 6 files changed, 144 insertions(+), 184 deletions(-) delete mode 100644 lib/widgets/article/article_list.dart diff --git a/lib/screens/discovery/articles.dart b/lib/screens/discovery/articles.dart index 3e854f22..20faa6aa 100644 --- a/lib/screens/discovery/articles.dart +++ b/lib/screens/discovery/articles.dart @@ -33,7 +33,12 @@ class ArticlesListNotifier extends AsyncNotifier> Future> fetch() async { final client = ref.read(apiClientProvider); - final queryParams = {'limit': pageSize, 'offset': fetchedCount.toString()}; + final queryParams = { + 'limit': pageSize, + 'offset': fetchedCount.toString(), + 'feedId': arg.feedId, + 'publisherId': arg.publisherId, + }..removeWhere((key, value) => value == null); try { final response = await client.get( @@ -41,13 +46,10 @@ class ArticlesListNotifier extends AsyncNotifier> queryParameters: queryParams, ); - final articles = - response.data - .map( - (json) => SnWebArticle.fromJson(json as Map), - ) - .cast() - .toList(); + final articles = response.data + .map((json) => SnWebArticle.fromJson(json as Map)) + .cast() + .toList(); totalCount = int.tryParse(response.headers.value('X-Total') ?? '0') ?? 0; @@ -81,6 +83,7 @@ class SliverArticlesList extends ConsumerWidget { ArticleListQuery(feedId: feedId, publisherId: publisherId), ); return PaginationList( + spacing: 12, provider: provider, notifier: provider.notifier, isRefreshable: false, @@ -184,18 +187,16 @@ class ArticlesScreen extends ConsumerWidget { ), ); }, - 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')), - ), + 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')), + ), ); } } diff --git a/lib/screens/discovery/feeds/feed_detail.dart b/lib/screens/discovery/feeds/feed_detail.dart index 57e4af00..bebc7a6b 100644 --- a/lib/screens/discovery/feeds/feed_detail.dart +++ b/lib/screens/discovery/feeds/feed_detail.dart @@ -44,11 +44,10 @@ class MarketplaceWebFeedContentNotifier queryParameters: queryParams, ); totalCount = int.parse(response.headers.value('X-Total') ?? '0'); - final articles = - response.data - .map((json) => SnWebArticle.fromJson(json)) - .cast() - .toList(); + final articles = response.data + .map((json) => SnWebArticle.fromJson(json)) + .cast() + .toList(); return articles; } @@ -116,31 +115,30 @@ class MarketplaceWebFeedDetailScreen extends HookConsumerWidget { // Feed meta feed .when( - data: - (data) => Column( - crossAxisAlignment: CrossAxisAlignment.stretch, + data: (data) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text(data.description ?? 'descriptionNone'.tr()), + Row( + spacing: 4, children: [ - Text(data.description ?? 'descriptionNone'.tr()), - Row( - spacing: 4, - children: [ - const Icon(Symbols.rss_feed, size: 16), - Text( - 'webFeedArticleCount'.plural( - feedNotifier.totalCount ?? 0, - ), - ), - ], - ).opacity(0.85), - Row( - spacing: 4, - children: [ - const Icon(Symbols.link, size: 16), - SelectableText(data.url), - ], - ).opacity(0.85), + const Icon(Symbols.rss_feed, size: 16), + Text( + 'webFeedArticleCount'.plural( + feedNotifier.totalCount ?? 0, + ), + ), ], - ), + ).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(), ) @@ -149,10 +147,12 @@ class MarketplaceWebFeedDetailScreen extends HookConsumerWidget { // Articles list Expanded( child: PaginationList( + spacing: 8, + padding: EdgeInsets.symmetric(vertical: 8), provider: marketplaceWebFeedContentNotifierProvider(id), notifier: marketplaceWebFeedContentNotifierProvider(id).notifier, itemBuilder: (context, index, article) { - return WebArticleCard(article: article); + return WebArticleCard(article: article).padding(horizontal: 12); }, ), ), @@ -165,29 +165,25 @@ class MarketplaceWebFeedDetailScreen extends HookConsumerWidget { ), color: Theme.of(context).colorScheme.surfaceContainer, 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(), - ), + 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), + ).center(), + error: (_, _) => OutlinedButton.icon( + onPressed: subscribeToFeed, + icon: const Icon(Symbols.add_circle), + label: Text('subscribe').tr(), + ), ), ), ], diff --git a/lib/screens/discovery/feeds/feed_marketplace.dart b/lib/screens/discovery/feeds/feed_marketplace.dart index 9016c753..d623b1a6 100644 --- a/lib/screens/discovery/feeds/feed_marketplace.dart +++ b/lib/screens/discovery/feeds/feed_marketplace.dart @@ -38,11 +38,10 @@ class MarketplaceWebFeedsNotifier extends AsyncNotifier> ); totalCount = int.parse(response.headers.value('X-Total') ?? '0'); - final feeds = - response.data - .map((e) => SnWebFeed.fromJson(e)) - .cast() - .toList(); + final feeds = response.data + .map((e) => SnWebFeed.fromJson(e)) + .cast() + .toList(); return feeds; } @@ -92,8 +91,8 @@ class MarketplaceWebFeedsScreen extends HookConsumerWidget { padding: WidgetStateProperty.all( const EdgeInsets.symmetric(horizontal: 24), ), - onTapOutside: - (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), trailing: [ if (query.value != null && query.value!.isNotEmpty) IconButton( @@ -128,6 +127,7 @@ class MarketplaceWebFeedsScreen extends HookConsumerWidget { padding: EdgeInsets.zero, itemBuilder: (context, index, feed) { return ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 24), title: Text(feed.title), subtitle: Text(feed.description ?? ''), trailing: const Icon(Symbols.chevron_right), diff --git a/lib/widgets/article/article_list.dart b/lib/widgets/article/article_list.dart deleted file mode 100644 index 8b137891..00000000 --- a/lib/widgets/article/article_list.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/lib/widgets/paging/pagination_list.dart b/lib/widgets/paging/pagination_list.dart index 8c7d2db8..26d9bf24 100644 --- a/lib/widgets/paging/pagination_list.dart +++ b/lib/widgets/paging/pagination_list.dart @@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_riverpod/misc.dart'; +import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/pods/paging.dart'; import 'package:island/widgets/extended_refresh_indicator.dart'; @@ -17,6 +18,8 @@ class PaginationList extends HookConsumerWidget { final ProviderListenable>> provider; final Refreshable> notifier; final Widget? Function(BuildContext, int, T) itemBuilder; + final Widget? Function(BuildContext, int, T)? seperatorBuilder; + final double? spacing; final bool isRefreshable; final bool isSliver; final bool showDefaultWidgets; @@ -28,6 +31,8 @@ class PaginationList extends HookConsumerWidget { required this.provider, required this.notifier, required this.itemBuilder, + this.seperatorBuilder, + this.spacing, this.isRefreshable = true, this.isSliver = false, this.showDefaultWidgets = true, @@ -71,7 +76,7 @@ class PaginationList extends HookConsumerWidget { return SliverFillRemaining(child: content); } - final listView = SuperSliverList.builder( + final listView = SuperSliverList.separated( itemCount: (data.value?.length ?? 0) + 1, itemBuilder: (context, idx) { if (idx == data.value?.length) { @@ -86,6 +91,20 @@ class PaginationList extends HookConsumerWidget { if (entry != null) return itemBuilder(context, idx, entry); return null; }, + separatorBuilder: (context, index) { + if (seperatorBuilder != null) { + final entry = data.value?[index]; + if (entry != null) { + return seperatorBuilder!(context, index, entry) ?? + const SizedBox(); + } + return const SizedBox(); + } + if (spacing != null && spacing! > 0) { + return Gap(spacing!); + } + return const SizedBox(); + }, ); return isRefreshable @@ -126,7 +145,7 @@ class PaginationList extends HookConsumerWidget { return SizedBox(key: const ValueKey('error'), child: content); } - final listView = SuperListView.builder( + final listView = SuperListView.separated( padding: padding, itemCount: (data.value?.length ?? 0) + 1, itemBuilder: (context, idx) { @@ -142,6 +161,20 @@ class PaginationList extends HookConsumerWidget { if (entry != null) return itemBuilder(context, idx, entry); return null; }, + separatorBuilder: (context, index) { + if (seperatorBuilder != null) { + final entry = data.value?[index]; + if (entry != null) { + return seperatorBuilder!(context, index, entry) ?? + const SizedBox(); + } + return const SizedBox(); + } + if (spacing != null && spacing! > 0) { + return Gap(spacing!); + } + return const SizedBox(); + }, ); return SizedBox( diff --git a/lib/widgets/web_article_card.dart b/lib/widgets/web_article_card.dart index a99d1190..af369e06 100644 --- a/lib/widgets/web_article_card.dart +++ b/lib/widgets/web_article_card.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:island/models/webfeed.dart'; import 'package:island/services/time.dart'; +import 'package:material_symbols_icons/symbols.dart'; class WebArticleCard extends StatelessWidget { final SnWebArticle article; @@ -22,9 +23,6 @@ class WebArticleCard extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - return ConstrainedBox( constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), child: Card( @@ -32,108 +30,41 @@ class WebArticleCard extends StatelessWidget { clipBehavior: Clip.antiAlias, child: InkWell( onTap: () => _onTap(context), - child: AspectRatio( - aspectRatio: 16 / 9, - child: Stack( - fit: StackFit.expand, - children: [ - // Image or fallback - article.preview?.imageUrl != null - ? CachedNetworkImage( - imageUrl: article.preview!.imageUrl!, - fit: BoxFit.cover, - width: double.infinity, - height: double.infinity, - ) - : ColoredBox( - color: colorScheme.secondaryContainer, - child: const Center( - child: Icon( - Icons.article_outlined, - size: 48, - color: Colors.white, - ), - ), - ), - // Gradient overlay - Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Colors.transparent, - Colors.black.withOpacity(0.7), - ], - ), + child: Column( + children: [ + if (article.preview?.imageUrl != null) + AspectRatio( + aspectRatio: 16 / 9, + child: CachedNetworkImage( + imageUrl: article.preview!.imageUrl!, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, ), ), - // Title - Align( - alignment: Alignment.bottomLeft, - child: Container( - padding: const EdgeInsets.only( - left: 12, - right: 12, - bottom: 8, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - if (showDetails) - const SizedBox(height: 8) - else - Spacer(), - Text( - article.title, - style: theme.textTheme.titleSmall?.copyWith( - color: Colors.white, - fontWeight: FontWeight.bold, - height: 1.3, - ), - maxLines: showDetails ? 3 : 1, - overflow: TextOverflow.ellipsis, - ), - if (showDetails && - article.author?.isNotEmpty == true) ...[ - const SizedBox(height: 4), - Text( - article.author!, - style: TextStyle( - fontSize: 10, - color: Colors.white.withOpacity(0.9), - fontWeight: FontWeight.w500, - ), - ), - ], - if (showDetails) const Spacer(), - if (showDetails && article.publishedAt != null) ...[ - Text( - '${article.publishedAt!.formatSystem()} · ${article.publishedAt!.formatRelative(context)}', - style: const TextStyle( - fontSize: 9, - color: Colors.white70, - ), - ), - const SizedBox(height: 2), - ], - Text( - article.feed?.title ?? 'Unknown Source', - style: const TextStyle( - fontSize: 9, - color: Colors.white70, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), + ListTile( + isThreeLine: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 4, ), - ], - ), + trailing: const Icon(Symbols.chevron_right), + title: Text(article.title), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${article.createdAt.formatSystem()} · ${article.createdAt.formatRelative(context)}', + ), + Text( + article.feed?.title ?? 'Unknown Source', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], ), ), ),