2025-09-20 22:03:55 +05:30
|
|
|
import 'dart:convert';
|
|
|
|
|
import 'dart:typed_data';
|
|
|
|
|
|
|
|
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
|
|
|
|
import 'package:flutter/material.dart';
|
2025-10-23 17:36:31 +05:30
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
2025-09-20 22:03:55 +05:30
|
|
|
|
|
|
|
|
import '../services/brand_service.dart';
|
|
|
|
|
import '../theme/theme_extensions.dart';
|
2025-10-23 17:36:31 +05:30
|
|
|
import 'package:conduit/core/network/self_signed_image_cache_manager.dart';
|
|
|
|
|
import 'package:conduit/core/network/image_header_utils.dart';
|
2025-09-20 22:03:55 +05:30
|
|
|
|
|
|
|
|
typedef AvatarWidgetBuilder =
|
|
|
|
|
Widget Function(BuildContext context, double size);
|
|
|
|
|
|
2025-10-23 17:36:31 +05:30
|
|
|
class AvatarImage extends ConsumerWidget {
|
2025-09-20 22:03:55 +05:30
|
|
|
final double size;
|
|
|
|
|
final String? imageUrl;
|
|
|
|
|
final BorderRadius? borderRadius;
|
|
|
|
|
final AvatarWidgetBuilder fallbackBuilder;
|
|
|
|
|
final AvatarWidgetBuilder? placeholderBuilder;
|
|
|
|
|
|
|
|
|
|
const AvatarImage({
|
|
|
|
|
super.key,
|
|
|
|
|
required this.size,
|
|
|
|
|
required this.fallbackBuilder,
|
|
|
|
|
this.imageUrl,
|
|
|
|
|
this.borderRadius,
|
|
|
|
|
this.placeholderBuilder,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
BorderRadius get _radius => borderRadius ?? BorderRadius.circular(size / 2);
|
|
|
|
|
|
|
|
|
|
@override
|
2025-10-23 17:36:31 +05:30
|
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
2025-09-20 22:03:55 +05:30
|
|
|
final url = imageUrl?.trim();
|
|
|
|
|
if (url == null || url.isEmpty) {
|
|
|
|
|
return fallbackBuilder(context, size);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (url.startsWith('data:image')) {
|
|
|
|
|
final content = _decodeDataImage(url);
|
|
|
|
|
if (content != null) {
|
|
|
|
|
return ClipRRect(
|
|
|
|
|
borderRadius: _radius,
|
|
|
|
|
child: Image.memory(
|
|
|
|
|
content,
|
|
|
|
|
width: size,
|
|
|
|
|
height: size,
|
|
|
|
|
fit: BoxFit.cover,
|
|
|
|
|
errorBuilder: (context, error, stackTrace) =>
|
|
|
|
|
fallbackBuilder(context, size),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return fallbackBuilder(context, size);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-23 17:36:31 +05:30
|
|
|
// Build auth/custom headers when loading from network
|
|
|
|
|
final headers = buildImageHeadersFromWidgetRef(ref);
|
|
|
|
|
|
|
|
|
|
final cacheManager = ref.read(selfSignedImageCacheManagerProvider);
|
|
|
|
|
|
2025-09-20 22:03:55 +05:30
|
|
|
return ClipRRect(
|
|
|
|
|
borderRadius: _radius,
|
|
|
|
|
child: CachedNetworkImage(
|
|
|
|
|
imageUrl: url,
|
|
|
|
|
width: size,
|
|
|
|
|
height: size,
|
|
|
|
|
fit: BoxFit.cover,
|
2025-10-23 17:36:31 +05:30
|
|
|
cacheManager: cacheManager,
|
|
|
|
|
httpHeaders: headers,
|
2025-09-20 22:03:55 +05:30
|
|
|
placeholder: (context, _) =>
|
|
|
|
|
(placeholderBuilder ?? _defaultPlaceholder)(context, size),
|
|
|
|
|
errorWidget: (context, url, error) => fallbackBuilder(context, size),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AvatarWidgetBuilder get _defaultPlaceholder => (context, size) {
|
|
|
|
|
return Container(
|
|
|
|
|
width: size,
|
|
|
|
|
height: size,
|
|
|
|
|
alignment: Alignment.center,
|
|
|
|
|
color: context.conduitTheme.surfaceContainer.withValues(alpha: 0.35),
|
|
|
|
|
child: SizedBox(
|
|
|
|
|
width: size * 0.35,
|
|
|
|
|
height: size * 0.35,
|
|
|
|
|
child: CircularProgressIndicator(
|
|
|
|
|
strokeWidth: 2,
|
|
|
|
|
valueColor: AlwaysStoppedAnimation(
|
|
|
|
|
context.conduitTheme.buttonPrimary,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Uint8List? _decodeDataImage(String dataUrl) {
|
|
|
|
|
try {
|
|
|
|
|
final commaIndex = dataUrl.indexOf(',');
|
|
|
|
|
if (commaIndex == -1) return null;
|
|
|
|
|
final base64Data = dataUrl.substring(commaIndex + 1);
|
|
|
|
|
return base64Decode(base64Data);
|
|
|
|
|
} catch (_) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class UserAvatar extends StatelessWidget {
|
|
|
|
|
final double size;
|
|
|
|
|
final String? imageUrl;
|
|
|
|
|
final String? fallbackText;
|
|
|
|
|
|
|
|
|
|
const UserAvatar({
|
|
|
|
|
super.key,
|
|
|
|
|
required this.size,
|
|
|
|
|
this.imageUrl,
|
|
|
|
|
this.fallbackText,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return AvatarImage(
|
|
|
|
|
size: size,
|
|
|
|
|
imageUrl: imageUrl,
|
|
|
|
|
fallbackBuilder: (context, size) => BrandService.createBrandAvatar(
|
|
|
|
|
size: size,
|
|
|
|
|
fallbackText: fallbackText,
|
|
|
|
|
context: context,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|