🐛 Fix serval bugs during the changes

This commit is contained in:
2025-12-06 21:05:29 +08:00
parent 51853698b9
commit 25f23f7f93
6 changed files with 144 additions and 184 deletions

View File

@@ -33,7 +33,12 @@ class ArticlesListNotifier extends AsyncNotifier<List<SnWebArticle>>
Future<List<SnWebArticle>> fetch() async { Future<List<SnWebArticle>> fetch() async {
final client = ref.read(apiClientProvider); 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 { try {
final response = await client.get( final response = await client.get(
@@ -41,13 +46,10 @@ class ArticlesListNotifier extends AsyncNotifier<List<SnWebArticle>>
queryParameters: queryParams, queryParameters: queryParams,
); );
final articles = final articles = response.data
response.data .map((json) => SnWebArticle.fromJson(json as Map<String, dynamic>))
.map( .cast<SnWebArticle>()
(json) => SnWebArticle.fromJson(json as Map<String, dynamic>), .toList();
)
.cast<SnWebArticle>()
.toList();
totalCount = int.tryParse(response.headers.value('X-Total') ?? '0') ?? 0; totalCount = int.tryParse(response.headers.value('X-Total') ?? '0') ?? 0;
@@ -81,6 +83,7 @@ class SliverArticlesList extends ConsumerWidget {
ArticleListQuery(feedId: feedId, publisherId: publisherId), ArticleListQuery(feedId: feedId, publisherId: publisherId),
); );
return PaginationList( return PaginationList(
spacing: 12,
provider: provider, provider: provider,
notifier: provider.notifier, notifier: provider.notifier,
isRefreshable: false, isRefreshable: false,
@@ -184,18 +187,16 @@ class ArticlesScreen extends ConsumerWidget {
), ),
); );
}, },
loading: loading: () => AppScaffold(
() => AppScaffold( isNoBackground: false,
isNoBackground: false, appBar: AppBar(title: const Text('Articles')),
appBar: AppBar(title: const Text('Articles')), body: const Center(child: CircularProgressIndicator()),
body: const Center(child: CircularProgressIndicator()), ),
), error: (err, stack) => AppScaffold(
error: isNoBackground: false,
(err, stack) => AppScaffold( appBar: AppBar(title: const Text('Articles')),
isNoBackground: false, body: Center(child: Text('Error: $err')),
appBar: AppBar(title: const Text('Articles')), ),
body: Center(child: Text('Error: $err')),
),
); );
} }
} }

View File

@@ -44,11 +44,10 @@ class MarketplaceWebFeedContentNotifier
queryParameters: queryParams, queryParameters: queryParams,
); );
totalCount = int.parse(response.headers.value('X-Total') ?? '0'); totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final articles = final articles = response.data
response.data .map((json) => SnWebArticle.fromJson(json))
.map((json) => SnWebArticle.fromJson(json)) .cast<SnWebArticle>()
.cast<SnWebArticle>() .toList();
.toList();
return articles; return articles;
} }
@@ -116,31 +115,30 @@ class MarketplaceWebFeedDetailScreen extends HookConsumerWidget {
// Feed meta // Feed meta
feed feed
.when( .when(
data: data: (data) => Column(
(data) => Column( crossAxisAlignment: CrossAxisAlignment.stretch,
crossAxisAlignment: CrossAxisAlignment.stretch, children: [
Text(data.description ?? 'descriptionNone'.tr()),
Row(
spacing: 4,
children: [ children: [
Text(data.description ?? 'descriptionNone'.tr()), const Icon(Symbols.rss_feed, size: 16),
Row( Text(
spacing: 4, 'webFeedArticleCount'.plural(
children: [ feedNotifier.totalCount ?? 0,
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),
], ],
), ).opacity(0.85),
Row(
spacing: 4,
children: [
const Icon(Symbols.link, size: 16),
SelectableText(data.url),
],
).opacity(0.85),
],
),
error: (err, _) => Text(err.toString()), error: (err, _) => Text(err.toString()),
loading: () => CircularProgressIndicator().center(), loading: () => CircularProgressIndicator().center(),
) )
@@ -149,10 +147,12 @@ class MarketplaceWebFeedDetailScreen extends HookConsumerWidget {
// Articles list // Articles list
Expanded( Expanded(
child: PaginationList( child: PaginationList(
spacing: 8,
padding: EdgeInsets.symmetric(vertical: 8),
provider: marketplaceWebFeedContentNotifierProvider(id), provider: marketplaceWebFeedContentNotifierProvider(id),
notifier: marketplaceWebFeedContentNotifierProvider(id).notifier, notifier: marketplaceWebFeedContentNotifierProvider(id).notifier,
itemBuilder: (context, index, article) { 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, color: Theme.of(context).colorScheme.surfaceContainer,
child: subscribed.when( child: subscribed.when(
data: data: (isSubscribed) => FilledButton.icon(
(isSubscribed) => FilledButton.icon( onPressed: isSubscribed ? unsubscribeFromFeed : subscribeToFeed,
onPressed: icon: Icon(
isSubscribed ? unsubscribeFromFeed : subscribeToFeed, isSubscribed ? Symbols.remove_circle : Symbols.add_circle,
icon: Icon( ),
isSubscribed ? Symbols.remove_circle : Symbols.add_circle, label: Text(
), isSubscribed ? 'unsubscribe'.tr() : 'subscribe'.tr(),
label: Text( ),
isSubscribed ? 'unsubscribe'.tr() : 'subscribe'.tr(), ),
), loading: () => const SizedBox(
), height: 32,
loading: width: 32,
() => const SizedBox( child: CircularProgressIndicator(strokeWidth: 2),
height: 32, ).center(),
width: 32, error: (_, _) => OutlinedButton.icon(
child: CircularProgressIndicator(strokeWidth: 2), onPressed: subscribeToFeed,
), icon: const Icon(Symbols.add_circle),
error: label: Text('subscribe').tr(),
(_, _) => OutlinedButton.icon( ),
onPressed: subscribeToFeed,
icon: const Icon(Symbols.add_circle),
label: Text('subscribe').tr(),
),
), ),
), ),
], ],

View File

@@ -38,11 +38,10 @@ class MarketplaceWebFeedsNotifier extends AsyncNotifier<List<SnWebFeed>>
); );
totalCount = int.parse(response.headers.value('X-Total') ?? '0'); totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final feeds = final feeds = response.data
response.data .map((e) => SnWebFeed.fromJson(e))
.map((e) => SnWebFeed.fromJson(e)) .cast<SnWebFeed>()
.cast<SnWebFeed>() .toList();
.toList();
return feeds; return feeds;
} }
@@ -92,8 +91,8 @@ class MarketplaceWebFeedsScreen extends HookConsumerWidget {
padding: WidgetStateProperty.all( padding: WidgetStateProperty.all(
const EdgeInsets.symmetric(horizontal: 24), const EdgeInsets.symmetric(horizontal: 24),
), ),
onTapOutside: onTapOutside: (_) =>
(_) => FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
trailing: [ trailing: [
if (query.value != null && query.value!.isNotEmpty) if (query.value != null && query.value!.isNotEmpty)
IconButton( IconButton(
@@ -128,6 +127,7 @@ class MarketplaceWebFeedsScreen extends HookConsumerWidget {
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
itemBuilder: (context, index, feed) { itemBuilder: (context, index, feed) {
return ListTile( return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
title: Text(feed.title), title: Text(feed.title),
subtitle: Text(feed.description ?? ''), subtitle: Text(feed.description ?? ''),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),

View File

@@ -1 +0,0 @@

View File

@@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_riverpod/misc.dart'; import 'package:flutter_riverpod/misc.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/paging.dart'; import 'package:island/pods/paging.dart';
import 'package:island/widgets/extended_refresh_indicator.dart'; import 'package:island/widgets/extended_refresh_indicator.dart';
@@ -17,6 +18,8 @@ class PaginationList<T> extends HookConsumerWidget {
final ProviderListenable<AsyncValue<List<T>>> provider; final ProviderListenable<AsyncValue<List<T>>> provider;
final Refreshable<PaginationController<T>> notifier; final Refreshable<PaginationController<T>> notifier;
final Widget? Function(BuildContext, int, T) itemBuilder; final Widget? Function(BuildContext, int, T) itemBuilder;
final Widget? Function(BuildContext, int, T)? seperatorBuilder;
final double? spacing;
final bool isRefreshable; final bool isRefreshable;
final bool isSliver; final bool isSliver;
final bool showDefaultWidgets; final bool showDefaultWidgets;
@@ -28,6 +31,8 @@ class PaginationList<T> extends HookConsumerWidget {
required this.provider, required this.provider,
required this.notifier, required this.notifier,
required this.itemBuilder, required this.itemBuilder,
this.seperatorBuilder,
this.spacing,
this.isRefreshable = true, this.isRefreshable = true,
this.isSliver = false, this.isSliver = false,
this.showDefaultWidgets = true, this.showDefaultWidgets = true,
@@ -71,7 +76,7 @@ class PaginationList<T> extends HookConsumerWidget {
return SliverFillRemaining(child: content); return SliverFillRemaining(child: content);
} }
final listView = SuperSliverList.builder( final listView = SuperSliverList.separated(
itemCount: (data.value?.length ?? 0) + 1, itemCount: (data.value?.length ?? 0) + 1,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
if (idx == data.value?.length) { if (idx == data.value?.length) {
@@ -86,6 +91,20 @@ class PaginationList<T> extends HookConsumerWidget {
if (entry != null) return itemBuilder(context, idx, entry); if (entry != null) return itemBuilder(context, idx, entry);
return null; 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 return isRefreshable
@@ -126,7 +145,7 @@ class PaginationList<T> extends HookConsumerWidget {
return SizedBox(key: const ValueKey('error'), child: content); return SizedBox(key: const ValueKey('error'), child: content);
} }
final listView = SuperListView.builder( final listView = SuperListView.separated(
padding: padding, padding: padding,
itemCount: (data.value?.length ?? 0) + 1, itemCount: (data.value?.length ?? 0) + 1,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
@@ -142,6 +161,20 @@ class PaginationList<T> extends HookConsumerWidget {
if (entry != null) return itemBuilder(context, idx, entry); if (entry != null) return itemBuilder(context, idx, entry);
return null; 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( return SizedBox(

View File

@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:island/models/webfeed.dart'; import 'package:island/models/webfeed.dart';
import 'package:island/services/time.dart'; import 'package:island/services/time.dart';
import 'package:material_symbols_icons/symbols.dart';
class WebArticleCard extends StatelessWidget { class WebArticleCard extends StatelessWidget {
final SnWebArticle article; final SnWebArticle article;
@@ -22,9 +23,6 @@ class WebArticleCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return ConstrainedBox( return ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
child: Card( child: Card(
@@ -32,108 +30,41 @@ class WebArticleCard extends StatelessWidget {
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: InkWell( child: InkWell(
onTap: () => _onTap(context), onTap: () => _onTap(context),
child: AspectRatio( child: Column(
aspectRatio: 16 / 9, children: [
child: Stack( if (article.preview?.imageUrl != null)
fit: StackFit.expand, AspectRatio(
children: [ aspectRatio: 16 / 9,
// Image or fallback child: CachedNetworkImage(
article.preview?.imageUrl != null imageUrl: article.preview!.imageUrl!,
? CachedNetworkImage( fit: BoxFit.cover,
imageUrl: article.preview!.imageUrl!, width: double.infinity,
fit: BoxFit.cover, height: double.infinity,
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),
],
),
), ),
), ),
// Title ListTile(
Align( isThreeLine: true,
alignment: Alignment.bottomLeft, contentPadding: const EdgeInsets.symmetric(
child: Container( horizontal: 20,
padding: const EdgeInsets.only( vertical: 4,
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,
),
],
),
),
), ),
], 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,
),
],
),
),
],
), ),
), ),
), ),