import 'package:flutter/material.dart'; import 'package:flutter_boring_avatars/flutter_boring_avatars.dart'; import '../services/offer_thumbnail.dart' show decodeDataUri; import '../services/profile_cache.dart'; import 'avatar.dart'; import 'seed_glyph.dart'; import 'theme.dart'; /// Palette for the generated default avatars — earthy seed-and-leaf tones so /// the fallback art sits inside the app's palette rather than fighting it. const _generatedAvatarPalette = BoringAvatarPalette([ seedGreen, seedRating, // warm amber Color(0xFFB5541F), // terracotta pot Color(0xFF6B4F2A), // seed-coat brown Color(0xFFD3E8C8), // pale leaf ]); /// 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 generated pattern drawn deterministically from /// their [pubkey] (boring-avatars "beam"). 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 coloured-initial disc. 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 Semantics( label: name, image: true, child: SizedBox.square( dimension: radius * 2, child: BoringAvatar( name: pubkey, type: BoringAvatarType.beam, palette: _generatedAvatarPalette, shape: const OvalBorder(), ), ), ); } } /// 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(); }