🐛 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,11 +46,8 @@ 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(
(json) => SnWebArticle.fromJson(json as Map<String, dynamic>),
)
.cast<SnWebArticle>() .cast<SnWebArticle>()
.toList(); .toList();
@@ -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,14 +187,12 @@ 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: error: (err, stack) => AppScaffold(
(err, stack) => AppScaffold(
isNoBackground: false, isNoBackground: false,
appBar: AppBar(title: const Text('Articles')), appBar: AppBar(title: const Text('Articles')),
body: Center(child: Text('Error: $err')), body: Center(child: Text('Error: $err')),

View File

@@ -44,8 +44,7 @@ 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();
@@ -116,8 +115,7 @@ 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: [ children: [
Text(data.description ?? 'descriptionNone'.tr()), Text(data.description ?? 'descriptionNone'.tr()),
@@ -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,10 +165,8 @@ 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:
isSubscribed ? unsubscribeFromFeed : subscribeToFeed,
icon: Icon( icon: Icon(
isSubscribed ? Symbols.remove_circle : Symbols.add_circle, isSubscribed ? Symbols.remove_circle : Symbols.add_circle,
), ),
@@ -176,14 +174,12 @@ class MarketplaceWebFeedDetailScreen extends HookConsumerWidget {
isSubscribed ? 'unsubscribe'.tr() : 'subscribe'.tr(), isSubscribed ? 'unsubscribe'.tr() : 'subscribe'.tr(),
), ),
), ),
loading: loading: () => const SizedBox(
() => const SizedBox(
height: 32, height: 32,
width: 32, width: 32,
child: CircularProgressIndicator(strokeWidth: 2), child: CircularProgressIndicator(strokeWidth: 2),
), ).center(),
error: error: (_, _) => OutlinedButton.icon(
(_, _) => OutlinedButton.icon(
onPressed: subscribeToFeed, onPressed: subscribeToFeed,
icon: const Icon(Symbols.add_circle), icon: const Icon(Symbols.add_circle),
label: Text('subscribe').tr(), label: Text('subscribe').tr(),

View File

@@ -38,8 +38,7 @@ 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();
@@ -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,111 +30,44 @@ 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,
child: Stack(
fit: StackFit.expand,
children: [ children: [
// Image or fallback if (article.preview?.imageUrl != null)
article.preview?.imageUrl != null AspectRatio(
? CachedNetworkImage( aspectRatio: 16 / 9,
child: CachedNetworkImage(
imageUrl: article.preview!.imageUrl!, imageUrl: article.preview!.imageUrl!,
fit: BoxFit.cover, fit: BoxFit.cover,
width: double.infinity, width: double.infinity,
height: double.infinity, height: double.infinity,
)
: ColoredBox(
color: colorScheme.secondaryContainer,
child: const Center(
child: Icon(
Icons.article_outlined,
size: 48,
color: Colors.white,
), ),
), ),
ListTile(
isThreeLine: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 4,
), ),
// Gradient overlay trailing: const Icon(Symbols.chevron_right),
Container( title: Text(article.title),
decoration: BoxDecoration( subtitle: Column(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withOpacity(0.7),
],
),
),
),
// Title
Align(
alignment: Alignment.bottomLeft,
child: Container(
padding: const EdgeInsets.only(
left: 12,
right: 12,
bottom: 8,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [ children: [
if (showDetails)
const SizedBox(height: 8)
else
Spacer(),
Text( Text(
article.title, '${article.createdAt.formatSystem()} · ${article.createdAt.formatRelative(context)}',
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( Text(
article.feed?.title ?? 'Unknown Source', article.feed?.title ?? 'Unknown Source',
style: const TextStyle(
fontSize: 9,
color: Colors.white70,
),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
], ],
), ),
), ),
),
], ],
), ),
), ),
), ),
),
); );
} }
} }