Release v3.7.3: Add artist posts row and fix comic page indicator
Build and Release / Build Windows x64 (push) Has been cancelled
Build and Release / Build macOS (push) Has been cancelled

This commit is contained in:
2026-09-05 21:27:29 +03:00
parent 5434c5dc7b
commit 3c91af8790
2 changed files with 179 additions and 9 deletions
@@ -48,6 +48,18 @@ final postProviderInstanceProvider =
return ref.watch(providerManagerProvider).getProviderInstance(providerId);
});
/// Fetches up to 6 recent posts from the same artist.
final artistPostsProvider =
FutureProvider.family<List<Post>, String>((ref, artistTag) async {
if (artistTag.isEmpty) return const [];
final result = await ref.watch(feedServiceProvider).refresh(
tags: [artistTag],
limit: 7,
);
if (result is! Success<List<Post>>) return const [];
return (result as Success<List<Post>>).data.take(6).toList(growable: false);
});
class PostDetailsScreen extends ConsumerWidget {
const PostDetailsScreen({
required this.providerId,
@@ -377,6 +389,7 @@ class PostDetailsScreen extends ConsumerWidget {
fileSizeBytes: fileSizeBytes,
e621Provider: e621Provider,
),
_ArtistPostsCard(post: post),
if (post.hasRelations) ...[
const SizedBox(height: 16),
PostRelationsCard(
@@ -638,6 +651,7 @@ class PostDetailsScreen extends ConsumerWidget {
fileSizeBytes: fileSizeBytes,
e621Provider: e621Provider,
),
_ArtistPostsCard(post: post),
if (post.hasRelations) ...[
const SizedBox(height: 12),
PostRelationsCard(
@@ -1204,7 +1218,10 @@ class _MobilePostPagerState extends State<_MobilePostPager> {
if (widget.posts.length <= 1) return pager;
// Show the page indicator overlay only for comic/pool lists.
final isComic = widget.posts.first.hasPools;
if (!isComic) return pager;
return Stack(
children: [
pager,
@@ -1232,15 +1249,11 @@ class _MobilePostPagerState extends State<_MobilePostPager> {
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isComic) ...[
const Icon(Icons.auto_stories_rounded,
size: 14, color: Colors.white),
const SizedBox(width: 6),
],
const Icon(Icons.auto_stories_rounded,
size: 14, color: Colors.white),
const SizedBox(width: 6),
Text(
isComic
? 'Комикс • Стр. ${_currentPage + 1} из ${widget.posts.length}'
: '${_currentPage + 1} / ${widget.posts.length}',
'Комикс • Стр. ${_currentPage + 1} из ${widget.posts.length}',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
@@ -1396,6 +1409,163 @@ class _DownloadIntent extends Intent {
const _DownloadIntent();
}
// ---------------------------------------------------------------------------
// Artist posts card
// ---------------------------------------------------------------------------
class _ArtistPostsCard extends ConsumerWidget {
const _ArtistPostsCard({required this.post});
final Post post;
String _artistTag(Post post) {
final artists = post.tagGroups['artist'] ??
post.tags
.where((t) => t.startsWith('artist:'))
.map((t) => t.replaceFirst('artist:', ''))
.toList();
if (artists.isEmpty) return '';
return 'artist:${artists.first}';
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final tag = _artistTag(post);
if (tag.isEmpty) return const SizedBox.shrink();
final artistPosts = ref.watch(artistPostsProvider(tag));
final isRu = Localizations.maybeLocaleOf(context)?.languageCode == 'ru';
final scheme = Theme.of(context).colorScheme;
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final artistName = tag.replaceFirst('artist:', '');
return artistPosts.when(
loading: () => const SizedBox.shrink(),
error: (_, __) => const SizedBox.shrink(),
data: (posts) {
if (posts.isEmpty) return const SizedBox.shrink();
// exclude the current post
final filtered =
posts.where((p) => p.cacheKey != post.cacheKey).take(6).toList();
if (filtered.isEmpty) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.only(top: 12),
child: Container(
decoration: BoxDecoration(
color: isDark
? scheme.surfaceContainerHigh.withValues(alpha: 0.70)
: scheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isDark
? Colors.white.withValues(alpha: 0.12)
: scheme.outlineVariant.withValues(alpha: 0.40),
width: 1.1,
),
),
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.palette_rounded,
size: 18, color: scheme.primary),
const SizedBox(width: 8),
Expanded(
child: Text(
isRu
? 'Ещё от $artistName'
: 'More by $artistName',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
InkWell(
onTap: () => context.push(
'/search?q=${Uri.encodeComponent(tag)}',
),
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
isRu ? 'Все' : 'See all',
style: theme.textTheme.labelSmall?.copyWith(
color: scheme.primary,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 3),
Icon(Icons.arrow_forward_ios_rounded,
size: 11, color: scheme.primary),
],
),
),
),
],
),
const SizedBox(height: 12),
SizedBox(
height: 110,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: filtered.length,
separatorBuilder: (_, __) => const SizedBox(width: 8),
itemBuilder: (context, index) {
final p = filtered[index];
return InkWell(
onTap: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => PostDetailsScreen(
providerId: p.providerId,
postId: p.id,
initialPost: p,
),
),
),
borderRadius: BorderRadius.circular(12),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: CachedNetworkImage(
imageUrl: p.previewUrl.isNotEmpty
? p.previewUrl
: p.sampleUrl,
width: 110,
height: 110,
fit: BoxFit.cover,
memCacheWidth: 220,
memCacheHeight: 220,
errorWidget: (_, __, ___) => Container(
width: 110,
height: 110,
color: scheme.surfaceContainerHigh,
child: Icon(Icons.broken_image_rounded,
color: scheme.onSurfaceVariant),
),
),
),
);
},
),
),
],
),
),
);
},
);
}
}
class _CommentsSection extends ConsumerStatefulWidget {
const _CommentsSection({required this.post});
+1 -1
View File
@@ -1,7 +1,7 @@
name: gel_rule_app
description: Local backend/core layer for a Flutter booru browser app.
publish_to: "none"
version: 3.7.2+90
version: 3.7.3+91
environment:
sdk: ">=3.3.0 <4.0.0"