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 = [
|
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(
|
LunarisChange(
|
||||||
version: '2.3.6',
|
version: '2.3.6',
|
||||||
title: 'Pawchive Accounts Integration & Favorites Synchronization',
|
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({
|
const _PostInfoCard({
|
||||||
required this.post,
|
required this.post,
|
||||||
required this.strings,
|
required this.strings,
|
||||||
@@ -1198,8 +1198,90 @@ class _PostInfoCard extends StatelessWidget {
|
|||||||
final DownloadedMedia? localMedia;
|
final DownloadedMedia? localMedia;
|
||||||
final int? fileSizeBytes;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
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 theme = Theme.of(context);
|
||||||
final scheme = theme.colorScheme;
|
final scheme = theme.colorScheme;
|
||||||
final artists = post.tagGroups['artist'] ??
|
final artists = post.tagGroups['artist'] ??
|
||||||
@@ -1208,6 +1290,14 @@ class _PostInfoCard extends StatelessWidget {
|
|||||||
.map((t) => t.replaceFirst('artist:', ''))
|
.map((t) => t.replaceFirst('artist:', ''))
|
||||||
.toList();
|
.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(
|
return Container(
|
||||||
padding: const EdgeInsets.all(14),
|
padding: const EdgeInsets.all(14),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -1293,7 +1383,7 @@ class _PostInfoCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
_formatDate(post.createdAt),
|
_formatPostDate(post.createdAt),
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: scheme.onSurfaceVariant,
|
color: scheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
@@ -1321,8 +1411,8 @@ class _PostInfoCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
fileSizeBytes != null && fileSizeBytes! > 0
|
fileSizeBytes != null && fileSizeBytes > 0
|
||||||
? DownloadedMediaService.formatBytes(fileSizeBytes!)
|
? DownloadedMediaService.formatBytes(fileSizeBytes)
|
||||||
: (strings.ru ? 'Офлайн' : 'Offline'),
|
: (strings.ru ? 'Офлайн' : 'Offline'),
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
color: scheme.onPrimaryContainer,
|
color: scheme.onPrimaryContainer,
|
||||||
@@ -1341,9 +1431,10 @@ class _PostInfoCard extends StatelessWidget {
|
|||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
if (dimensionLabel != null)
|
||||||
_SpecBadge(
|
_SpecBadge(
|
||||||
icon: Icons.aspect_ratio_rounded,
|
icon: Icons.aspect_ratio_rounded,
|
||||||
label: '${post.width} × ${post.height}',
|
label: dimensionLabel,
|
||||||
),
|
),
|
||||||
if (post.fileType.isNotEmpty)
|
if (post.fileType.isNotEmpty)
|
||||||
_SpecBadge(
|
_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 {
|
class _SpecBadge extends StatelessWidget {
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
name: gel_rule_app
|
name: gel_rule_app
|
||||||
description: Local backend/core layer for a Flutter booru browser app.
|
description: Local backend/core layer for a Flutter booru browser app.
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 2.3.6+46
|
version: 2.3.7+47
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.3.0 <4.0.0"
|
sdk: ">=3.3.0 <4.0.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user