🐛 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 {
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<List<SnWebArticle>>
queryParameters: queryParams,
);
final articles =
response.data
.map(
(json) => SnWebArticle.fromJson(json as Map<String, dynamic>),
)
.cast<SnWebArticle>()
.toList();
final articles = response.data
.map((json) => SnWebArticle.fromJson(json as Map<String, dynamic>))
.cast<SnWebArticle>()
.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')),
),
);
}
}

View File

@@ -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<SnWebArticle>()
.toList();
final articles = response.data
.map((json) => SnWebArticle.fromJson(json))
.cast<SnWebArticle>()
.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(),
),
),
),
],

View File

@@ -38,11 +38,10 @@ class MarketplaceWebFeedsNotifier extends AsyncNotifier<List<SnWebFeed>>
);
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final feeds =
response.data
.map((e) => SnWebFeed.fromJson(e))
.cast<SnWebFeed>()
.toList();
final feeds = response.data
.map((e) => SnWebFeed.fromJson(e))
.cast<SnWebFeed>()
.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),

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_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<T> extends HookConsumerWidget {
final ProviderListenable<AsyncValue<List<T>>> provider;
final Refreshable<PaginationController<T>> 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<T> 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<T> 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<T> 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<T> 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<T> 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(

View File

@@ -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,
),
],
),
),
],
),
),
),