Hero: pagination dots and each slide's title/badges/description were
both independently bottom-aligned in the same Box, so the dots sat
right on top of (or overlapping) the last line of the synopsis
instead of below it with a gap. Increased the slide content's bottom
inset from 20dp to 44dp so it stops short of the dots row.
Catalog rows: "View All" text replaced with a right-arrow icon,
matching the reference's icon-only affordance.
MobileBottomNav: dropped the top hairline divider and switched to a
flat pure-black background (was a near-black AMOLED-conditional
color), bumped icon size 26->28dp and vertical padding 10->16dp for
more breathing room - closer to the reference's cleaner, more open
spacing.
Compile-verified on shared commonMain, iosMain, and both Android
flavors.
Home hero: dropped the Play button (whole hero is already clickable),
made the age-rating badge plain text instead of a filled accent pill
so all metadata reads consistently, and merged the tabs row + bell/
avatar row into one - Home/Series/Movies on the left, notifications +
avatar on the right, both overlaid on the hero image together instead
of stacked as two separate rows.
MobileBottomNav (Android's native bottom bar): removed the Settings
tab entirely - it was actually rendering as a profile-avatar-with-
quick-switch-menu, which is now redundant with the Home hero's avatar
(wired to the same Settings destination). Bottom nav is Home/Discover/
Calendar/Library only. Also dropped the text labels and the selected-
state background pill per direction - just icons now, filled variant
when selected vs outline otherwise, matching the "make icon filled
when clicked, that's it" ask exactly.
Compile-verified on shared commonMain, iosMain, and both Android
flavors.
The bell+avatar row and Home/Series/Movies tabs were rendered as a
separate solid-background section stacked above the hero - reference
image has them overlaid directly on the hero image, scrolling away
together with it. Moved FluxaHomeOverlayBar into the same Box as
FluxaHomeHero with a top-down scrim gradient for legibility over
varying artwork, instead of a fixed opaque header.
FluxaNavigationBar (the shared bottom nav, used on iOS since Android
renders its own native MobileBottomNav instead) was iterating every
FluxaDestination including ProfileList/AddonStore/Auth, none of which
belong in a persistent tab row - they're already reached other ways
(the new Home avatar, Settings' manage-addons button, the login flow).
Introduced FluxaBottomNavDestinations restricted to the six real
top-level screens.
Hero content is now center-aligned (logo, title fallback, badges,
description) instead of left-aligned, description line-height
tightened, and runtime is now shown in the badge row for movies -
CatalogItemUiModel didn't carry runtimeLabel before, only Detail's
model did, despite Meta.runtime being available all along.
Compile-verified on shared commonMain, iosMain, and both Android
flavors.
No logo/wordmark anywhere (per explicit direction) - just a
notification bell and the active profile's avatar top-right, with a
Home/Series/Movies tab row underneath.
The tabs aren't decorative: HomeViewModel already had a working
currentFilter/setFilter("all"/"movie"/"series") mechanism used
internally for billboard filtering, just never exposed through the
shared UI layer. Added CatalogAction.FilterChanged and
CatalogHomeDataSource.setFilter() to wire it through properly -
AndroidCatalogHomeDataSource now threads currentFilter into
orderHomeCategories() too, which already supported a filter param
that was previously always called with the "all" default.
Notification bell opens a new NotificationsScreen, modeled as an
overlay on FluxaAppState (same pattern as the Detail overlay - a
showNotifications flag, openNotifications()/closeNotifications(), no
new FluxaDestination so it doesn't leak into the enum-driven bottom
nav). It reuses CalendarUiState/CalendarDataSource directly - no new
data source - and groups the existing flat release list into Coming
Soon / New / Later sections using kotlinx-datetime (added as a new
shared dependency; date arithmetic on ISO date strings needs real
calendar math, not string comparison, and there's no JVM-only
java.time available in commonMain). Tapping a notification reuses the
existing CalendarAction.ItemSelected handling to open Detail.
Compile-verified on shared commonMain, iosMain, and both Android
flavors. Visual result and date-bucketing logic unverified against a
running app.
Detail screen fixes from review:
- Resume button content is now actually centered (outer Column lacked
horizontalAlignment).
- Download buttons (primary + per-episode) get a download icon.
- Season selector is now a visible pill with a dropdown-arrow icon
instead of plain unstyled text, and opens a ModalBottomSheet listing
seasons instead of an inline expand - easier to tell it's tappable
and consistent with the "bottom sheet, not dropdown" request.
- Episode thumbnails are bigger (was fixed 120dp, now split evenly
with the text column) and synopsis moved to a full-width line below
the thumbnail+title row instead of being squeezed into the narrow
text column beside the thumbnail.
- Hero renders the title logo (MetaDetail.logo, now exposed as
DetailUiModel.logoUrl... already existed, just unused) when
available, falling back to text title otherwise.
Home hero: same logo-over-text treatment, plus an age-rating badge,
season count, and a description line - all sourced from Meta fields
(description/ageRating/seasonsCount) that were already fetched but
not exposed on CatalogItemUiModel before now.
Screen transitions: FluxaApp's main content switch (destination
changes, opening/closing Detail, entering profile edit) was an
instant when-block swap with zero animation. Wrapped it in
AnimatedContent keyed on a computed screen identity, with a fade+scale
enter and fade exit - the "no animation when navigating" gap.
Per-screen composables switched from Modifier.weight(1f) (needs
ColumnScope, no longer valid once nested inside AnimatedContent's
content lambda) to Modifier.fillMaxSize(), with weight(1f) moved to
the AnimatedContent itself.
Not addressed, scoped out: HBO Max's top bar (app logo, notification
bell, profile avatar) and category tab row (Home/Series/Movies/...)
on the Home screen reference image - Fluxa's Home has no matching
data model (no per-category content-type filter, no notification
system) and the existing bottom FluxaNavigationBar already serves
destination-switching, so adding a duplicate top tab row felt like
UI without backing functionality rather than a real gap.
Compile-verified on shared commonMain, iosMain, and both Android
flavors. Visual result unverified - no way to render Compose UI in
this environment.
Full-bleed tall hero (poster/backdrop) with title, type eyebrow, and
metadata (age rating badge, year, season count or runtime) overlaid
directly on the image via a bottom gradient scrim, matching a
Netflix/HBO Max-style detail layout instead of the earlier flat
sectioned one.
Adds a proper close/back affordance and share icon to the shared
Detail screen for the first time - it previously had no way to
dismiss itself from within the UI. Threaded onBack through
FluxaApp/FluxaAppHost/AppRoutesHost: the in-Home overlay now clears
itself locally (FluxaAppState.clearDetail()), while the standalone
mobile route pops the nav stack via navigateBackSafely - reusing the
same mobileDetailScreen-null guard pattern already established for
onDetailNavigationEvent, so the overlay and route cases don't collide.
Play becomes Resume when there's saved progress (shows "Resume S1 E2"
using the resume episode's season/number when resolvable), with a
Restart button underneath that dispatches the new
DetailAction.Play(fromStart = true) to explicitly bypass resume -
DetailNavigationLogic.forPlay/forStream now take a fromStart flag
that forces progress to 0 regardless of saved position.
Extends DetailUiModel with castNames and ageRating (from MetaDetail,
already fetched, just not exposed to the shared UI model before) and
DetailEpisodeUiModel with runtimeLabel (from Video.episodeRuntime).
Compile-verified on shared commonMain, iosMain, and both Android
flavors. Visual result not verified against the reference images in a
running app - no way to render Compose UI in this environment.
Fixes AppleAuthDataSource: continueWithNuvio/continueWithStremio/
continueWithoutAccount were incorrectly routed through Swift
notifications - Android handles all three as pure local UI-state
transitions (no network), so this matches that instead of adding
unnecessary indirection.
submit() for the Credentials stage now posts a real request that
FluxaAppleAuthStartup.swift picks up: FluxaAppleStremioAuthClient
mirrors StremioService.kt's api.strem.io/api/login and /api/register
endpoints via URLSession, and the resulting session (id/email/authKey)
is persisted through FluxaAppleAuthStore using the Keychain generic-
password API (kSecClassGenericPassword, accessible-after-first-unlock),
not UserDefaults - this is the first credential-bearing data iOS
persists, unlike the existing UserDefaults-backed local stores.
Nuvio login/import is out of scope here: Android's version pulls
profile/addons/library/progress/history/collections through
NuvioAccountImportCoordinator, which needs library and addon-store
integration iOS doesn't have yet. The Nuvio stage transition works
(continueWithNuvio), but submitting Nuvio credentials currently has no
Swift listener - same "reachable but inert" scope already established
for AddonStore.
The error-state message on failed Stremio login is a hardcoded English
string rather than routed through AppStrings, since calling Kotlin's
AppStrings.t() from pure Swift needs framework-export verification I
can't perform without Xcode.
Caveat: this Swift code (Keychain access, URLSession networking) is
unverified - no Xcode/macOS toolchain available in this environment.
Only the Kotlin side (AppleAuthDataSource, all Gradle targets) is
compile-checked. Needs a real Xcode build and device/simulator test
before shipping.
Extracts the "which stream/episode plays, is it cs3, how much resume
progress" decision into a pure DetailNavigationLogic object in
commonMain. DetailStore (used by mobile's shared CMP Detail screen)
now delegates to it instead of carrying its own copy.
DetailRoute.kt (Android TV's legacy Detail route - mobile no longer
reaches this file after the earlier Detail route migration) now calls
the same DetailNavigationLogic functions instead of its own hand-
rolled duplicate of the same branching, so both platforms make this
decision through one code path. TvDetailScreenContent's rendering is
untouched - it still reads DetailViewModel directly, since it needs
richer domain data (MetaDetail/Video/Stream) than the lightweight
shared Detail UI models carry.
Preserves the movie-vs-series distinction from the original TV logic:
Play button on a movie always targets a null video id (no accidental
resume-progress carryover), matching the legacy behavior exactly.
Two real bugs surfaced while scoping the TV Detail work:
1. DetailNavigationEvent didn't carry resume progress, so
AppRoutesHost's mobile handler applied mobileDetailScreen's
initialProgress unconditionally to whatever stream/episode got
played - including a different episode than the one that was
actually paused. DetailStore now computes progress the same way
the legacy DetailRoute.kt always did: only resume if the target
video id matches the saved one, otherwise start at 0. Fixed the
underlying resume/lastVideoId resolution in AndroidDetailDataSource
too, which wasn't honoring the DetailRequestUiModel's own
lastVideoId/initialProgress overrides (screen.lastVideoId ??
savedPlayback in the legacy code) - it only ever looked at
savedPlayback.
2. onDetailNavigationEvent fired unconditionally on the shared
FluxaAppHost instance, which is reused for both the in-Home Detail
overlay and the standalone Screen.Detail route. Clicking Play in
the overlay already had a working navigation path via
onSharedPlayRequested (a separate, pre-existing callback) - Phase 1
added a second, redundant navigateTo() call on top of it for every
overlay Play/stream click. Guarded the handler to only act when
mobileDetailScreen is non-null (i.e. actually in the standalone
route), restoring the overlay to its original single-navigation
behavior.
AppleSettingsDataSource is a real, fully local implementation backed
by NSUserDefaults - all ~50 SettingsUiState fields (general,
appearance, playback, subtitles, advanced, addons, downloads, system,
TMDB account toggles) persist across launches and round-trip through
update methods immediately, with no backend dependency since Settings
is pure preference storage. Content-feed toggle/reorder actions and
sync-disconnect stay no-ops since they depend on addon catalog/sync
integration iOS doesn't have yet.
AppleAddonStoreDataSource and AppleAuthDataSource are compile-only
bridges: real UI-state handling (input text, form fields, detected
input type) plus NSNotificationCenter action dispatch following the
established Apple*DataSource pattern, but the notifications currently
have no Swift-side listener. Unlike Detail/Library/Discover/Calendar,
Android's AddonStore/Auth data sources call direct Kotlin services
(PluginManager, StremioRepository, AppContainer.authService) with no
Rust-headless or existing Swift-networking equivalent to bridge to -
building real login/addon-manifest networking for iOS is a separate,
larger effort involving actual security/product decisions (session
storage, Keychain use) that shouldn't be guessed at.
AppleFluxaPlatformServices now implements FluxaSettingsServices,
FluxaAddonStoreServices, FluxaAuthServices, making the shared
Settings/AddonStore/Auth CMP screens reachable via FluxaAppHost on
iOS. Compile-verified across shared (iosMain + commonMain) and both
Android flavors.
These three files were already broken in the working tree, written
against older shapes of DiscoverDataSource/LibraryDataSource/
ProfileDataSource before those contracts evolved (loadMore() added to
Discover; LibraryUiState restructured from a flat watchlist into
planned/completed/favorites/collections/downloadGroups; Profile's
settings fields split out into the separate SettingsDataSource/
SettingsUiState feature, leaving ProfileDataSource with pin/biometric/
save-profile methods only).
AppleDiscoverDataSource: added loadMore() posting the current filters
as a load-more notification.
AppleLibraryDataSource: updateJson() now decodes the current
LibraryUiState shape; added createCollection/renameCollection/
deleteCollection/cancelDownload following the existing notification-
dispatch pattern.
AppleProfileDataSource: rewritten against the current ProfileDataSource
interface (no more SettingsUiState) - single default Apple profile
backed by NSUserDefaults for name/avatar/pin/biometric flag, matching
the local-store pattern already used by FluxaAppleLibraryStore.swift.
:shared:compileKotlinIosSimulatorArm64 now builds clean.
The Phase 1 DetailContracts extension (season/episode/streams/
downloads) added five new abstract members to DetailDataSource that
AppleDetailDataSource didn't implement, breaking the iOS build. Adds
NSNotificationCenter dispatch for each, following the same
postNotificationName pattern the existing loadDetail/toggleWatchlist
methods use, plus the missing runtimeLabel field in the JSON decode.
Adds billboard/hero state, per-row cardLayout/isActionRow/topTenEnabled
flags, and an ItemFocused action to CatalogHomeUiState, keeping
CatalogItemUiModel itself unchanged (still no domain objects - Meta
stays out of anything a LazyRow/LazyColumn renders, since Stream/Meta
carry mutable fields that make Compose unable to skip recomposition).
AndroidCatalogHomeDataSource now populates these from HomeViewModel's
existing billboard/category state, and exposes resolveMeta(id, type)
as an Android-only boundary helper so navigation code can reconstruct
a full Meta for Screen.Detail/Screen.Player without it ever touching
the UI layer - same resolve-at-the-boundary pattern used for streams
in AndroidDetailDataSource.resolveStream().
Groundwork for migrating the Android TV Home screen off HomeViewModel
onto the same shared CatalogHomeStore mobile already uses; the TV
Compose screen itself is not yet rewired.
Adds season/episode picker and sources list to the shared Detail
composable, switches FluxaAppState's selectedDetail to carry a
DetailRequestUiModel directly (instead of a full CatalogItemUiModel)
so the standalone Screen.Detail route can seed it without going
through a catalog item first, and threads a detailRequest +
onDetailNavigationEvent pair through FluxaAppHost.
AppRoutesHost now builds a DetailRequestUiModel from Screen.Detail and
hands the shared Detail screen's navigation decisions (play a
specific stream vs. go pick sources, based on cs3: id prefixes) back
to Screen.Player / Screen.Sources, reconstructing the full Stream
object via AndroidDetailDataSource.resolveStream() so headers/
subtitles aren't lost. TV still uses the legacy DetailRoute for now.
Adds season/episode picking, stream listing/filtering, and download
actions to the shared Detail feature (DetailContracts, DetailStore),
and wires AndroidDetailDataSource to the existing DetailViewModel to
back them. Navigation decisions (play directly vs. go to source
selection, based on cs3: id prefixes) now live in DetailStore and are
exposed via a DetailNavigationEvent flow for the route layer to
consume.
First slice of migrating the standalone Detail route off the legacy
Android-only DetailScreen onto the shared CMP Detail screen.
Swaps the Playback icon from a circled play button to a video/on-demand
icon, and bumps every Settings row icon (nav rows, brand action rows) to a
larger size for better legibility.
Adds bottom content padding (120dp, matching Home/Discover) to the shared
Settings and Library screens so their content isn't hidden behind the
mobile bottom navigation bar, which neither screen accounted for.
Reuses the app's existing ic_stremio/ic_nuvio/ic_trakt/ic_simkl/ic_anilist
drawables as icon slots on the shared Settings account rows, matching the
pattern already used for the Auth screen's provider buttons. Swaps the
Catalogs nav icon from a single open book to a stacked-books icon.
Also removes MyAnimeList support from the shared Settings feature end to
end (contracts, store, screen, Android data source, host wiring) since the
product no longer supports it — only Stremio/Nuvio/Trakt/Simkl/AniList
remain.
Adds Material icons to each Settings hub nav row (Account, General,
Appearance, Playback, Catalogs, Add-ons, Downloads, Developer) and shows
the active profile's avatar next to the display name at the top of the hub.
Removes the "Restart app" action end-to-end (action, store handling,
FluxaAppHost plumbing) since it was a no-op in both native and shared code.
Extends the shared Library feature from a bare watchlist grid to the full
native surface: Planned/Completed/Favorites/Downloads/Collections sections,
a movie/series/anime type filter, user collection create/rename/delete, and
offline download folders with per-episode progress and cancel.
AndroidLibraryDataSource adapts HomeViewModel's watchlist/likedItems/
libraryUiState (Trakt/MAL/Simkl planned & completed) and OfflineDownloadManager
to the shared contract, reusing the existing toOfflineDownloadGroups()/
formatDownloadBytes() helpers rather than reimplementing them. One known
simplification: user collections are basic title-only entries (create/
rename/delete) without the native app's deeper folder/catalog-source editor
and JSON import/export, which is a materially separate sub-feature.
Mobile now routes Screen.Watchlist through the shared Library destination;
Android TV keeps its native WatchlistScreen since TV migration is deferred.
Adds a comprehensive Settings feature under shared/commonMain covering all
9 native categories: Account (Stremio/Nuvio/Trakt/MAL/Simkl/AniList/TMDB/
notifications), General, Appearance (+ Home/Detail sub-pages), Playback
(+ Subtitles/Advanced sub-pages), Content (hero/home/top-10 catalog feed
toggles and reordering), Addons, Downloads, and System/Developer.
AndroidSettingsDataSource adapts existing UserProfile fields, HomeViewModel's
addon/CS3 feed discovery, OfflineDownloadManager, and LastMediaDebugInfoStore
to the shared contract with no new business logic, reusing the existing
domain.discovery.MetadataFeeds helpers for feed selection/ordering. All UI
strings reuse the existing i18n keys already shipped for the native mobile
settings screen rather than introducing new ones.
This replaces the old minimal ProfileSettingsScreen stub (2 toggles) that
previously backed FluxaDestination.Settings. Mobile now routes Screen.Settings
through the shared screen; Android TV keeps its native SettingsScreen since
TV migration is deferred.
Adds ProfileListScreen (avatar grid, manage mode, PIN unlock overlay with
biometric fast-path) and ProfileEditScreen (name, avatar upload, PIN,
biometric toggle, delete) under shared/commonMain, extending the existing
ProfileContracts/Store stub rather than replacing it. AndroidProfileDataSource
gains PIN-gated profile switching, save/delete, all backed by the existing
ProfileManager/PinHasher — no new business logic. The avatar image picker and
BiometricPrompt stay Android-side and are bubbled into the shared screen via
plain callbacks. Mobile now routes Screen.Profiles through the shared
ProfileList destination; Android TV keeps native ProfileScreen/ProfileEditScreen
since TV migration is deferred.
Adds AuthContracts/Store/Screen under shared/commonMain covering the
credentials (login/signup/Stremio), Nuvio sign-in, and Nuvio import
stages that previously lived only in MobileLoginScreen/MobileNuvioLoginScreen.
AndroidAuthDataSource adapts the existing StremioService/NuvioAccountImportCoordinator/
ProfileManager logic to the shared contract, with no new business logic.
Mobile now routes Screen.Welcome/Screen.Login through the shared screen;
Android TV keeps its native LoginScreen/WelcomeScreen since TV migration
is deferred.
Adds AddonStoreContracts/Store/Screen under shared/commonMain following
the established Discover/Library pattern, wires it into FluxaApp/FluxaAppHost
as a new destination, and adds AndroidAddonStoreDataSource which adapts the
existing PluginManager/StremioRepository/AddonActions logic to the shared
contract. Mobile now routes Screen.AddonStore through the shared CMP screen;
Android TV keeps the native AddonStoreScreen for now since TV migration is
deferred.
Type/Catalog/Genre pills sat in a plain Row with no scroll container
and unconstrained label width, so a long catalog name would grow its
pill wide enough to push Genre off-screen with no way to reach it.
Added horizontalScroll to the row (matches Nuvio's own filter row) and
capped each pill's label at 140dp so one long name can no longer
dominate the whole row even when scrollable.
Discover only ever showed one page of results with no way to load
more on scroll. The backend piece (per-catalog page fetch with skip+
genre) already existed in Rust (dispatch_discover_page/FetchDiscoverPage)
but was never wired to an Android effect handler at all, and its
results-append was gated behind the internal multi-catalog-merge flow
that a manual load-more wouldn't go through.
- Rust: made the results.extend() in the fetchDiscoverPage completion
unconditional (was only running when continuing the internal merge
chain); the recursive dispatch_next_initial_page call stays gated.
- Kotlin: added the missing "fetchDiscoverPage" case (reuses the
existing fetchCatalogPage handler, identical payload shape), and
HomeViewModel.loadMoreDiscoverResults() which dispatches
discoverPageRequested with skip = current result count and folds
the returned page into both _headlessDiscoverResults and
_headlessDiscoverResultSources (the latter needed since new items
otherwise had no addon source mapped, breaking playback routing).
- Shared: DiscoverAction.LoadMore end to end (contract, store,
AndroidDiscoverDataSource resolving the selected catalog's
transportUrl/id from state, DiscoverScreen triggering it via a
LazyGridState scroll-near-end check).
Also fixed the mobile bottom nav bar overlapping Discover/Search
content — their grids only had bottom=20dp contentPadding instead of
the 120dp already used elsewhere to clear the floating nav bar.
Diagnosed against NuvioMobile's open-source discover implementation
(same addons, more catalogs visible there). Two real gaps:
1. buildDiscoverCatalogOptions() excluded any catalog requiring an
extra parameter beyond genre. Nuvio's equivalent (supportsDiscover())
explicitly whitelists "skip" (pagination) as never blocking a
catalog even when required — Fluxa's filter didn't, silently
dropping every catalog that declares skip as required, which is a
common Stremio addon SDK convention. Added "skip" to the allowed set.
2. The Type filter (movie/series) was hardcoded in the shared Discover
UI instead of derived from what addons actually expose. Nuvio builds
its type list from distinct catalog types across installed addons,
which is why "anime" shows up there. Added a new
buildDiscoverContentTypes() helper (same extras filter, minus the
"all"/type restriction), threaded contentTypes through the
readDiscoverCatalogFilters effect, Rust's DiscoverState (new field,
needed since the effect completion flows back through the engine's
own state, not directly to Kotlin), HomeViewModel, and the shared
DiscoverUiState/DiscoverScreen, with a movie/series fallback so the
dropdown isn't empty before the first fetch completes.
FluxaColorScheme set primary/secondary to FluxaColors.accent (orange),
which Material3 defaults every unstyled component to (switches, text
field focus/cursor, bottom sheet drag handle, etc.) — turning the
whole app orange instead of the intended black/white look. Primary is
now white with black-on-white content color; accent stays only where
explicitly used (Discover's selected filter/checkmark, calendar
selection).
The filter bottom sheet's option list was a plain Column with no
scroll container, so profiles with many catalogs had no way to reach
options past the first screenful. Switched to a LazyColumn capped at
420dp so it scrolls internally within the sheet.
Diagnosed via logcat: MainActivity's own profile-driven loadInitialData
and the shared shell's eager CatalogAction.Refresh both fired on cold
start, running the same ~5s home bootstrap fetch twice back-to-back.
AndroidCatalogHomeDataSource.refresh() now skips if categories are
already loaded or a load is already in flight, since MainActivity's
existing effect is the real trigger.
Discover filters previously left catalogKey unset until the user
picked one manually, meaning results loaded with no catalog selected
and less specific data. Now auto-selects the first catalog option once
it loads, matching native's behavior.
Filter picker UX: swapped Material3's DropdownMenu (rendered as a tall
default-sized popup) for a ModalBottomSheet, matching native's actual
mobile picker pattern (full-width sheet, checkmark on selection).
Replaced every text-glyph "icon" (⌕, ⌄, ‹, ›, ✓) with real vector
icons. Added compose.materialIconsExtended (Compose Multiplatform's
own KMP-compatible icon library) to the shared kmp-compose convention
plugin so :shared can use Icons.Filled.* directly.
Removed every "let the user know it's updating" indicator that showed
even when cached content was already on screen: Calendar's inline
header spinner, Discover's isSearching check firing before checking
for already-loaded results (same bug class as Search's fix), and
Detail's isLoading check firing before checking whether content was
already loaded. All now silently keep showing the last-known content
while a refresh is in flight, only falling back to a loading state
when there's genuinely nothing yet.
Discover's search bar and filter dropdowns still used bare Material3
defaults. Replaced the OutlinedTextField with a custom pill search bar
matching native's exact treatment (56dp pill, search glyph, borderless
BasicTextField) and gave the filter dropdowns a chevron affordance and
a themed, checkmark-style highlighted selection in the popup menu.
HomeCategoryCache.load() has existed since before this migration but
was never called anywhere — the ViewModel only ever wrote to it
(setCategoriesAndCache -> homeCategoryCache.save), so cold app starts
always waited on a full network fetch with nothing to show. Home now
seeds _categories from the cached snapshot synchronously in
loadInitialData before the network fetch starts, so a relaunch shows
last-known content immediately while the real refresh happens in the
background, same as the isLoading/rows.isEmpty() gating already in
place downstream.
Skeleton placeholders (Home, Search, Discover) were static gray boxes.
Added Modifier.skeletonShimmer(), a small infinite-transition color
pulse between FluxaColors.surfaceCard and surfaceRaised, applied to
all three.
Search checked isLoading before checking whether results/rows already
existed, so any isLoading flip (every debounce cycle, not just first
load) hid already-loaded content behind a full-screen spinner. Reordered
so cached results always render first; the spinner (now a skeleton
grid, matching Discover) only shows when there's genuinely nothing yet.
Home's blocking spinner replaced with a skeleton (hero placeholder +
3 skeleton rows) for the same first-load case.
Calendar no longer gates the month grid behind isLoading at all — once
year/month are known the grid renders unconditionally, so it stays on
screen while a background refresh is in flight and release dots just
appear as data arrives, instead of the whole grid disappearing behind
a spinner every time the destination is revisited.
The shared shell was only mounted while on Home/Search/Discover/
Calendar; navigating to any other native screen (Detail, Sources,
Player, Settings, AddonStore) disposed it entirely, so returning to
any of those four tabs remounted from scratch — fresh stores, fresh
StateFlow defaults (isLoading=true, empty items), and a real re-fetch,
even though the underlying HomeViewModel data was already cached. That
produced the "blank calendar/grid when coming back from another page"
symptom.
FluxaAppHost (mobile) is now always composed as a background layer
behind AppRoutesHost's native AnimatedContent, which only draws on top
(opaque) when the active screen isn't one of those four. The shell
itself is never disposed by navigating elsewhere, so its stores keep
their last-known state and returning to a tab is instant.
Also replaced Discover's full-screen blocking spinner on first load
with a skeleton grid (18 placeholder cards) so the filter row stays
visible and the transition to real results doesn't hide the whole
screen behind a spinner.
MaterialTheme { ... } never set a colorScheme, so every unstyled
Material3 component (OutlinedTextField borders/labels, DropdownMenu
surface/text) rendered with the stock light scheme against the app's
black background instead of matching FluxaColors — the actual cause
of the dropdown/search-bar UI looking broken. Added a dark ColorScheme
built from FluxaColors so every shared screen inherits consistent
theming without per-component overrides.
Calendar was still list-only; rebuilt it as an actual numbered month
grid: a localized weekday header, day cells with leading/trailing
blanks so weeks align (pure-Kotlin Sakamoto's algorithm for the first
weekday and a leap-year-aware days-in-month table, no new platform
expect/actual needed), a dot on days with releases, and a selected-day
detail panel below the grid — matching native's grid+tap-to-see-detail
behavior instead of a flat scrollable date-grouped list.
AppRoutesHost was mounting a fresh FluxaAppHost per Screen navigation,
so every tab switch among Home/Search/Discover/Calendar recreated
CatalogHomeStore and re-triggered CatalogAction.Refresh, which force-
reloads the whole Home catalog over the network regardless of which
tab was actually opened. Hoist the shared shell out of AnimatedContent
so it stays mounted across those four screens and only remounts when
leaving them entirely (Detail/Sources/Player/Settings/etc., same as
before).
Discover regressions from the last pass, now fixed to match native:
- Filter chips replaced with dropdown menus (DropdownMenu/MenuItem)
matching native's Type/Catalog/Genre picker pattern.
- Grid restored to a fixed 3-column layout instead of adaptive sizing.
- Added the inline search box native's Explore screen has, reusing
SearchResultRows/SearchResults (now internal, not private) so typing
in Discover shows grouped search results instead of the filtered
browse grid, same as native.
Wires Screen.Search/Explore/Calendar to FluxaAppHost on mobile, same
pattern as Home. TV keeps using the existing native screens untouched.
To preserve native feature parity in the shared UI:
- Search: per-addon grouped result rows (was a flat grid), a clear
history action, and a query-specific "no results for X" message with
a recent-history fallback, matching native's MobileSearchScreen.
- Discover: filter chips are now a scrollable LazyRow instead of a
plain Row, avoiding the same overflow-wrap bug fixed on Home's row
headers when there are many genre options.
- Calendar: added month navigation (prev/next + localized month
title), day-grouped list headers via the existing cross-platform
localizedShortMonthDay/localizedMonthTitle helpers, replacing the
flat undated list.
Deliberately not replicated (documented simplification, not a bug):
native's Search and Explore screens each embed a blended search+browse
UX (explore filters shown pre-typing in Search; inline search box in
Explore). Shared keeps Search and Discover as clean separate screens
since both capabilities are still fully reachable via their own tabs.
Also skipped: Explore's list/grid view toggle, swipe-to-change-month
and the day-grid calendar view (list view covers the same data).
Row title now truncates with weight+ellipsis instead of squeezing "View
All" into a vertical letter stack. LazyColumn reserves 120dp bottom
padding to clear the floating mobile bottom nav bar (matching the
native HomeScreen constant). Catalog row titles use
displayHomeCategoryTitle() instead of the addon-qualified
semanticName, matching what native screens show.
FluxaHomeContent gains a real hero: a looping HorizontalPager (virtual
page count wraparound) over the first row's top items, dot indicators,
5s auto-advance, and a distinct "home-hero:" cache key so it decodes a
fresh full-size bitmap instead of reusing the small poster card's
cached one. CatalogItemUiModel gains a backdropUrl so the hero can use
Meta.homeHeroBackdrop() (the same high-res backdrop native hero UI
uses) instead of stretching the small vertical poster artwork.
Renders FluxaAppHost for Screen.Home on DeviceType.Mobile using the
already-built AndroidFluxaPlatformServices bridge, replacing the native
HomeRoute/MobileHomeHero path for that screen. FluxaApp/FluxaAppHost
gain showNavigationBar and external destination params so Android can
drive navigation from its own bottom bar instead of the shared shell's
internal one, and an onPlayRequested hook lets Detail's Play action
hand off to the native Sources/Player flow. Android TV is untouched.