From 848fee6ff1f3c5068599162eb3619a22ed56eb25 Mon Sep 17 00:00:00 2001 From: RarDog Date: Fri, 4 Sep 2026 14:42:07 +0300 Subject: [PATCH] fix: lazy-resolve image dimensions for providers without width/height metadata (Pawchive) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Convert _PostInfoCard to StatefulWidget - Lazily decode real pixel dimensions (e.g. 1920×1080) from network when post.width==0 && post.height==0 - Show '… × …' while resolving, actual size once decoded - Hide resolution badge for video/link posts where dimensions can't be resolved - In-memory cache prevents redundant re-downloads - Bump version to 2.3.7+47 --- lib/app/changelog.dart | 9 ++ .../presentation/post_details_screen.dart | 113 ++++++++++++++++-- pubspec.yaml | 2 +- 3 files changed, 112 insertions(+), 12 deletions(-) diff --git a/lib/app/changelog.dart b/lib/app/changelog.dart index 3715085..32b27f4 100644 --- a/lib/app/changelog.dart +++ b/lib/app/changelog.dart @@ -1,4 +1,13 @@ const lunarisChangelog = [ + LunarisChange( + version: '2.3.7', + title: 'Lazy Media Dimensions for Pawchive & Unknown-Size Providers', + bullets: [ + 'Auto-Resolve Image Dimensions: when a provider (such as Pawchive) does not supply width/height metadata, the app now lazily resolves the real pixel dimensions by decoding the image in the background and displays them as soon as they are ready.', + 'No More "0 × 0": the resolution badge now shows "… × …" while dimensions are being fetched, and the correct resolution (e.g. 1920 × 1080) once decoded. The badge is hidden entirely for video/link posts where dimensions cannot be resolved.', + 'Session Cache: resolved image dimensions are cached in memory for the lifetime of the app so repeated visits to the same post do not trigger re-downloads.', + ], + ), LunarisChange( version: '2.3.6', title: 'Pawchive Accounts Integration & Favorites Synchronization', diff --git a/lib/features/post/presentation/post_details_screen.dart b/lib/features/post/presentation/post_details_screen.dart index 88f6be3..a664b84 100644 --- a/lib/features/post/presentation/post_details_screen.dart +++ b/lib/features/post/presentation/post_details_screen.dart @@ -1185,7 +1185,7 @@ class _CommentsSectionState extends ConsumerState<_CommentsSection> { } } -class _PostInfoCard extends StatelessWidget { +class _PostInfoCard extends StatefulWidget { const _PostInfoCard({ required this.post, required this.strings, @@ -1198,8 +1198,90 @@ class _PostInfoCard extends StatelessWidget { final DownloadedMedia? localMedia; final int? fileSizeBytes; + @override + State<_PostInfoCard> createState() => _PostInfoCardState(); +} + +class _PostInfoCardState extends State<_PostInfoCard> { + // Lazily resolved dimensions (used when post.width/height == 0). + int? _resolvedWidth; + int? _resolvedHeight; + bool _resolvingDimensions = false; + + static final _dimensionCache = {}; + + static bool _isImageType(String fileType) { + const imageTypes = {'image', 'jpeg', 'jpg', 'png', 'gif', 'webp', 'avif'}; + return imageTypes.contains(fileType.toLowerCase()); + } + + @override + void initState() { + super.initState(); + _maybeResolveDimensions(); + } + + @override + void didUpdateWidget(_PostInfoCard old) { + super.didUpdateWidget(old); + if (old.post.fileUrl != widget.post.fileUrl) { + _resolvedWidth = null; + _resolvedHeight = null; + _resolvingDimensions = false; + _maybeResolveDimensions(); + } + } + + void _maybeResolveDimensions() { + final post = widget.post; + // Only attempt lazy resolution when dimensions are unknown (0) and it's + // an image (not video/link which we can't resolve this way). + if (post.width != 0 || post.height != 0) return; + if (!_isImageType(post.fileType)) return; + final url = post.fileUrl.isNotEmpty ? post.fileUrl : post.sampleUrl; + if (url.isEmpty) return; + + // Check cache first. + if (_dimensionCache.containsKey(url)) { + final cached = _dimensionCache[url]!; + _resolvedWidth = cached.$1; + _resolvedHeight = cached.$2; + return; + } + + if (_resolvingDimensions) return; + _resolvingDimensions = true; + + final imageProvider = NetworkImage(url); + final completer = imageProvider.resolve(ImageConfiguration.empty); + completer.addListener( + ImageStreamListener( + (info, _) { + final w = info.image.width; + final h = info.image.height; + _dimensionCache[url] = (w, h); + if (mounted) { + setState(() { + _resolvedWidth = w; + _resolvedHeight = h; + _resolvingDimensions = false; + }); + } + }, + onError: (_, __) { + if (mounted) setState(() => _resolvingDimensions = false); + }, + ), + ); + } + @override Widget build(BuildContext context) { + final post = widget.post; + final strings = widget.strings; + final localMedia = widget.localMedia; + final fileSizeBytes = widget.fileSizeBytes; + final theme = Theme.of(context); final scheme = theme.colorScheme; final artists = post.tagGroups['artist'] ?? @@ -1208,6 +1290,14 @@ class _PostInfoCard extends StatelessWidget { .map((t) => t.replaceFirst('artist:', '')) .toList(); + // Determine which dimensions to display. + final displayWidth = post.width != 0 ? post.width : _resolvedWidth; + final displayHeight = post.height != 0 ? post.height : _resolvedHeight; + final hasDimensions = displayWidth != null && displayHeight != null; + final dimensionLabel = hasDimensions + ? '$displayWidth × $displayHeight' + : (_resolvingDimensions ? '… × …' : null); + return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( @@ -1293,7 +1383,7 @@ class _PostInfoCard extends StatelessWidget { ), ), Text( - _formatDate(post.createdAt), + _formatPostDate(post.createdAt), style: theme.textTheme.bodySmall?.copyWith( color: scheme.onSurfaceVariant, ), @@ -1321,8 +1411,8 @@ class _PostInfoCard extends StatelessWidget { ), const SizedBox(width: 4), Text( - fileSizeBytes != null && fileSizeBytes! > 0 - ? DownloadedMediaService.formatBytes(fileSizeBytes!) + fileSizeBytes != null && fileSizeBytes > 0 + ? DownloadedMediaService.formatBytes(fileSizeBytes) : (strings.ru ? 'Офлайн' : 'Offline'), style: theme.textTheme.labelSmall?.copyWith( color: scheme.onPrimaryContainer, @@ -1341,10 +1431,11 @@ class _PostInfoCard extends StatelessWidget { runSpacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [ - _SpecBadge( - icon: Icons.aspect_ratio_rounded, - label: '${post.width} × ${post.height}', - ), + if (dimensionLabel != null) + _SpecBadge( + icon: Icons.aspect_ratio_rounded, + label: dimensionLabel, + ), if (post.fileType.isNotEmpty) _SpecBadge( icon: Icons.insert_drive_file_outlined, @@ -1373,10 +1464,10 @@ class _PostInfoCard extends StatelessWidget { ), ); } +} - static String _formatDate(DateTime date) { - return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; - } +String _formatPostDate(DateTime date) { + return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; } class _SpecBadge extends StatelessWidget { diff --git a/pubspec.yaml b/pubspec.yaml index b6e0970..eb717a5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: gel_rule_app description: Local backend/core layer for a Flutter booru browser app. publish_to: "none" -version: 2.3.6+46 +version: 2.3.7+47 environment: sdk: ">=3.3.0 <4.0.0"