Commit graph

256 commits

Author SHA1 Message Date
KhooLy
e9ccd2e358 Push actual episode number to AniList instead of watched-episode count
When marking a show COMPLETED on AniList, progress now comes from
parsing the season/episode locator off each watched episode's ID and
taking the max episode number, falling back to episodes.size only if
that parsing yields nothing. Previously it always pushed episodes.size,
which undercounts progress for shows with specials or merged
multi-season watch history.
2026-07-19 17:12:45 +03:00
KhooLy
f4ea52a890 Fetch MKV attachments in a single streamed read instead of two requests
fetchAttachmentsViaSeekHead previously issued a 16-byte header range
request, parsed the EBML element length, then issued a second range
request for the full attachments blob. Now it opens one range request
up to MAX_ATTACHMENTS_BYTES and streams the header plus payload off
the same connection, saving a round trip per playback start.
2026-07-19 17:12:41 +03:00
KhooLy
eaef1fb4eb Thread trailer URL from detail state into DetailUiModel
Adds trailerUrl to DetailUiState and DetailUiModel so the detail
screen's data source can pass the resolved trailer through, and
adds the missing HeroPageChanged branch to CatalogHomeStore's
no-op action list.
2026-07-19 17:12:37 +03:00
KhooLy
ad6d158f43 Route player HTTP traffic through Cronet for QUIC/connection migration
Adds Cronet (org.chromium.net:cronet-embedded + Google's cronet-okhttp
transport adapter) as an OkHttp network interceptor on the player's two
HTTP clients (cache pre-warm + main ExoPlayer data source). All existing
interceptor logic — header injection, Dolby Vision manifest rewriting,
localhost proxy timeouts — is untouched since Cronet only replaces the
final transport, not the OkHttp API surface.

This mainly benefits direct-URL/debrid streams (real TLS connections),
which can now survive network handoffs (wifi<->cellular) and lossy
connections without a full reconnect/rebuffer. Torrent-proxied streams
stay on loopback where this has no effect either way.

Falls back to plain OkHttp transparently if Cronet engine creation
fails for any reason (unsupported ABI, native lib issue, etc.) so
there's no regression risk.
2026-07-19 17:09:52 +03:00
KhooLy
35f9c67092 Reorganize Home/Detail appearance settings into drill-down sections
Home Screen and Detail Screen settings now split into dedicated
sub-pages (Hero Banner, Continue Watching, Navigation for Home;
Hero Banner, Episodes for Detail) instead of one long mixed page,
so a specific setting is reachable by name instead of scrolling.

Also removes the dead "Trailer on Hero" toggle in Detail Screen
settings, which persisted a value but was never read by any
consuming code — it just duplicated the real "Trailer on Detail
Hero" setting and caused exactly this kind of navigation confusion.
2026-07-19 15:58:20 +03:00
KhooLy
dfbae648cd Cache hero billboard pool to disk so it paints immediately on cold start 2026-07-19 15:44:41 +03:00
KhooLy
a77c73803c Reserve per-type slots in hero billboard pool so tab filters keep enough items 2026-07-19 15:37:03 +03:00
KhooLy
0b953b6247 Follow SeekHead to locate MKV chapters past the first Cluster
MkvChapterFetcher's fixed prefix scan missed Chapters on releases where
it sits past the first Cluster (e.g. large embedded attachments pushing
it ~10MB in on one observed torrent release). Wire up the new
parseMkvChaptersAtOffsetNative bridge and the seekOffset the Rust
parser now reports: when Chapters isn't in the initial head fetch,
follow the SeekHead-derived offset with one small targeted range fetch
instead. Shrink the initial fetch from a 4MB prefix guess down to 64KB
now that it only needs to cover the (always compact, near-the-front)
SeekHead rather than hoping Chapters itself falls within it.

Add unitTests.isReturnDefaultValues so android.util.Log calls in
Robolectric-less JVM unit tests don't throw, and a regression test
covering the SeekHead follow-up path end to end.
2026-07-19 03:02:23 +03:00
KhooLy
a858cadfd6 Migrate Android trailer resolution to fluxa-core and fix hero playback UI
- Route YouTube trailer resolution through the shared headless engine
  (headless_engine/trailer.rs) instead of a hand-rolled Innertube client
  in TrailerResolver, mirroring what fluxa-desktop already does. Wire
  the two HTTP effect kinds (fetchYoutubeTrailerWatchConfig/Player) as a
  generic passthrough in FluxaAndroidHeadlessEnvironment.
- Wire HeroTrailerVideoSurface's ExoPlayer through
  TrailerResolver.mediaDataSourceFactory() so trailer playback actually
  gets the browser UA + Range header YouTube's CDN requires — this was
  the root cause of the black-screen hero trailer.
- Fix AndroidCatalogHomeDataSource pinning the active billboard movie to
  index 0 of heroItems on every index change, which desynced the pager's
  page index from item identity and made forward swipes visually snap
  back to the previous item.
- Hide synopsis/metadata/play button and shrink the logo while a hero
  trailer plays, soften the bottom scrim so it doesn't black out most of
  the video, and add trailer subtitles (selection/normalize/parse) via
  the same fluxa-core functions fluxa-desktop uses, rendered as a
  position-synced overlay above the shrunk logo.
2026-07-19 02:30:59 +03:00
KhooLy
efdabb628b Fix MKV chapter fetch header/range handling and add regression test
MkvChapterFetcher sent bare stream headers instead of routing through
StreamRequestPolicy (default User-Agent, referer), which many origins
require to respond to a Range request at all — unlike mpv, which always
gets proper headers. Also bail out instead of buffering an unbounded
body when a server ignores the Range header and returns 200 with the
full file.

Add MkvChapterFetcherTest using MockWebServer, exercising the real path
end-to-end (headers, 206 handling, JNI EBML parsing) on the JVM without
an emulator.
2026-07-19 02:30:16 +03:00
KhooLy
1039bfa5d3 Pull Stremio's library as a timestamped watchlist and reconcile it
LibraryItem never declared removed/_mtime so those fields were
silently dropped by Gson before Rust ever saw them, even though
library_state.rs already worked on raw JSON and could have used them.
Adds the fields, StremioRepository.getLibraryWatchlistWithTimestamps,
and wires it into the same mergeWatchlist reconciliation the other
providers use -- but pull-only (reconcileWatchlistPullOnly), not the
full push-capable path.

Deliberately not pushing local watchlist changes to Stremio: that
would need a "toggle membership" write that preserves whatever
progress/state fields already exist on the remote item (a
read-modify-write), which isn't built and isn't safe to guess at
without being able to verify Stremio's actual datastore PUT semantics
(full-replace vs partial-patch) against a live account.
2026-07-18 20:14:09 +03:00
KhooLy
fea466cf27 Support removing items from the AniList watchlist
pushWatchlist's AniList branch only ever called SaveMediaListEntry
(add/update) -- removing locally never propagated, since AniList's
DeleteMediaListEntry mutation needs the list-entry id, not the media
id, and nothing looked it up. Adds a lookup query (Media(id) {
mediaListEntry { id } }, which resolves against the authenticated
viewer implicitly, no separate viewer-id round trip needed) followed
by the delete mutation, and branches pushWatchlist's AniList call on
isInWatchlist same as the other providers.

Un-marking a single episode as watched is left out of this pass --
AniList tracks a show-level episode-progress counter, not discrete
per-episode watched flags, so "unwatch this one episode" doesn't map
cleanly onto its data model without risking corrupting progress.
2026-07-18 20:06:31 +03:00
KhooLy
edc69261e0 Pull AniList's watchlist (PLANNING status) and reconcile it by timestamp
The AniList GraphQL query only ever fetched status: CURRENT, so the
watchlist-add push added in the AniList phase had nothing to
reconcile against on the next sync -- pushed items would just get
re-pushed or silently diverge. Drops the CURRENT-only filter (Rust's
anilist_entries_to_sync already builds a timestamped watchlist array
from PLANNING entries, just needed the query to actually fetch them)
and wires it through the same mergeWatchlist reconciliation Trakt
uses. Generalizes reconcileTraktWatchlist -> reconcileWatchlist since
the logic is fully provider-agnostic.
2026-07-18 20:04:22 +03:00
KhooLy
32e7774056 Add Simkl watched-episode pull with timestamp reconciliation
Simkl mark-watched push existed but nothing was ever pulled back --
marking an episode watched on simkl.com never reached Fluxa. Simkl's
sync/all-items already returns per-episode watched_at when fetched
with extended=full (already the default), just unused until now.

Adds getSimklWatchedEpisodesWithTimestamps (watching+completed status,
shows+anime types) and generalizes the Trakt watched-episode
reconciliation (reconcileTraktWatched -> reconcileWatchedEpisodes)
since the merge logic and push path are provider-agnostic -- both
Trakt and Simkl now call the same function.
2026-07-18 20:00:25 +03:00
KhooLy
2339bfdc3c Reconcile Simkl watchlist one-directionally (remote wins)
Simkl's sync/all-items endpoint has no "added to list" timestamp
(last_watched_at is watch-history, not list-membership, and is null
for plantowatch items) so a real timestamp merge like Trakt's isn't
possible without risking local removals being silently undone by a
fabricated timestamp. Per explicit decision: items on Simkl's
plantowatch list are treated as authoritative and always restored
locally each sync, overriding any local removal tombstone. Removing
something from Simkl's own list doesn't remove it locally -- that
would need a persisted prior-snapshot to detect, out of scope here.
2026-07-18 19:58:06 +03:00
KhooLy
871bd2dcb7 Reconcile Trakt watched-episode state by timestamp, same as watchlist
Trakt's watched-state pull only ever wrote into the external mirror
table for display; local watched_episodes and Trakt's remote state
were never compared or reconciled, so un-marking an episode locally
never propagated and remote-only watched episodes never landed
locally. Trakt's watched-shows response already carries per-episode
last_watched_at, just unused until now.

Adds getWatchedEpisodesWithTimestamps (parses last_watched_at),
global (not per-series) local watched-episode/removal snapshot
queries, and reuses the mergeWatched core_invoke method (Phase 1) to
decide push vs. apply per composite episode id, mirroring the
watchlist reconciliation pattern.
2026-07-18 19:50:46 +03:00
KhooLy
0171ae2d1d Add AniList push: watchlist add and mark-watched now reach AniList
AniList integration was structurally pull-only — no mutation was ever
sent, so adding to the watchlist or marking something watched in
Fluxa never propagated back, even though the stored OAuth token
already has full read/write access (AniList's OAuth has no scope
parameter). Wires pushWatchlist/pushMarkWatched to call the new
anilistSaveMediaListEntryVariables core_invoke method and POST a
SaveMediaListEntry mutation through the existing generic
anilistGraphQl endpoint.

Watchlist removal isn't covered: AniList's delete mutation needs the
list-entry id (not the media id), which requires an extra lookup
query — left as follow-up rather than scope creep here. Same for
un-marking watched.
2026-07-18 19:28:39 +03:00
KhooLy
afddc0ec7c Support removing items from the Simkl watchlist
pushWatchlist's Simkl branch was guarded by `if (isInWatchlist && ...)`
with no else — removing an item locally never removed it on Simkl,
since only sync/add-to-list was ever called. Adds the mirroring
sync/remove-from-list endpoint and routes the push based on
isInWatchlist, matching how the Trakt branch already handles both
directions.

Durability (WorkManager-backed) for watchlist push in general —
Trakt's branch has the same gap — is left as follow-up; this only
fixes the missing Simkl remove call.
2026-07-18 19:23:46 +03:00
KhooLy
da733e5226 Make Stremio playback progress push durable via WorkManager
StremioRepository.savePlaybackProgress was fire-and-forget on the
calling coroutine at both call sites (HomePlaybackController and the
writePlaybackProgress headless effect) — an app kill mid-request
silently dropped the push with no retry, unlike Trakt/Simkl scrobble
and Nuvio's progress push which are all worker-backed.

Adds StremioPlaybackProgressPushWorker on the ProviderSyncPushWorker
base, keyed by profileId+contentId same as Nuvio's so rapid saves
coalesce via ExistingWorkPolicy.REPLACE. savePlaybackProgress now
returns Boolean instead of swallowing failure, since the worker needs
to know whether to retry.
2026-07-18 19:22:10 +03:00
KhooLy
4082dbde74 Extract shared ProviderSyncPushWorker base from the three scrobble/push workers
TraktScrobbleWorker, SimklScrobbleWorker, and NuvioPlaybackProgressPushWorker
had hand-copied WorkManager boilerplate: identical Constraints/backoff
policy, enqueueUniqueWork wiring, and profile-lookup/failure-bookkeeping
against ProfileManager. Behavior-preserving refactor onto a common base
so the next durable workers (Stremio, Simkl watchlist, AniList) don't
duplicate it a fourth and fifth time.
2026-07-18 19:19:43 +03:00
KhooLy
eaa863adde Reconcile Trakt watchlist against the local watchlist by timestamp
Trakt's watchlist pull previously fed only a live, un-timestamped,
display-only union in HomeLibraryCoordinator/AndroidLibraryDataSource
(distinctBy id, recomputed on every emission, never persisted or
pushed back). This wires it into the Phase 0/1 groundwork instead:

- TraktSyncItem now parses listed_at, exposed via
  TraktSyncClient.getWatchlistWithListedAt / TraktRepository
- ExternalSyncMergeBridge wraps the mergeWatchlistTimestamped
  core_invoke call for Kotlin callers
- WatchlistManager gains a local membership snapshot (active entries +
  removal tombstones) and applyRemoteWatchlistAdd
- HomeLibraryCoordinator.load() now reconciles Trakt's remote
  watchlist against the local one after each fetch: items the merge
  says are locally newer get pushed via the existing
  ExternalSyncPushCoordinator.pushWatchlist, items remote is newer on
  get applied into the local watchlist_entries table

Simkl and Stremio are follow-up work — their fetch shapes differ
(Simkl has no true watchlist endpoint, Stremio's is a unified
datastore blob) and need separate integration passes.
2026-07-18 19:16:22 +03:00
KhooLy
f11e9cee12 Stop passing raw Meta into PlayerSettingsPanel and EpisodeSidebar
Both only ever read id/type/seasonsCount from the 39-field Meta model,
in violation of the leaf-UI-gets-narrow-models rule in CLAUDE.md.
Adds seasonsCount to the existing PlayerContentUiModel (already
computed once via meta.toPlayerContentUiModel() at the top of
PlayerScreenContent) and threads that through instead.

The other Player files that take a Meta parameter (PlayerLoadingEffects,
PlayerTrackMemoryEffects, PlayerPipAndEpisodes, etc.) are LaunchedEffect
wrappers that hand Meta straight to ViewModel/repository calls for
persistence and scrobbling — that's the "effects/coordinators" layer
CLAUDE.md explicitly allows to hold domain objects, not a violation.
2026-07-18 18:58:38 +03:00
KhooLy
89c8c90669 Add local mutation timestamps for watchlist and watched state
Lays the groundwork for timestamp-based conflict resolution against
Trakt/Simkl/Stremio (matching the pattern Nuvio already uses), by
giving watchlist entries and watched episodes an updatedAt column and
a soft-delete tombstone table so removals carry a comparable
timestamp instead of vanishing on hard delete. Progress already had
updatedAt; this brings watchlist/watched up to the same level.
2026-07-18 18:36:18 +03:00
KhooLy
757d19e8f4 Improve calendar defaults and navigation icons 2026-07-18 18:04:41 +03:00
KhooLy
62d20e23c4 Refine mobile navigation and poster actions 2026-07-18 17:56:43 +03:00
KhooLy
909425202a Improve calendar library and catalog experiences 2026-07-18 16:52:38 +03:00
KhooLy
9a75c888f5 Add continue watching source preference and Nuvio account sync improvements
Adds a per-profile continue-watching source preference (Fluxa/Stremio/
Nuvio/Trakt/Simkl/AniList), a background worker to push playback
progress to Nuvio instead of pushing inline, dedup of mark-watched
pushes against already-watched episodes, and general Nuvio account
import/merge fixes.
2026-07-18 15:02:19 +03:00
KhooLy
92e7518d70 Fix CloudStream episode matching to avoid cross-season false positives
When a provider lists the requested season but not the requested
episode, fall through to no-match instead of an episode-only match
that could silently grab a different season's same-numbered episode.
2026-07-18 15:01:52 +03:00
KhooLy
ad06eb94f5 Add subtitle format labels and container track IDs; rework inline delay controls
Removes the standalone inline audio/subtitle delay adjust pill and
its state, and adds MediaTrack.subtitleFormatLabel/containerTrackId
so subtitle tracks can be labeled by codec (SRT/ASS/VTT/TTML/PGS/DVB/
VOBSUB).
2026-07-18 15:01:46 +03:00
KhooLy
ae3e135b6f Retire FluxaCoreNative's hand-written JNI in favor of core_invoke
Every stateless method on FluxaCoreNative now calls the corresponding
core_invoke route via FluxaCoreUniFfi instead of a hand-written
external fun; only the handle-lifecycle functions (headless engine,
app core state) keep their dedicated JNI bindings. NuvioCoreBridge and
ExternalLibraryClient's generic coreInvoke calls move onto the same
UniFFI path. Public method signatures are unchanged, so no other call
site needed to change.
2026-07-18 15:01:38 +03:00
KhooLy
c5df51f68a Relocate UniFFI codegen and bindings from app module to data module
FluxaCoreUniFfi.kt and the UniFFI-generated com.fluxa.core.uniffi
bindings only existed in the app Gradle module, but FluxaCoreNative.kt
(data module) needs to call into them and data cannot depend on app.
Moves the generateFluxaCoreUniFfiBindings task and the Kotlin facade
into data, which app already depends on.
2026-07-18 15:01:27 +03:00
KhooLy
276c4fb766 Move calendar business logic to fluxa-core via coreInvoke
Season-candidate selection, widget row formatting, and release
notification filtering now call the equivalent Rust calendar_plan
functions through core_invoke instead of duplicating that logic in
Kotlin.
2026-07-18 15:01:13 +03:00
KhooLy
0fa3ef723e Collapse bottom action row space while scrubbing
The row under the seekbar exited with fade and slide only, so its
layout space stayed reserved until the animation finished and then
snapped away. While scrubbing, the seekbar was therefore held up in
its buttons-visible position and the time and thumbnail preview
anchored above it floated high over empty space. The row now shrinks
vertically as it fades, taking its spacing with it, so the seekbar
settles to the bottom and the preview stays snug above it.
2026-07-17 17:59:52 +03:00
KhooLy
39b66eadf2 Freeze last frame on screen while scrubbing instead of disabling live thumbnails
Restores the live seek used to capture preview thumbnails during
scrubbing, but hides its side effect: when a drag starts, the current
surface frame is captured via PixelCopy and drawn full-screen under the
controls until the drag ends. The real player can jump between buckets
for thumbnail capture without the visible video changing; the frame you
started dragging on stays on screen until release commits the seek.
2026-07-17 17:53:34 +03:00
KhooLy
35ae8ea14a Fix adjust slider lag, drop fade-on-drag, and stop live seeking while scrubbing
Opacity sliders wrote the profile on every drag frame, which is what
made them stutter; they now hold a local live value that drives the
sample cue and only commit to the profile on release. With the panel
usable during drags, the fade-to-15% behavior is removed and the sample
cue moves lower (20dp from the bottom) instead.

The seek preview thumbnail system seeked the real player whenever a
drag paused over an uncached bucket, so scrubbing caused audible
jumps mid-drag. It now serves cached frames only, keeping the
background capture loop; the actual seek happens solely on seekbar
release, which the seekbar already implemented.
2026-07-17 17:50:08 +03:00
KhooLy
f9d4e664fe Keep subtitle preview visible under a compact top-right adjust card
The adjust card stacked its slider blocks two lines tall, making the
content roughly the full height of a landscape phone screen, so even
top-anchored it buried the sample cue. Slider items collapse to single
rows (title, slider, percent) and the card anchors top-right instead of
top-center, clearing the bottom-center subtitle zone both vertically
and horizontally.
2026-07-17 17:42:59 +03:00
KhooLy
01b85c0f1b Stop player menus leaking drags to volume gestures and make adjust page scroll
Drags on a sheet, card, or the speed popup were only consumed when a
child (slider, list) handled them; anything else bubbled up to the
player's volume/brightness drag layer, so swiping on the subtitle
adjust panel changed the volume. The shell root and the speed popup now
consume all unhandled drag gestures, while child scrollables and
sliders keep priority. The adjust page content also becomes vertically
scrollable since it can overflow the top-anchored card on short
landscape screens.
2026-07-17 17:40:38 +03:00
KhooLy
b4751da22e Move adjust entry to category headers and keep subtitles visible while adjusting
The adjust entry no longer scrolls with the track lists: each category
header (column label in the two-column card, the tab row in portrait)
carries a small tune icon. Audio's icon opens the inline delay pill
directly; the subtitles icon opens the adjust panel.

The adjust panel now anchors to the top of the screen with a near-zero
scrim so the bottom-center subtitle area stays visible, and while a
slider is being dragged the whole card fades to 15% opacity. A sample
cue styled with the live text/background/outline opacity values renders
at the subtitle position whenever the panel is open, so there is always
something to judge adjustments against.
2026-07-17 17:36:23 +03:00
KhooLy
d0207f50c6 Redesign player menus: anchored speed popup, combined tracks card, inline delay pill
Every player menu previously shared one full-chrome sidebar shell. The
shell now renders a translucent 24dp-radius card (0xE6101418, no
border) that fades in with a short upward drift, centered in landscape,
with no subtitle line and no close button; portrait keeps the bottom
sheet with its drag handle. Headers only render when a title or back
action exists.

Speed is no longer a sheet: it is a compact popup near the controls
that scales and fades in from its corner over a light scrim, listing
plain speed rows with the current one checked.

Audio and subtitles merge into a single Netflix-style two-column card
in landscape (portrait keeps the segmented tabs). Audio delay and
subtitle delay leave the card entirely: selecting them collapses the
menu to a floating pill over the video with -/value/+ controls so the
sync effect is visible live while adjusting. Subtitle styling keeps the
in-card adjust page.
2026-07-17 17:26:56 +03:00
KhooLy
0262acd528 Turn landscape player sheets into content-hugging floating cards
The landscape sheet stretched full height and flush to the screen edge,
so short menus like Audio or Speed left a huge empty void below their
rows. Side sheets are now 340dp floating cards (400dp on TV) rounded on
all corners, vertically centered with a 16dp edge margin, sized to
their content with a viewport-bounded max height. The slide-in
animation accounts for the margin, and the header is tightened with a
smaller, quieter close button.
2026-07-17 17:12:21 +03:00
KhooLy
5b153bfd3b Declutter player settings sheets
The sheets opened with an all-caps title, an instruction sentence, a
panel spanning almost half the screen, and two-line rows where every
speed carried a redundant Slower/Faster label. Titles are now title
case (Audio, Subtitles, Playback speed), the instruction line is gone,
the landscape sheet narrows from 46% to 38% width capped at 400dp,
rows tighten to 46dp with single-line speed entries (only 1x keeps a
Normal label), and mono/stereo channel counts render as words instead
of 1.0/2.0.
2026-07-17 17:07:59 +03:00
KhooLy
481aeb63e8 Restyle sidebar track rows with rounded selection highlight
Rows in the player sheets were flat full-bleed text with a 4dp inset;
the only selection cue was the trailing check. Rows are now rounded
12dp containers with a subtle white highlight behind the selected one,
matching ripple bounds, and the header subtitle is slightly larger.
2026-07-17 16:58:00 +03:00
KhooLy
7f93efbd41 Polish player sidebar open/close and unify speed menu
The sidebar previously entered with a fade plus a 44px nudge and
disappeared instantly on close. It now slides the full distance from
its edge (right for side sheets, bottom for bottom sheets) with the
scrim fading in sync, and close requests play the exit animation
before the panel is removed, including swipe-to-dismiss. Panel height
changes between tabs are animated instead of snapping.

The mobile speed menu drops its bordered chip grid in favor of the
same checkmarked rows used by the audio, subtitle, and source lists.
2026-07-17 16:55:54 +03:00
KhooLy
86c169de48 Suppress auto-PiP when launching external player
Opening the external player fires ON_PAUSE on MainActivity, and
PlayerLifecycleEffect auto-entered PiP because the player screen was
still active and the profile PiP setting was on. Gate the auto-enter
behind a suppression flag set for the duration of the external launch,
cleared on activity result and ON_RESUME.
2026-07-17 16:50:46 +03:00
KhooLy
bdc4ab6b9a Redesign player controls chrome and sidebar layout
Reworks the player overlay chrome, sidebar shell, settings and
source sidebars, mark-segment sidebar, and mobile dialogs, plus
new dimension/icon tokens and i18n strings for the redesign.
2026-07-17 16:03:54 +03:00
KhooLy
61e5b2bd52 Fix Discover crash on scroll from duplicate grid item keys
Addon catalogs can return overlapping items across paginated skip
offsets, so the discover results list could contain the same
type:id twice. LazyVerticalGrid requires unique keys and crashed
with IllegalArgumentException once a duplicate scrolled into view.
Dedupe results in the coordinator where they're assigned.
2026-07-17 16:03:47 +03:00
KhooLy
83f452da15 Use spring animation for bottom nav icon scale
Swaps the flat tween for a medium-bouncy spring so the selected tab
icon overshoots slightly on scale-up instead of growing linearly.
2026-07-17 16:03:40 +03:00
KhooLy
8df4874b0a Fix hero indicator dots not tracking swipe position
The 5-dot sliding window re-centered on the current page every swipe,
so the highlighted dot's position within the window stayed constant
while paging through the middle of the list. Show one dot per item
instead since the hero caps at 8 items.
2026-07-17 16:03:25 +03:00
KhooLy
3440831ed1 Preserve per-tab scroll position across bottom nav switches
Wrap the screen AnimatedContent in a SaveableStateHolder keyed by
screen so each destination's LazyColumn/LazyList state survives being
switched away from and back to, instead of resetting to the top.
2026-07-17 06:18:44 +03:00
KhooLy
8f459a0f76 Redesign bottom nav bar and home screen scroll/hero UX
Bottom bar now overlays content edge-to-edge with a scrim background
and top hairline instead of reserving fixed space, gains labels,
FluxaIcons, selection animation, and a calendar badge for new
releases. Home screen's filter/notification/avatar bar is pinned and
fades in as the hero scrolls past instead of scrolling away with it.
Hero auto-swipe now pauses during user interaction, gained a Play
button, capped height, and windowed dot indicators. Row "view all"
now navigates to the category grid instead of silently loading more
into the row.
2026-07-17 06:17:18 +03:00