Repository navigation
Rework export feature into lightweight, dynamically-played recaps - #53
Merged
Merged
Conversation
Replace the export feature with recaps: a recap is now just a date range + name, persisted only when explicitly saved. Viewing a recap plays its constituent daily videos back-to-back via an ExoPlayer queue (auto-advance, loop) instead of a pre-stitched file; stitching into a single video now only happens on demand, when the user taps "Export to single video" to share it. Recaps can be created via whole months/weeks/years (auto-named and listed by date range) in addition to the existing custom date-range flow, which now requires a name and saves immediately. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LrYZZ5W6x1h2oGmcPAUH5m
|
A month/week/year is now only offered once it has fully elapsed (e.g. September isn't listed until 1st October), and a fully-elapsed period with no recorded videos still shows up, greyed out and non-clickable, with a note that it has no videos. Also collapses the four separate create-recap cards on the hub into a single card that opens a new page for picking which kind of recap to create. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LrYZZ5W6x1h2oGmcPAUH5m
The greyed-out state for empty periods was compounding alpha across background and text, making it nearly invisible; now a single alpha applied before the background fades the whole card as one unit, and those rows use smaller padding/type since they're not interactive. Date ranges for month/week/year options now drop whatever's implied by the period itself (e.g. "10 - 30 September" instead of repeating the month and year on both ends), and month/week labels only show the year when it differs from the current year. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LrYZZ5W6x1h2oGmcPAUH5m
Cards were full-width but content-light, so a 2-column grid uses the space better. Options stay ordered latest-first, now paired with reverseLayout so the page opens scrolled to the latest one at the bottom, with older ones revealed by scrolling up (oldest at the top). Also replaces the vague "no videos yet" empty state with a period- aware message explaining why nothing's showing yet and when the first one will appear (e.g. "after the end of this calendar month"). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LrYZZ5W6x1h2oGmcPAUH5m
Within-row order came out newest-to-oldest left-to-right, since reverseLayout only flips row order, not the order within a row, and the list itself is latest-first. Each pair of items is now reversed before handing it to the grid, so left-to-right reads oldest-to-newest as expected while rows stay bottom-to-top newest-to-oldest. Cards also varied in height depending on font size, padding, and whether the "No videos" line was present. They now share the same title size and padding, and the subtitle always reserves two lines (via minLines/maxLines, not a fixed dp height) so every card matches regardless of content or font scaling. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LrYZZ5W6x1h2oGmcPAUH5m
The reverseLayout + reversed-pairs trick correctly ordered rows but produced a confusing layout for odd counts (a lone oldest card stuck at the top, paired items packed at the bottom). Replaced it with the much simpler oldest-first list laid out normally, with the grid's initial scroll position set to the last item - same "opens scrolled to latest, scroll up for older" behavior, without the row gymnastics or odd-count surprises. Also extends the "No videos" case into an ordinary video-count line shown on every card (e.g. "12 videos", "1 video", "0 videos"), so having no videos is no longer a special case in the display logic. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LrYZZ5W6x1h2oGmcPAUH5m
The queue player never paused the shared player on dispose, so audio kept playing after navigating away from a recap. It now pauses (and still resets repeat mode) when the screen is torn down, tying playback to the page's lifecycle. Also tightens the gap between a period card's date range and video count lines, and reorders the fixed recap types to Week, Month, Year (ascending duration) before Custom. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LrYZZ5W6x1h2oGmcPAUH5m
VideoToolbarLayout sized the video purely from its aspect ratio and width, so a portrait video could claim nearly the whole screen height, squeezing the bottom bar down to a sliver - and since that bar wasn't scrollable, its controls (save/export buttons, checkbox) were simply clipped off and unreachable. VideoToolbarLayout now takes an optional maxVideoHeightFraction (default 1f, so existing callers are unaffected) capping the video's height so it pillarboxes instead of crowding out the bottom bar; the recap view screen uses 0.6f, since its bar is unusually tall. Its bottom bar is also now scrollable as a fallback, so nothing is ever permanently unreachable regardless of aspect ratio or screen size. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LrYZZ5W6x1h2oGmcPAUH5m
Revert VideoToolbarLayout to its original behaviour (video sized purely by aspect ratio, no height cap, no scrolling bottom bar) - the earlier fix traded one problem for a worse one. Instead, the recap view screen's bottom bar is cut down to match the height every other video screen already uses: a close button, the recap's name, and a save button, in a single VideoControlsRow. Thumbnails, playback controls, the date-stamp checkbox and the export button are all dropped from this screen for now, pending a rethink of where that UX belongs; the recap just plays on loop behind the bar. RecapViewViewModel/Page are trimmed to match (aspect ratio, video files, name, save/unsave). The export-to-single-video flow (progress page, view model, request model) is left in place, just unreferenced from this screen for now. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LrYZZ5W6x1h2oGmcPAUH5m
Replace the plain header/Save row with a bottom sheet matching the approved mockup: a chevron toggle expands the sheet to reveal export options (currently just the date-stamp checkbox) below a fixed header row, without ever overlaying the video. The header centers the recap name (wrapping onto multiple lines if needed) between a borderless back button and save/share icon buttons. Tapping share now wires back up the dormant RecapExportProgress flow: the ViewModel derives the recap's RecapDay list and builds a RecapExportRequest honoring the sheet's date-stamp toggle. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LrYZZ5W6x1h2oGmcPAUH5m
The bottom sheet's export options never made sense in place: sharing a recap requires stitching it into a file first, which takes time, so picking export options and watching that happen can't both fit in a row docked over the video. RecapView's bottom bar goes back to being static - same look, but no chevron, no expand/collapse, no options tucked inside it. Tapping share now opens a new RecapSharePage (replacing the dormant RecapExportProgress flow) that walks through the whole job: pick whether to include the date stamp, kick off the export, watch its progress, then hit Share to send the finished file to the system share sheet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LrYZZ5W6x1h2oGmcPAUH5m
Previously every export overwrote the same fixed export.mp4, so returning to share a recap always re-ran the (slow) video stitching step even if nothing had changed. VideoExportDao now writes each export to a path keyed by the recap's date range and its date-stamp choice, so the two options for the same recap are cached independently, and a failed export cleans up its own partial output so it's never mistaken for a valid cached file. RecapShareViewModel checks for a matching cached export whenever the page loads or the date-stamp toggle changes, jumping straight to the Ready/share state instead of the options screen when one already exists. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LrYZZ5W6x1h2oGmcPAUH5m
Replace the bare includeDateStamp boolean threaded through VideoExportDao/VideoExporter with a RecapExportOptions value holding all export choices and deriving the cache key from them. Adding a future export option (e.g. a filter or resolution choice) now just means adding a field to RecapExportOptions and folding it into its cacheKey, rather than threading a new parameter through every method along the export path. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LrYZZ5W6x1h2oGmcPAUH5m
The bar still had the rounded-top-corners/AppSurfaceVariant look left over from the earlier bottom-sheet design, making it look like a sheet header rather than a plain video toolbar. It now sits directly on VideoToolbarLayout's own black background with no surface color or shape of its own, matching the calendar day view's bottom bar. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LrYZZ5W6x1h2oGmcPAUH5m
Every other video toolbar (calendar day, record video) fills the whole area below the video and centers its content within it, rather than docking to the bottom edge. RecapView's bottom bar was aligned to BottomCenter instead of Center, so it broke that convention. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LrYZZ5W6x1h2oGmcPAUH5m
Opening the share page previously jumped straight to "ready to share" whenever a cached export already matched the default options, skipping the options screen entirely. The user should always see the options screen first to choose what they want, and only then - once they confirm - does a cache hit skip the export and go straight to ready. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LrYZZ5W6x1h2oGmcPAUH5m
fallbackToDestructiveMigration() wipes the whole database on any unhandled version bump, not just this one - dangerous for every future schema change, not just the saved-export -> saved-recap rework. Add MIGRATION_1_2, which only drops the old, now-unused saved_exports table and creates the new saved_recaps table; every other table (none yet, but future ones) is left untouched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LrYZZ5W6x1h2oGmcPAUH5m
Replace the standalone RecapSharePage with RecapShareSheet, a modal bottom sheet docked over the recap video. It carries the whole flow (pick options, create the export, share it) through the same sheet rather than navigating to a new page - dismissing it just hides it, it doesn't cancel an in-progress export or discard a finished one, so reopening it resumes wherever it left off. RecapShareViewModel now gets its date range/name directly (no more Parcelable RecapShareRequest nav arg) and has its `days` list pushed in from RecapViewViewModel via updateDays, since it's no longer tied to navigation timing. It also now emits the share request automatically the moment an export finishes (or a cached one is reused), rather than waiting for an explicit tap - the Share button on the ready screen just re-opens the system share sheet on demand. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LrYZZ5W6x1h2oGmcPAUH5m
Replace the hand-rolled scrim + AnimatedVisibility overlay with Compose Material's own ModalBottomSheetLayout. This gets scrim-tap, swipe-down and back-press dismissal for free, correctly handled by the framework rather than a half-reimplemented version of it. ModalBottomSheetState is hoisted up to RecapViewPageContent (which wraps the whole page content via the layout's `content` slot) so the share icon in the bottom bar, a sibling of the sheet's own content, can call `.show()` on the same state the sheet reads from. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LrYZZ5W6x1h2oGmcPAUH5m
Root.kt pads the whole app away from the system bars before NormalRouter is ever composed, so ModalBottomSheetLayout's "hidden" offset is computed against that already-reduced height - the sheet's full content was still being measured/composed while hidden, which could leave a sliver visible right at the boundary with the navigation bar. The sheet body now only composes once it's visible or about to become visible (targetValue != Hidden), so there's nothing sized enough to peek out while truly closed - only a 1-line drag handle, which replaces the close button (the whole sheet is already swipeable-to-dismiss, so the handle is just a visual cue now). Also add a "New export" button next to Share on the ready screen, going back to the options screen to pick different settings and potentially produce a different export - reusing the same backToOptions() transition the Failed screen's "Try again" already used (renamed from retryAfterFailure). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LrYZZ5W6x1h2oGmcPAUH5m
- Remove the auto-opened system share sheet on export completion - sharing now only ever happens when the user taps the Share button. - Give the sheet's content area a fixed height (sized for its tallest state) instead of letting it resize to fit whichever state is showing, so switching between options/progress/ready/failed no longer changes the sheet's own size. - The drag handle was still being composed unconditionally even while the sheet was meant to be fully hidden, which (combined with the content Column not filling the sheet's width) left a thin, left-aligned sliver visible between the app's own bottom bar and the system navigation bar. The handle now shares the same visible-or-becoming-visible guard as the rest of the sheet body, and the content Column fills the full sheet width so it's genuinely centered. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LrYZZ5W6x1h2oGmcPAUH5m
The sheet's content is only unmounted once sheetState.currentValue becomes Hidden, which is correct - it needs to stay mounted through the whole close animation so there's something to animate away. But the default spring animationSpec has a long, visually-flat settle tail: the sheet looks like it's stopped moving well before currentValue actually flips, so the (still fully mounted, and - since Root pads the whole app away from the system bars - sitting unclipped in the navigation bar's area) sheet content stayed visible for roughly a second after the animation appeared done. A tween settles deterministically and matches the visible motion, so currentValue (and the content-unmount it triggers) now follows the animation closely instead of trailing behind it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LrYZZ5W6x1h2oGmcPAUH5m
The content-unmount-while-hidden approach from the last fix caused a worse problem: since the sheet's content had to stay mounted for the whole close animation (it's only safe to remove once sheetState.currentValue actually settles on Hidden), the gating instead raced the animation - the frame capture shows real content snapping away well before the slide finishes, leaving a bare, still-sliding drag-handle-only remnant for the rest of the animation. Root cause: Root.kt pads the whole app away from the system bars before any screen is composed, so ModalBottomSheetLayout only ever sees that reduced height as its available space. Its "hidden" anchor is defined as that height, which places the sheet's top edge right at the boundary with the (transparent, edge-to-edge) navigation bar - not below the true screen edge - so the sheet's own content, drawn unclipped past that boundary, was genuinely visible in the nav bar's area for as long as it stayed mounted. Fixed at the source instead: a `fullWindowHeight` layout modifier measures ModalBottomSheetLayout against the real window height (`LocalView.current.height`) rather than the reduced incoming constraint, so its hidden anchor now reaches the true screen edge. The sheet's own `navigationBarsPadding` already grows its content by the same amount this adds back to the available height, so the *visible* position is unaffected - only "hidden" moves to where it should've been all along. Content is always mounted now, so the animation plays through cleanly in one continuous motion. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LrYZZ5W6x1h2oGmcPAUH5m
… bar Forcing ModalBottomSheetLayout to measure against the true window height (rather than the height Root.kt's systemBarsPadding leaves available) was meant to only extend the sheet's hidden resting position further down, past the navigation bar. In practice it also affected the layout of `content` (the actual page, video included), pushing it up under the status bar - a worse, constantly-visible regression traded for a narrower edge case (the sheet barely peeking near the nav bar only while hidden). Decompiling reimagined-navigation's NavHost confirmed it (and Root's own Box) both use plain Alignment.TopStart, ruling out the most obvious "something recenters the oversized content" explanation, so the precise mechanism remains unclear - reverting this now rather than risk further unverified layout changes without a device to test on. The sheet's own animation/sizing fixes from the previous commit (always-mounted content, fixed content height, tween animation) are unaffected and stay in place. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LrYZZ5W6x1h2oGmcPAUH5m
Root previously applied systemBarsPadding() globally, pre-consuming both the status bar AND navigation bar space from every screen before it composed - including RecapShareSheet's ModalBottomSheetLayout, whose hidden-position math is based on the height it's given. That mismatch was the root cause of the bottom sheet resting visibly above the nav bar when fully hidden. Root now only applies statusBarsPadding(), globally, since every screen wants that. Each screen that has content flush against the bottom edge now applies its own navigationBarsPadding() instead: RecapHub, RecapCreateType, RecapCreatePeriodList, RecapCreateCustom, Debug, CrashLog and RequestPermissionsPageContent's root Column, plus SetupRouter's root Column (which owns the Next/Skip button shared by every setup step page, so the individual step pages need no changes). Screens that already self-handled bottom insets (VideoToolbarLayout, CheckVideoPageContent, RecapShareSheet, Calendar) are unaffected. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LrYZZ5W6x1h2oGmcPAUH5m
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
VideoQueuePlayer, auto-advance + loop) instead of a pre-stitched merged file. Stitching into a single video (Media3 Transformer, unchanged) only happens on demand when the user taps "Export to single video" to share it.id, name, startDate, endDate.Test plan
./gradlew assembleDebug/./gradlew test(CI will run this on the PR).🤖 Generated with Claude Code
https://claude.ai/code/session_01LrYZZ5W6x1h2oGmcPAUH5m
Generated by Claude Code