Files
Ensemble/lib/widgets/cached_image.dart
ChrisandClaude Opus 4.6 e91bdf2353 Add detail track cache, image prefetch, and offline caching improvements
Cache album/playlist tracks in a new DetailTrackCache DB table (schema v7)
so detail screens load instantly on revisit. Prefetch album artwork after
sync via ImagePrefetchService with batched downloads and LRU eviction.
Expand sync_service and cache_service to support the new caching layers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 12:42:43 +00:00

155 lines
4.3 KiB
Dart

import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
/// A wrapper around CachedNetworkImage for consistent image caching throughout the app.
///
/// Benefits over NetworkImage:
/// - Disk caching (persists across app restarts)
/// - Memory caching with configurable limits
/// - Placeholder and error widgets
/// - Fade-in animation
class CachedImage extends StatelessWidget {
final String imageUrl;
final double? width;
final double? height;
final BoxFit fit;
final Widget? placeholder;
final Widget? errorWidget;
final int? cacheWidth;
final int? cacheHeight;
final BorderRadius? borderRadius;
final BaseCacheManager? cacheManager;
const CachedImage({
super.key,
required this.imageUrl,
this.width,
this.height,
this.fit = BoxFit.cover,
this.placeholder,
this.errorWidget,
this.cacheWidth,
this.cacheHeight,
this.borderRadius,
this.cacheManager,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
Widget image = CachedNetworkImage(
imageUrl: imageUrl,
width: width,
height: height,
fit: fit,
memCacheWidth: cacheWidth,
memCacheHeight: cacheHeight,
cacheManager: cacheManager,
fadeInDuration: const Duration(milliseconds: 150),
fadeOutDuration: const Duration(milliseconds: 150),
placeholder: (context, url) => placeholder ?? Container(
color: colorScheme.surfaceVariant,
child: Center(
child: CircularProgressIndicator(
strokeWidth: 2,
color: colorScheme.onSurfaceVariant.withOpacity(0.5),
),
),
),
errorWidget: (context, url, error) => errorWidget ?? Container(
color: colorScheme.surfaceVariant,
child: Icon(
Icons.broken_image_rounded,
color: colorScheme.onSurfaceVariant,
),
),
);
if (borderRadius != null) {
image = ClipRRect(
borderRadius: borderRadius!,
child: image,
);
}
return image;
}
}
/// A circular cached image, perfect for artist avatars
class CachedCircleAvatar extends StatelessWidget {
final String? imageUrl;
final double radius;
final Widget? fallbackIcon;
final Color? backgroundColor;
const CachedCircleAvatar({
super.key,
this.imageUrl,
this.radius = 24,
this.fallbackIcon,
this.backgroundColor,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final bgColor = backgroundColor ?? colorScheme.surfaceVariant;
if (imageUrl == null || imageUrl!.isEmpty) {
return CircleAvatar(
radius: radius,
backgroundColor: bgColor,
child: fallbackIcon ?? Icon(
Icons.person_rounded,
color: colorScheme.onSurfaceVariant,
size: radius,
),
);
}
return CachedNetworkImage(
imageUrl: imageUrl!,
memCacheWidth: (radius * 4).toInt(),
memCacheHeight: (radius * 4).toInt(),
fadeInDuration: const Duration(milliseconds: 150),
fadeOutDuration: const Duration(milliseconds: 150),
imageBuilder: (context, imageProvider) => CircleAvatar(
radius: radius,
backgroundColor: bgColor,
backgroundImage: imageProvider,
),
placeholder: (context, url) => CircleAvatar(
radius: radius,
backgroundColor: bgColor,
child: SizedBox(
width: radius,
height: radius,
child: CircularProgressIndicator(
strokeWidth: 2,
color: colorScheme.onSurfaceVariant.withOpacity(0.5),
),
),
),
errorWidget: (context, url, error) => CircleAvatar(
radius: radius,
backgroundColor: bgColor,
child: fallbackIcon ?? Icon(
Icons.person_rounded,
color: colorScheme.onSurfaceVariant,
size: radius,
),
),
);
}
}
/// Get a CachedNetworkImageProvider for use with DecorationImage, CircleAvatar, etc.
/// This is useful when you need an ImageProvider rather than a widget.
CachedNetworkImageProvider? cachedImageProvider(String? imageUrl) {
if (imageUrl == null || imageUrl.isEmpty) return null;
return CachedNetworkImageProvider(imageUrl);
}