Show real photos for people instead of scene art

Your avatar is your photo, picked from the device by tapping it on Home or
from a new Profile photo row in Settings, shrunk to 256 px and kept on the
phone (there is no upload endpoint yet). Group members show the photo the
server sends in a new `photo` field, everywhere avatars appear. Without a
photo, an initial on a neutral disc. Mock mode bundles free Unsplash
portraits for the sample people.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KhQh614DMeYoQEH9SoMRUw
This commit is contained in:
alvocool
2026-11-09 08:22:41 +03:00
parent 72786701a9
commit 1b5d105400
33 changed files with 406 additions and 168 deletions

View File

@@ -0,0 +1,56 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import '../about/internal/application/TextType.dart';
import '../utils/Colors.dart';
import 'text/Text.dart';
/// Where a portrait comes from. A stored `pic` is one of:
/// - `data:image/...;base64,...` — a photo picked on this device
/// - `http(s)://...` — a photo the server holds
/// - `asset:assets/images/...` — a bundled sample (mock mode)
/// Anything else, or empty, has no photo.
ImageProvider? portraitImage(String pic) {
if (pic.startsWith("data:image")) {
final int comma = pic.indexOf(",");
if (comma < 0) {
return null;
}
return MemoryImage(base64Decode(pic.substring(comma + 1)));
}
if (pic.startsWith("http://") || pic.startsWith("https://")) {
return NetworkImage(pic);
}
if (pic.startsWith("asset:")) {
return AssetImage(pic.substring("asset:".length));
}
return null;
}
/// A round photo of a person. Without a photo, their initial on a soft
/// neutral disc — never a landscape: a person should look like a person.
Widget portrait(String pic, String name, double size,
{Color? border, double borderWidth = 2}) {
final ImageProvider? image = portraitImage(pic);
final String initial =
name.trim().isEmpty ? "?" : name.trim().substring(0, 1).toUpperCase();
return Container(
width: size,
height: size,
alignment: Alignment.center,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colorMuted,
border: Border.all(color: border ?? colorWhite, width: borderWidth),
image: image == null
? null
: DecorationImage(image: image, fit: BoxFit.cover),
),
child: image == null
? text(initial, size * 0.4, TextType.Medium, color: colorInkSoft)
: null,
);
}