103 lines
2.9 KiB
Dart
103 lines
2.9 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
/// Native Flutter profile avatar that displays either:
|
|
/// 1. User's avatar image URL (if provided)
|
|
/// 2. Initials derived from full name (fallback)
|
|
///
|
|
/// Pass [heroTag] to participate in a Hero transition to/from a destination
|
|
/// that uses the same tag (e.g., the profile screen's large avatar).
|
|
class ProfileAvatar extends StatelessWidget {
|
|
const ProfileAvatar({
|
|
super.key,
|
|
required this.fullName,
|
|
this.avatarUrl,
|
|
this.radius = 18,
|
|
this.heroTag,
|
|
});
|
|
|
|
final String fullName;
|
|
final String? avatarUrl;
|
|
final double radius;
|
|
|
|
/// When non-null, wraps the avatar in a [Hero] with this tag.
|
|
final Object? heroTag;
|
|
|
|
String _getInitials() {
|
|
final trimmed = fullName.trim();
|
|
if (trimmed.isEmpty) return 'U';
|
|
|
|
final parts = trimmed.split(RegExp(r'\s+'));
|
|
if (parts.length == 1) {
|
|
return parts[0].substring(0, 1).toUpperCase();
|
|
}
|
|
|
|
// Get first letter of first and last name
|
|
return '${parts.first[0]}${parts.last[0]}'.toUpperCase();
|
|
}
|
|
|
|
/// Returns a (background, foreground) pair from the M3 tonal palette.
|
|
///
|
|
/// Uses a deterministic hash of the initials to cycle through the scheme's
|
|
/// semantic container colors so every avatar is theme-aware and accessible.
|
|
(Color, Color) _getTonalColors(String initials, ColorScheme cs) {
|
|
final hash =
|
|
initials.codeUnitAt(0) +
|
|
(initials.length > 1 ? initials.codeUnitAt(1) * 256 : 0);
|
|
|
|
// Six M3-compliant container pairs (background / on-color text).
|
|
final pairs = [
|
|
(cs.primaryContainer, cs.onPrimaryContainer),
|
|
(cs.secondaryContainer, cs.onSecondaryContainer),
|
|
(cs.tertiaryContainer, cs.onTertiaryContainer),
|
|
(cs.errorContainer, cs.onErrorContainer),
|
|
(cs.primary, cs.onPrimary),
|
|
(cs.secondary, cs.onSecondary),
|
|
];
|
|
|
|
return pairs[hash % pairs.length];
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final initials = _getInitials();
|
|
|
|
Widget avatar;
|
|
|
|
// If avatar URL is provided, attempt to load the image
|
|
if (avatarUrl != null && avatarUrl!.isNotEmpty) {
|
|
avatar = CircleAvatar(
|
|
radius: radius,
|
|
backgroundImage: NetworkImage(avatarUrl!),
|
|
onBackgroundImageError: (_, _) {
|
|
// Silently fall back to initials if image fails
|
|
},
|
|
child: null, // Image will display if loaded successfully
|
|
);
|
|
} else {
|
|
final (bg, fg) = _getTonalColors(
|
|
initials,
|
|
Theme.of(context).colorScheme,
|
|
);
|
|
|
|
// Fallback to initials
|
|
avatar = CircleAvatar(
|
|
radius: radius,
|
|
backgroundColor: bg,
|
|
child: Text(
|
|
initials,
|
|
style: TextStyle(
|
|
color: fg,
|
|
fontSize: radius * 0.8,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (heroTag != null) {
|
|
return Hero(tag: heroTag!, child: avatar);
|
|
}
|
|
return avatar;
|
|
}
|
|
}
|