Initials, shared. Home's partner bubble and the Settings "Connected with" row
are two views of one person and gave different answers when there was no photo —
Home showed initials, Settings showed a heart. Settings now shows the partner's
initials in a 48dp circle, so the photo and no-photo states share one
silhouette. The helper is lifted to ui/components/Initials.kt rather than copied;
an identical copy in two files is exactly how packArtworkRes drifted. The
unpaired state keeps the heart — there is nobody to take initials from yet.
Framing. Picking a photo now opens a crop sheet: pinch to zoom, drag to move,
inside the real avatar circle. The avatar was a blind ContentScale.Crop, so a
non-square or off-centre photo was centre-cropped and could lose the subject —
Ava's 640x480 test photo cropped straight past her face. The crop is applied at
pick time and handed back as a normal Uri, so setPhotoUri → upload is untouched.
EXIF orientation is honoured (a portrait selfie would otherwise crop sideways),
output is a 512px JPEG, and the source aspect is respected.
Two bugs found by driving it live, both of which looked fine in code:
- Panning did nothing at 1x. The clamp was viewport*(scale-1)/2, which is 0 at
1x — correct only for a square source. A cover-fit 640x480 photo already
overflows horizontally at 1x, so that pinned it dead centre and made framing
impossible: the whole point of the sheet. Now clamped to the picture's actual
overflow, computed from the source aspect.
- Panning then revealed empty space at the circle's edge. ContentScale.Crop had
already discarded the overflow and returned a viewport-sized node, so
graphicsLayer was sliding an already-cropped square around — the pixels being
panned to had been thrown away first. The preview now draws the bitmap itself
at cover*scale with the same maths as the crop, so preview == output.
Adds androidx.exifinterface. Verified live on 5554: sheet opens, pan moves the
picture (pixel-probed), no empty edge. 0 FATAL.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Centralize all plugin + dependency versions in gradle/libs.versions.toml (the
modern Gradle standard) and switch the root + app build scripts to the generated
libs.* / alias(libs.plugins.*) accessors. No version changes — a 1:1 migration of
the 57 hardcoded coordinates. Full assembleDebug + unit suite green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Migrate all 56 Compose screens from collectAsState() to the lifecycle-aware
collectAsStateWithLifecycle() (added androidx.lifecycle:lifecycle-runtime-compose).
Upstream flow collection now pauses when the UI isn't visible instead of running
in the background — the modern, recommended idiom, and it unifies the two files
already using it with the rest of the app.
No behavior change for visible screens; compiles + unit suite green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Content-free daily-question state + streak, mirrored from HomeViewModel via a
dedicated DataStore (TodayWidgetUpdater -> TodayWidgetState). App-lock drops it
to a generic card so partner-activity state doesn't sit on the protected home
screen; signed-out/unpaired show neutral prompts. Tapping opens the daily
question via MainActivity's app_route extra.
Verified: provider registers, state bridge writes, headline logic unit-tested
(TodayWidgetTest), no crash on launch. On-home-screen render + a Remote Config
kill switch are tracked in Future.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- PartnerNotificationManagerTest: notification creation, type mapping, deep links
- build.gradle.kts: enable Robolectric and default return values for unit tests
- AppRoute: fix NPE in asRouteArg when android.net.Uri is stubbed on test classpath