82 lines
2.1 KiB
Dart
82 lines
2.1 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../theme/app_theme.dart';
|
|
|
|
/// User profile photo with a graceful fallback when the URL fails (e.g.
|
|
/// Google `lh3.googleusercontent.com` returning HTTP 429).
|
|
class ProfileAvatar extends StatelessWidget {
|
|
const ProfileAvatar({
|
|
super.key,
|
|
this.photoUrl,
|
|
this.radius = 36,
|
|
});
|
|
|
|
final String? photoUrl;
|
|
final double radius;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final String? url = photoUrl?.trim();
|
|
if (url == null || url.isEmpty) {
|
|
return _fallbackAvatar();
|
|
}
|
|
|
|
final double size = radius * 2;
|
|
return ClipOval(
|
|
child: SizedBox(
|
|
width: size,
|
|
height: size,
|
|
child: Image.network(
|
|
url,
|
|
width: size,
|
|
height: size,
|
|
fit: BoxFit.cover,
|
|
// Prevents NetworkImageLoadException from bubbling to the console
|
|
// when Google rate-limits profile photo URLs.
|
|
errorBuilder: (_, Object error, StackTrace? stackTrace) {
|
|
return _fallbackContents();
|
|
},
|
|
loadingBuilder: (
|
|
BuildContext context,
|
|
Widget child,
|
|
ImageChunkEvent? progress,
|
|
) {
|
|
if (progress == null) {
|
|
return child;
|
|
}
|
|
return ColoredBox(
|
|
color: AppColors.surfaceElevated,
|
|
child: Center(
|
|
child: SizedBox(
|
|
width: radius * 0.55,
|
|
height: radius * 0.55,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
color: AppColors.accent.withValues(alpha: 0.7),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _fallbackAvatar() {
|
|
return CircleAvatar(
|
|
radius: radius,
|
|
backgroundColor: AppColors.surfaceElevated,
|
|
child: _fallbackContents(),
|
|
);
|
|
}
|
|
|
|
Widget _fallbackContents() {
|
|
return Icon(
|
|
Icons.person,
|
|
size: radius,
|
|
color: AppColors.accent.withValues(alpha: 0.85),
|
|
);
|
|
}
|
|
}
|