fix: lazy-resolve image dimensions for providers without width/height metadata (Pawchive)
- 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
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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 = <String, (int, int)>{};
|
||||
|
||||
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,9 +1431,10 @@ class _PostInfoCard extends StatelessWidget {
|
||||
runSpacing: 8,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
if (dimensionLabel != null)
|
||||
_SpecBadge(
|
||||
icon: Icons.aspect_ratio_rounded,
|
||||
label: '${post.width} × ${post.height}',
|
||||
label: dimensionLabel,
|
||||
),
|
||||
if (post.fileType.isNotEmpty)
|
||||
_SpecBadge(
|
||||
@@ -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 {
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user