144 lines
4.3 KiB
Dart
144 lines
4.3 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
|
import '../theme/theme_extensions.dart';
|
|
import 'improved_loading_states.dart';
|
|
|
|
/// Cached network image widget with progressive loading and error handling
|
|
class CachedImage extends StatelessWidget {
|
|
final String imageUrl;
|
|
final double? width;
|
|
final double? height;
|
|
final BoxFit fit;
|
|
final Widget? placeholder;
|
|
final Widget? errorWidget;
|
|
final Duration fadeInDuration;
|
|
final Duration fadeOutDuration;
|
|
final bool enableMemoryCache;
|
|
final int? maxWidthDiskCache;
|
|
final int? maxHeightDiskCache;
|
|
|
|
const CachedImage({
|
|
super.key,
|
|
required this.imageUrl,
|
|
this.width,
|
|
this.height,
|
|
this.fit = BoxFit.cover,
|
|
this.placeholder,
|
|
this.errorWidget,
|
|
this.fadeInDuration = const Duration(milliseconds: 300),
|
|
this.fadeOutDuration = const Duration(milliseconds: 100),
|
|
this.enableMemoryCache = true,
|
|
this.maxWidthDiskCache,
|
|
this.maxHeightDiskCache,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return CachedNetworkImage(
|
|
imageUrl: imageUrl,
|
|
width: width,
|
|
height: height,
|
|
fit: fit,
|
|
fadeInDuration: fadeInDuration,
|
|
fadeOutDuration: fadeOutDuration,
|
|
placeholder: placeholder != null
|
|
? (context, url) => placeholder!
|
|
: _buildDefaultPlaceholder,
|
|
errorWidget: errorWidget != null
|
|
? (context, url, error) => errorWidget!
|
|
: _buildDefaultErrorWidget,
|
|
memCacheWidth: enableMemoryCache ? width?.toInt() : null,
|
|
memCacheHeight: enableMemoryCache ? height?.toInt() : null,
|
|
maxWidthDiskCache: maxWidthDiskCache,
|
|
maxHeightDiskCache: maxHeightDiskCache,
|
|
useOldImageOnUrlChange: true,
|
|
filterQuality: FilterQuality.medium,
|
|
);
|
|
}
|
|
|
|
Widget _buildDefaultPlaceholder(BuildContext context, String url) {
|
|
return ShimmerLoader(
|
|
width: width ?? double.infinity,
|
|
height: height ?? 200,
|
|
borderRadius: BorderRadius.circular(8),
|
|
);
|
|
}
|
|
|
|
Widget _buildDefaultErrorWidget(
|
|
BuildContext context,
|
|
String url,
|
|
dynamic error,
|
|
) {
|
|
return Container(
|
|
width: width,
|
|
height: height,
|
|
color: context.conduitTheme.shimmerBase,
|
|
child: Icon(
|
|
Icons.broken_image,
|
|
color: context.conduitTheme.iconSecondary,
|
|
size: (width != null && height != null)
|
|
? (width! < height! ? width! * 0.5 : height! * 0.5)
|
|
: 24,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Cached circular avatar with progressive loading
|
|
class CachedAvatar extends StatelessWidget {
|
|
final String? imageUrl;
|
|
final String fallbackText;
|
|
final double radius;
|
|
final Color? backgroundColor;
|
|
final Color? textColor;
|
|
|
|
const CachedAvatar({
|
|
super.key,
|
|
this.imageUrl,
|
|
required this.fallbackText,
|
|
this.radius = 20,
|
|
this.backgroundColor,
|
|
this.textColor,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return CircleAvatar(
|
|
radius: radius,
|
|
backgroundColor:
|
|
backgroundColor ?? context.conduitTheme.surfaceBackground,
|
|
child: imageUrl != null
|
|
? ClipOval(
|
|
child: CachedNetworkImage(
|
|
imageUrl: imageUrl!,
|
|
width: radius * 2,
|
|
height: radius * 2,
|
|
fit: BoxFit.cover,
|
|
placeholder: (context, url) => CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
color: textColor ?? context.conduitTheme.iconSecondary,
|
|
),
|
|
errorWidget: (context, url, error) => Text(
|
|
fallbackText.isNotEmpty ? fallbackText[0].toUpperCase() : '?',
|
|
style: TextStyle(
|
|
color: textColor ?? context.conduitTheme.textPrimary,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: radius * 0.6,
|
|
),
|
|
),
|
|
memCacheWidth: (radius * 2).toInt(),
|
|
memCacheHeight: (radius * 2).toInt(),
|
|
),
|
|
)
|
|
: Text(
|
|
fallbackText.isNotEmpty ? fallbackText[0].toUpperCase() : '?',
|
|
style: TextStyle(
|
|
color: textColor ?? context.conduitTheme.textPrimary,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: radius * 0.6,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|