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

@@ -27,6 +27,7 @@ import '../../designs/Component.dart';
import '../../designs/FloatingNav.dart';
import '../../designs/GlassSurface.dart';
import '../../designs/MemberAvatar.dart';
import '../../designs/Portrait.dart';
import '../../designs/ProgressRing.dart';
import '../../designs/Responsive.dart';
import '../../designs/Shell.dart';
@@ -35,6 +36,7 @@ import '../../designs/input/InputFields.dart';
import '../../designs/scene/SceneArt.dart';
import '../../designs/text/Text.dart';
import '../../utils/Colors.dart';
import '../../utils/PhotoPicker.dart';
import '../../utils/CommonUtils.dart';
import '../../utils/StandingEngine.dart';
import '../../utils/Thresholds.dart';
@@ -247,6 +249,13 @@ class HomeState extends State<Home> implements ConnectHome {
}
}
void _onChangePhoto() async {
final String? pic = await PhotoPicker.pickPortrait();
if (pic != null) {
_model?.savePhoto(pic);
}
}
void _onOpenSettings() {
GroundedNavigation()
.navigateToPage(NavigatorType.justOpen, const Settings(), context);
@@ -1194,28 +1203,11 @@ class HomeState extends State<Home> implements ConnectHome {
// ── Header helpers ────────────────────────────────────────────────────────
/// A portrait cut from the painted landscape, with the initial on it.
/// Your photo. Tapping it picks a new one.
Widget _avatar(double size) {
final String initial =
_user.name.isEmpty ? "G" : _user.name.substring(0, 1).toUpperCase();
return Container(
width: size,
height: size,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: colorWhite, width: 2),
),
child: Stack(
fit: StackFit.expand,
children: [
SceneArt(palette: sceneWorkout, seed: sceneSeed(_user.name)),
Center(
child:
text(initial, size * 0.4, TextType.Medium, color: colorWhite),
),
],
),
return GestureDetector(
onTap: _onChangePhoto,
child: portrait(_user.pic, _user.name, size),
);
}
@@ -1705,7 +1697,10 @@ class HomeState extends State<Home> implements ConnectHome {
children: [
avatarStack(
group.members.map((member) => member.name).toList(),
26),
26,
photos: group.members
.map((member) => member.photo)
.toList()),
const Spacer(),
Icon(CupertinoIcons.chat_bubble_2_fill,
size: 16, color: colorInkSoft),

View File

@@ -80,6 +80,15 @@ class ViewHome extends ParentViewModel {
}
}
/// A new profile photo, kept on the device with the rest of the user's
/// details. There is no upload endpoint yet, so it does not leave the phone.
void savePhoto(String pic) async {
final UserDetails details = await getDataManager().getUserDetails();
details.pic = pic;
await getDataManager().setUserDetails(details);
connection.onUserLoaded(details);
}
void loadTodos() async {
connection.onTodosLoaded(await getDataManager().getTodos());
}
@@ -140,8 +149,7 @@ class ViewHome extends ParentViewModel {
/// Reminders are one-shots for the coming week, so they are re-placed
/// whenever the schedule is read.
void _placeReminders(List<RoutineChain> routines) async {
final ProgressTargets targets =
await getDataManager().getProgressTargets();
final ProgressTargets targets = await getDataManager().getProgressTargets();
if (targets.reminders) {
await LocalNotificationEngine.scheduleRoutineReminders(
routines, targets.reminderLead);