Skip to content

Implement full-featured Android app with backup, settings, and notifications#34

Merged
Greensand321 merged 38 commits into
claude/zip-backup-systemfrom
main
Jun 10, 2026
Merged

Implement full-featured Android app with backup, settings, and notifications#34
Greensand321 merged 38 commits into
claude/zip-backup-systemfrom
main

Conversation

@Greensand321

Copy link
Copy Markdown
Owner

No description provided.

Greensand321 and others added 30 commits March 28, 2026 15:59
Add ZIP full-backup system (photos + data) with inline STORE-mode ZIP…
Remove capture="environment" to allow photo library selection
Add meal timestamps, quick-select menu, heart health rating, combined…
Tighten meal step UI, fix quick-select to select-then-confirm flow
Fix quick-select chip highlighting and live text preview; restore cam…
Add A-Z sort toggle and drag-to-reorder to quick select menu
Add supplement tracker with tap-to-count and hold-to-reset
First milestone of the Option-3 full-native rewrite: a faithful, interactive
Compose port of the daily logging wizard, 14-theme system, day summary, and
supporting overlays. Backed by an in-memory repository (Room comes later).

- 8-step wizard (breakfast→lunch→dinner→ratings→heart→flags→notes→summary)
  with smartStep()/defStep() start logic ported 1:1 from index.html
- KetoColors theme system reproducing all 14 web themes (exact colour values)
- Day navigation, swipe gestures, keto button + timestamps, summary edit jumps
- Overview, supplements, quick-select, and theme-picker overlays
- Gradle wrapper committed; build requires Android SDK platform 35

https://claude.ai/code/session_01JkcDPXGfphLVjv2HReg96W
…eview-2oRMX

Add native Android (Jetpack Compose) interface demo
- Add key(stepIndex, viewedKey) around StepContent so Compose fully rebuilds
  the UI when the wizard step or date changes (fixes stale button states)
- Fix ThemePanel click propagation: panel now sits above scrim as a sibling
  Box with pointerInput consuming all events, so swatch taps are never eaten
  by the close-scrim handler
- Add SettingsSheet — ⚙️ button now opens a real settings screen (version,
  theme shortcut, data section, storage bar with future milestone notes)
- Wire onSettings → Overlay.SETTINGS in WizardScreen (was routing to Overview)
- Theme pick now closes the panel immediately after selection
- Fix Switch composable missing height (thumb was invisible)
- Remove unused TextStyle import from StepBodies
- Expand Previews.kt to 20 previews covering every wizard step, every overlay,
  and four theme variants so all screens are visible in the Design panel
- MainActivity reads vm.themeId inside setContent so Compose correctly re-wraps
  KetoTracker when the theme changes

https://claude.ai/code/session_01JkcDPXGfphLVjv2HReg96W
…eview-2oRMX

Fix 1a-1e: navigation, theme propagation, settings screen, all previews
- Fix swipe gesture: was using per-frame delta (never exceeded 40dp threshold).
  Now accumulates total displacement across the gesture and fires once on
  drag-end when total > 50dp, matching web app 50px threshold behaviour.
- Fix meal card styling: KetoCard now accepts compact=true which applies the
  web app .card-meal values (14dp vertical / 18dp horizontal padding, 9dp gap
  between children vs 24/20/18dp for regular cards). WizardScreen passes
  compact=step.isMeal to KetoCard.
- Add heart GOOD auto-advance: selectHeart(GOOD) now launches a viewModelScope
  coroutine that calls next() after 380ms, matching web app selHeart('good')
  setTimeout behaviour. Mild/Bad selections show the notes area without
  advancing.
- Add lifecycle-viewmodel-ktx dependency for viewModelScope.

https://claude.ai/code/session_01JkcDPXGfphLVjv2HReg96W
…eview-2oRMX

Audit fixes: swipe gesture, meal card styling, heart auto-advance
Adds full local persistence with zero schema migration risk using a
two-column Room table: `date TEXT PRIMARY KEY, data TEXT`. The `data`
column stores the full DayEntry as JSON via kotlinx.serialization.
Adding new fields to DayEntry only requires Kotlin changes + a default
value — the SQL schema never changes, so no migrations are ever needed.

Data layer:
- DayEntryEntity / DayEntryDao / KetoDatabase — Room setup
- IDayRepository interface with suspend load/save/deleteAll + Flow
- DayRepository — Room implementation with JSON encode/decode
- FakeDayRepository — in-memory impl (seeded demo data) for previews
- PrefsStore — DataStore<Preferences> for theme + future settings

ViewModel:
- Accepts IDayRepository + optional PrefsStore via constructor injection
- AppViewModel.factory(app) — production wiring (Room + DataStore)
- AppViewModel.preview() — design-time factory with FakeDayRepository
- allEntries: Map<String,DayEntry> state collected from observeAll() Flow
- loggedKeys() / hasEntry() / entryFor() now read from in-memory map
- update() saves asynchronously via viewModelScope; UI state is instant
- Theme changes are persisted to DataStore immediately

Build:
- KSP 2.0.21-1.0.28, serialization plugin 2.0.21 added to root gradle
- Room 2.6.1, DataStore 1.1.1, kotlinx-serialization-json 1.7.3 in app

Removes DemoRepository (replaced by FakeDayRepository).
Updates SettingsSheet to reflect that data is now truly persisted.

https://claude.ai/code/session_01JkcDPXGfphLVjv2HReg96W
…eference

Replaces the stale "interface demo" README (which still described
DemoRepository and in-memory-only persistence) with a full architecture
walkthrough — package layout, AppViewModel state model, the Room
JSON-column persistence design, and the Compose UI/theming approach —
plus an up-to-date feature-parity table against the v1.0 PWA and a
prioritized list of remaining work (calendar, snapshots, export/import,
photos, etc).

https://claude.ai/code/session_01JkcDPXGfphLVjv2HReg96W
…e O(n) cost

Every dao.upsert() previously re-triggered a full table re-query and
re-deserialization of every stored day via observeAll()'s Flow, so each
keystroke cost grew with the total number of logged days. Load the full
log once at startup into an in-memory map that update()/jumpTo() now
mutate directly, making per-write cost O(1) regardless of history size.

Also adds:
- A Channel-based one-shot error stream (AppViewModel.errors) wired to a
  Snackbar in WizardScreen, so failed saves/theme writes surface to the
  user instead of failing silently.
- JUnit/coroutines-test unit tests for DateUtils and DayRepository,
  covering the JSON-column round-trip, forward/backward compatibility,
  and corrupt-row handling that the zero-migration design depends on.

https://claude.ai/code/session_01JkcDPXGfphLVjv2HReg96W
Ports the web app's IndexedDB photo store to native idioms: system camera
intent (ActivityResultContracts.TakePicture + FileProvider, no CAMERA
permission), on-disk JPEG storage with EXIF-correct compression
(PhotoStore), and Coil-backed thumbnails/full-screen viewer/summary
badges (MealPhotoArea, PhotoViewer, PhotoIndicator). Generalizes the
error-toast Channel into a general-purpose message channel so photo
add/remove can surface success feedback the same way.

https://claude.ai/code/session_01JkcDPXGfphLVjv2HReg96W
Ports the web app's .cal-panel to a bottom-anchored CalendarPanel
overlay (styled like ThemePanel), opened from the header's date chip.
DateUtils.monthGrid() generates the 42-cell Sunday-first grid with
leading/trailing adjacent-month days; CalendarCell applies the same
3-tier color priority as the web version (tested+keto, >=2 keto meals,
any data) plus today/viewing rings, evaluated against the in-memory
allEntries map. Tapping a day in the shown month jumps straight to it
(closing the long-standing "jump to an arbitrary unlogged date" gap);
tapping an adjacent-month day re-centers the grid there instead.

https://claude.ai/code/session_01JkcDPXGfphLVjv2HReg96W
Native counterpart to the web app's exportAll/handleImport: SAF file
pickers replace Blob-download/<input type="file">, DataPortability owns
JSON encode/decode/merge, and a custom ImportConfirmDialog collapses the
web app's chained confirm() calls into a single merge/overwrite/skip
choice. Adds bulk-write infrastructure (saveAll/upsertAll) shared with
the future Snapshot-restore feature.

https://claude.ai/code/session_01JkcDPXGfphLVjv2HReg96W
Auto-theme: PrefsStore now persists autoThemeEnabled + separate dark/
light theme choices (mirroring kt_theme_auto/kt_theme_dark_auto/
kt_theme_light_auto). MainActivity resolves the active theme via the
existing resolveAutoTheme() + isSystemInDarkTheme() when enabled, and
ThemePanel gains Night/Day sections plus an Auto toggle that updates
each slot in place — the native counterpart of renderThemeGrid()'s
auto-mode behavior.

Storage stats: StorageUsage.compute() sizes the Room database file and
sums every on-disk JPEG via a new PhotoStore.usage(), reporting against
a fixed 512 MB display ceiling (app-private storage isn't quota-limited
like browser localStorage/IndexedDB — the same choice the web app's own
native code path makes). Settings now shows real used space, percentage,
and a day/photo breakdown instead of a static 0% bar.

Updates README feature-status table, v1↔v2 mapping, and roadmap to
reflect both features moving from partial to done.

https://claude.ai/code/session_01JkcDPXGfphLVjv2HReg96W
…e, and quick-select chip sync

- Replace createCaptureUri/addFromUri with a CaptureTarget(file, uri) flow:
  PhotoStore now reads compresses directly from the temp File and always
  deletes it in a finally block (success, failure, or user cancellation),
  fixing an unbounded cacheDir/captures/ leak. Added clearStaleCaptures()
  swept on app startup as a safety net for process death mid-capture.
- markKeto() now only stamps the current time when editing today's entry,
  and never overwrites an already-recorded meal time.
- selectHeart() cancels its pending 380ms auto-advance job when the
  selection changes before it fires, so only the final pick takes effect.
- QuickSelectSheet now tracks chip selection in a session-scoped Set
  (mirroring the web app's _qsSelected) instead of re-deriving it from
  comma-split free text, which mismatched typed entries and desynced on
  external edits.
- Document why DataPortability.merge's supplements ifEmpty check is an
  intentional improvement over the web's literal-equality mergeEntries.
- Add backup_rules.xml / data_extraction_rules.xml excluding photos/ from
  Auto Backup and device transfer, keeping the day-log DB backed up while
  skipping the larger photo cache (matches the web export's own scope).
…ename

Documentation referenced the old createCaptureUri/addFromUri API that the
camera-capture cache-leak fix replaced; describe the new CaptureTarget
flow, temp-file cleanup (incl. cancellation and clearStaleCaptures), and
the File-based compression path.
…dots

- Wizard step/day content now slides + fades in from the direction of
  travel (Next/swipe-left from the right, Back/swipe-right from the left,
  day-to-day summary navigation by calendar order) via a new
  StepTransition wrapper. Only the entrance animates — content always
  reads live `vm` state, so animating an exit would flash the new step's
  data through the old composable.
- Each overlay (ThemePanel, OverviewSheet, CalendarPanel, Supplements,
  QuickSelect, Settings) and the photo viewer now get their own
  AnimatedVisibility with a quick fade in/out, replacing the instant
  when-branch cut. The photo viewer renders a remembered `lastViewedPhoto`
  so its close fade has something to animate even after `viewingPhoto`
  goes null.
- Dots progress indicator eases its width and color via animateDpAsState/
  animateColorAsState instead of snapping between states; unified the
  active/inactive dot shape to a single 4dp corner radius so no shape
  animation is needed alongside size/color.
…eview-2oRMX

Claude/android feasibility review 2o rmx
…om serializer

@serializable on an enum generates a companion object that references
kotlinx.serialization internal APIs (_layoutlib_/_internal_/...), which
Android Studio's layoutlib preview renderer stubs with incompatible mocks.
This caused a NoClassDefFoundError on Heart.<clinit> whenever any preview
loaded FakeDayRepository (which accesses Heart.GOOD/MILD).

The fix removes @serializable from Heart and replaces it with HeartSerializer,
a hand-written KSerializer<Heart> that uses only public kotlinx.serialization
APIs (PrimitiveSerialDescriptor, encodeString/decodeString). DayEntry.heart
is annotated @serializable(with = HeartSerializer::class) so the JSON
encoding is identical (enum name as string), but no internal APIs are
referenced at class-init time.

https://claude.ai/code/session_01JkcDPXGfphLVjv2HReg96W
…eview-2oRMX

Fix Compose Preview crash: replace @serializable Heart enum with cust…
…rrors

Heart.kt and Meal.kt already existed locally but were never committed, so
keeping those enums in DayEntry.kt caused 'Redeclaration' compile errors
on any machine that had the local files. This commit:

- Adds Heart.kt (Heart enum + HeartSerializer — the @Serializable-free
  serializer introduced in the previous commit to fix Compose Preview crashes)
- Adds Meal.kt (Meal enum)
- Removes both declarations and all their related imports from DayEntry.kt,
  which now only contains the DayEntry data class itself

No behaviour change; same-package visibility means no import statements are
needed in DayEntry.kt to reference Heart, HeartSerializer, or Meal.

https://claude.ai/code/session_01JkcDPXGfphLVjv2HReg96W
…eview-2oRMX

Extract Heart and Meal into separate files; fix redeclaration build e…
… from DayEntry

The preview renderer uses layoutlib, which stubs kotlinx.serialization with
non-functional mocks. Any class whose JVM static initializer (<clinit>)
references KSerializer — including @serializable data classes — crashes when
first instantiated in a preview, regardless of whether serialization is ever
called.

Previous fixes patched Heart then DayEntry individually, but the crash kept
reappearing because the root cause was untouched: @serializable on the domain
model. The correct fix is to keep @serializable entirely off DayEntry.

This commit:
- Removes @serializable from DayEntry so its <clinit> has no serialization
  dependency — constructing DayEntry in FakeDayRepository is now safe in previews
- Adds DayEntrySurrogate: an internal @serializable mirror of DayEntry used
  only by DayRepository and DataPortability (neither of which are ever loaded
  during preview initialization)
- Updates DayRepository and DataPortability to encode/decode via the surrogate
- The JSON format on disk is identical (same field names, same types, same
  defaults), so existing database records and exports are fully compatible

https://claude.ai/code/session_01JkcDPXGfphLVjv2HReg96W
Greensand321 and others added 8 commits June 8, 2026 19:28
…eview-2oRMX

Fix root cause of Compose Preview render crash: isolate serialization…
Three quality-of-life improvements to reduce required taps:

1. Ratings auto-advance
   pickRating() now schedules a 380ms auto-advance once all three ratings
   (energy, happiness, portion) are filled — matching the web app's per-step
   selRate() behaviour, collapsed onto the single combined Ratings step.
   Re-picking any rating cancels and resets the timer. All manual navigation
   (next/back/skip/editAt/jumpTo) calls cancelPendingAdvance() so the delayed
   fire can never mis-step if the user navigates first.

2. Lunch gate before dinner
   defStep() now uses an effective smart step: if the clock says dinner time
   (≥14h) but lunch is still empty, it lands on LUNCH instead of DINNER.
   Lunch must be explicitly filled (or skipped with the Skip button) before
   the time-based jump to dinner is allowed.

3. Fasted-breakfast auto-mark
   loadDateEntry() (called at startup and on jumpTo) silently sets
   breakfastKeto=true for today when it is past 10:00 and breakfast text is
   blank and the flag is not yet set — recording a deliberate fast, reducing
   one extra tap for intermittent fasters.
   isIncomplete(BREAKFAST) treats a keto-flagged empty breakfast as complete,
   so defStep() skips it cleanly and lands on the next needed step.

https://claude.ai/code/session_01JkcDPXGfphLVjv2HReg96W
DayEntryDao.upsertAll was added for bulk saves but InMemoryDao (the
in-memory test double) was never updated to implement it, causing a
'not abstract and does not implement abstract member upsertAll' compile
error in compileDebugUnitTestKotlin.

https://claude.ai/code/session_01JkcDPXGfphLVjv2HReg96W
…eview-2oRMX

Claude/android feasibility review 2o rmx
…eriodic backup

Implements web-app feature parity for the Settings screen:

Data model / persistence:
- SnapshotMeta (@serializable) — lightweight metadata stored in DataStore; full
  entry data stored as filesDir/snapshots/snap_{id}.json via SnapshotStore
- ZipPortability — bundles data.json + every JPEG into a .zip for full backup
- BackupWorker (CoroutineWorker, WorkManager) — periodic JSON backup to
  getExternalFilesDir("backups"), prunes to last 7 files (daily/weekly schedule)
- PrefsStore expanded with snapshots, quickSelectItems, backupEnabled,
  backupFrequency flows backed by DataStore
- PhotoStore.listAllPhotoFiles() + restorePhoto() for ZIP export/import

AppViewModel additions:
- Snapshots: saveSnapshot, restoreSnapshot, deleteSnapshot, exportSnapshot
  (up to 25, mirrors web app limit; restoreSnapshot resets to today after restore)
- Quick-select: addQuickSelectItem, removeQuickSelectItem, resetQuickSelectDefaults
  (persisted in DataStore, falls back to DEFAULT_QUICK_FOODS constant)
- Backup scheduling: setBackupEnabled(context), setBackupFrequency(context)
- ZIP export/import: exportFullBackup, importFromZip (reuses pendingImport flow;
  photos are restored after entries commit inside confirmImport)
- cancelImport now clears pendingZipPhotos

Settings UI (full restructure):
- Category-based navigation with Crossfade(200ms) animation between pages
- SAF launchers at top level so results survive sub-page transitions
- Pages: Main (nav list) · Data & Backups · Quick-Select Foods ·
  Notifications (placeholder) · Storage · Appearance · About
- Data & Backups: JSON export/import, ZIP backup/import, named snapshot list
  with restore/export/delete confirmation dialogs, periodic backup toggle
  with Daily/Weekly frequency chips
- Quick-Select Foods: removable chip list + single-line BasicTextField to add
  items + "Restore Defaults" button
- Sheets.kt QuickSelectSheet now reads vm.quickSelectItems (dynamic, persisted)
  instead of the old hard-coded QUICK_FOODS constant

https://claude.ai/code/session_01JkcDPXGfphLVjv2HReg96W
…eview-2oRMX

feat: full Settings overhaul — snapshots, ZIP backup, quick-select, p…
- NotificationHelper: rich daily reminder with BigTextStyle, lock-screen
  visibility (VISIBILITY_PUBLIC), gold accent colour, avocado small icon,
  launcher large icon, and "Log Now" action button
- ic_notification.xml: monochrome avocado vector (evenOdd fill creates
  transparent pit hole so Android's tinting works correctly)
- ReminderWorker: CoroutineWorker scheduled via WorkManager that fires at
  the user-chosen hour; smart suppression skips notification when all three
  meals are already logged for the day
- POST_NOTIFICATIONS permission added to AndroidManifest (API 33+ runtime)
- NotificationHelper.createChannel() called in MainActivity.onCreate() so
  the channel is registered before any worker can fire
- PrefsStore: notificationsEnabled + notificationHour (default 20 / 8 PM)
  flows backed by DataStore
- AppViewModel: setNotificationsEnabled / setNotificationHour methods that
  create the channel, schedule or cancel ReminderWorker, and persist prefs
- SettingsSheet rewrite:
  - Merged "Storage" and "Data & Backups" into single "Data & Storage" tab;
    StorageBar sits at the top per UX request, backup/snapshot/export
    sections follow below
  - Fully functional Notifications page: runtime permission handling for
    API 33+, toggle pill, morning/afternoon/evening time presets, and an
    in-app preview card showing exactly what the notification will look like

https://claude.ai/code/session_01JkcDPXGfphLVjv2HReg96W
…eview-2oRMX

feat: full notification system + merge Storage/Data&Backups tabs
@Greensand321
Greensand321 merged commit d33ba1a into claude/zip-backup-system Jun 10, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants