import 'package:dicebear_core/dicebear_core.dart' show Avatar, Style; import 'package:dicebear_styles/thumbs.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import '../services/offer_thumbnail.dart' show decodeDataUri; import '../services/profile_cache.dart'; import 'avatar.dart'; import 'seed_glyph.dart'; /// A small round avatar for a person. When they've set an avatar ([picture] — a /// `data:` photo thumbnail or a `tane:seed:` illustration) it's shown; /// otherwise it falls back to a [GeneratedAvatar] drawn deterministically from /// their [pubkey]. Same pubkey → same picture on every device, so a person /// stays visually recognizable without sharing anything. class PeerAvatar extends StatelessWidget { const PeerAvatar({ required this.pubkey, this.name, this.picture, this.radius = 14, super.key, }); final String pubkey; final String? name; /// The person's chosen avatar value; null/empty → the [GeneratedAvatar]. final String? picture; final double radius; @override Widget build(BuildContext context) { final pic = picture?.trim() ?? ''; if (pic.startsWith('data:')) { final bytes = decodeDataUri(pic); if (bytes != null) { return CircleAvatar(radius: radius, backgroundImage: MemoryImage(bytes)); } } else if (pic.startsWith(avatarGlyphPrefix)) { final glyph = avatarGlyphChar(pic.substring(avatarGlyphPrefix.length)); if (glyph != null) { return CircleAvatar( radius: radius, backgroundColor: peerAvatarColor(pubkey), child: SeedGlyph(glyph, size: radius * 1.15, color: Colors.white), ); } } return GeneratedAvatar(pubkey: pubkey, name: name, radius: radius); } } /// The `thumbs` DiceBear style, parsed once — [Style.parse] validates the style /// definition against its schema, which is not free to repeat per build. final _thumbsStyle = Style.parse(thumbs); /// Rendered-SVG memo keyed by pubkey, so a person's avatar is generated once and /// reused across every rebuild and list row rather than re-rendered each time. final _svgCache = {}; /// Strips the `` block (the style's CC0 licence notice) that /// flutter_svg cannot parse and logs a warning for; CC0 requires no attribution /// in the rendered image, and the licence still ships with the package source. final _metadataElement = RegExp(r']*>.*?', dotAll: true); String _renderThumbs(String pubkey) => Avatar(_thumbsStyle, {'seed': pubkey}).svg.replaceAll(_metadataElement, ''); /// The deterministic default avatar for a person with no chosen [picture]: a /// friendly DiceBear "thumbs" face (CC0) seeded from their [pubkey], so the same /// key yields the same face on every device with nothing shared. Rendered as an /// SVG (no network, no assets). Exposes [pubkey] so call sites/tests can find it. class GeneratedAvatar extends StatelessWidget { const GeneratedAvatar({ required this.pubkey, this.name, this.radius = 14, super.key, }); final String pubkey; final String? name; final double radius; @override Widget build(BuildContext context) { final svg = _svgCache[pubkey] ??= _renderThumbs(pubkey); return Semantics( label: name, image: true, child: ClipOval( child: SizedBox.square( dimension: radius * 2, child: SvgPicture.string(svg, fit: BoxFit.cover), ), ), ); } } /// A [PeerAvatar] that looks the person's published avatar up from the /// [ProfileCache] (their kind:0 `picture`), falling back to the generated /// pattern while it loads or when none is cached / no cache is available. Use at /// list/row sites (one avatar each); for many avatars of the same few people /// (chat bubbles) resolve the picture once into state instead. class CachedAvatar extends StatelessWidget { const CachedAvatar({ required this.pubkey, this.name, this.cache, this.radius = 14, super.key, }); final String pubkey; final String? name; final ProfileCache? cache; final double radius; @override Widget build(BuildContext context) { final c = cache; if (c == null) { return PeerAvatar(pubkey: pubkey, name: name, radius: radius); } return FutureBuilder( future: c.picture(pubkey), builder: (context, snap) => PeerAvatar( pubkey: pubkey, name: name, picture: snap.data, radius: radius, ), ); } } /// A deterministic, readable avatar colour derived from [pubkey]. Hashes the /// key to a hue, then fixes saturation/lightness so white text stays legible on /// top. Pure and stable — exposed for testing. Color peerAvatarColor(String pubkey) { // FNV-1a over the code units — cheap, stable, well-spread across hues. var hash = 0x811c9dc5; for (final unit in pubkey.codeUnits) { hash = (hash ^ unit) * 0x01000193; hash &= 0xffffffff; } final hue = (hash % 360).toDouble(); return HSLColor.fromAHSL(1, hue, 0.45, 0.42).toColor(); }