tasq/lib/widgets/profile_avatar.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;
}
}