java.text.DateFormatSymbols/SimpleDateFormat are JVM-only, so the
calendar screens' weekday names, month titles, and short dates
couldn't move to KMP as-is. Adds an expect/actual trio in :core
(localizedShortWeekdayNames, localizedMonthTitle, localizedShortMonthDay)
— Android actual keeps the existing DateFormatSymbols/SimpleDateFormat
behavior, iOS actual uses NSDateFormatter/NSCalendar/NSLocale.
MobileCalendarDateUtils.kt's shortWeekdays/monthTitle/
formatCalendarListDate now delegate to these instead of doing the
java.text work directly — same signatures, so all 6 call sites
(TvCalendarScreen + 5 mobile calendar files) are untouched. This is
groundwork only: buildMonthCells/shiftMonth still use java.util.Calendar
and the calendar screens themselves aren't ported — that needs the
screens' JavaCalendar-typed state converted to kotlinx-datetime, a
separate, larger change.
Verified compiling for android, iosArm64, iosSimulatorArm64, and both
app product flavors still build.
Hilt is Android/Dagger-only and can't run on Kotlin/Native, so it
can't be used for anything moving into :shared. Koin is the standard
KMP alternative — this wires it up for real: a Koin module
(sharedModule) providing GreetingViewModel via the koin viewmodel DSL,
CmpSmokeTest now resolves it through koinViewModel() instead of
constructing it directly, and AppApplication.onCreate() calls
initSharedKoin() at startup so this isn't just compile-checked dead
code — Koin actually starts alongside Hilt (the two coexist fine,
verified by compiling the full app afterward).
This is the pattern for every ViewModel/ScreenModel that moves to
:shared going forward. HomeViewModel/DetailViewModel themselves stay
Hilt-based in app for now — porting them means porting their
repositories to KMP first, which is separate, larger work.
Verified compiling for android, iosArm64, iosSimulatorArm64, and both
app product flavors still build with Koin initialized at startup.
DeviceType/LocalDeviceType/touchClickable and the four poster/
horizontal card size preset functions were pure Compose (no Android
imports beyond foundation.clickable, which is multiplatform) — same
straight move as FluxaColors/FluxaDimensions, same package name, zero
call-site changes across the 38 files that reference them.
This unblocks moving more of the mobile card layout math into :shared
later, since the device-aware sizing helpers no longer live only in
the Android-only app module.
Verified compiling for android, iosArm64, iosSimulatorArm64, and both
app product flavors still build.
Adds CatalogCardUiModel + CatalogCard to :shared: a fully-resolved,
primitive-only render model (Dp/Sp/String/Float, no Meta/UserProfile)
and a Coil3-based composable that draws it, per this project's UI
optimization rule (leaf UI components get small immutable models, not
domain objects). MobileMovieCard.kt in app becomes a thin mapper —
Meta/UserProfile/DeviceType-aware layout math stays Android-side,
producing a CatalogCardUiModel that the shared composable renders.
Pinned coil3 to 3.4.0 (was 3.5.0): 3.5.0's iosarm64/iossimulatorarm64
klibs were built with a newer Kotlin ABI (2.4.0) than this project's
compiler (2.3.20) can consume. Revisit once the project bumps Kotlin.
Verified: CatalogCard/CatalogCardUiModel compile for android,
iosArm64, and iosSimulatorArm64; app's mobile and tv flavors both
still build against the rewritten MobileMovieCard.
First real slice of ui/catalog moving to :shared: both objects were
already pure Compose (Color/dp/sp), no Android imports, so this is a
straight module move with zero call-site changes elsewhere in app —
they keep the same package (com.fluxa.app.ui.catalog), so every
existing same-package and imported reference resolves unchanged
across the module boundary. Verified compiling for android,
iosArm64, and iosSimulatorArm64, and that both app product flavors
still build against it.
Every future shared mobile composable needs this theme foundation,
so it's the natural first thing to move.
New :shared module with a fluxa.kmp.compose convention plugin (Compose
Multiplatform's runtime/foundation/material3/ui, layered on the
existing fluxa.kmp.library android+iOS target setup) plus a smoke-test
composable proving a real @Composable using Material3 compiles for
android, iosArm64, and iosSimulatorArm64.
This is where ui/catalog/mobile/** screens will move to in the next
phase, sharing the same UI code between Android mobile and iOS.
Phase 0 of the iOS migration: core now targets android, iosArm64, and
iosSimulatorArm64 via a new fluxa.kmp.library convention plugin.
AppStrings and the TTL cache lose their JVM-only dependencies
(org.json, java.time, java.util.concurrent) in favor of
kotlinx-serialization, kotlinx-datetime, and small expect/actual
platform shims, with Locale-returning APIs kept as Android-only
extension functions since only Android UI consumes them today.
startTorrentServer's result was logged as success and the watcher was
started unconditionally, even when the Rust call failed to report
readiness (blank result). Gate both on the actual running state.
Two streams with the same playableUrl (or both missing one and sharing a
title) previously collided on the same key, which could cause item state
to leak between rows during scroll/recomposition.
The always-mounted NativeLibassSubtitleOverlay SurfaceView sits above the
video SurfaceView (setZOrderMediaOverlay). Its canvas was only ever locked
and cleared when an ASS renderer was active, so with no embedded ASS track
selected the overlay buffer kept whatever garbage content it started with.
On some devices (reported on Redmi/MIUI) that composites as opaque black,
hiding the video behind fully-drawn player controls.
- Use Chapters for Skipping: gates the existing chapter-derived skip
segment fallback (already computed by deriveSkipSegmentsFromChapters)
behind a new toggle instead of applying it unconditionally. Also
brought TV's Skip Segments section up to parity with mobile (IntroDB +
API key, AniSkip, chapter toggle, auto-skip), since TV was missing all
but auto-skip.
- Japanese audio for anime: overrides the resolved preferred audio
language to "ja" for anime-genre content when enabled, at the single
entry point both audio and subtitle track resolution already share.
- Autoplay countdown: the next-episode card now counts down and fires
automatically, instead of only advancing on click.
- Auto-retry next source: extends the existing Cloudstream-only
auto-fallback-on-error path to also apply generally when enabled.
Both onConnectStremio and MobileNuvioLoginView already existed but had no
UI trigger anywhere in Settings — onConnectStremio was a dead parameter
never invoked, and the Nuvio login view was only reachable from the
initial onboarding screen. Added a Stremio connection tile (TV + mobile)
and a Nuvio import row (mobile only, since TV's login screen has no
Nuvio path) that reuse the existing Screen.Login(startOnNuvio) navigation
already used by onboarding.
Mirrors the existing Trakt/MyAnimeList/Simkl connection pattern: a
fluxa://oauth/anilist deep link, authorize-URL launch, token exchange
through a new ExternalOAuthClient.exchangeAnilistCode call, and profile
fields (anilistAccessToken/RefreshToken/TokenExpiresAt) merged via the
existing authExchangeRequested headless effect. Adds a matching
AuthProvider::Anilist arm in fluxa_core's token_merge_plan_json so the
shared Rust merge path recognizes the new provider too.
Connect/disconnect tiles added to both TV and mobile Settings, folded
into the existing multi-provider "Disconnect" action.
Note: app/build.gradle.kts also carries a small pre-existing uncommitted
fix (rustCrateDir path) unrelated to this change, included here only
because it was already sitting in the working tree on the same file.
Adds a real network-transport check (ConnectivityManager) before starting
a torrent stream when Wi-Fi-only is enabled in Settings, refusing to start
on cellular instead of silently ignoring the toggle.
Note: this file already carried unrelated uncommitted engine-resiliency
work (ensureEngineReady/health-check/restart-on-failure) from before this
change — that part isn't mine, it's included here only because it was
already sitting in the working tree and touches the same file.
Adds the missing torrentSpeedPreset settings row (the field already
controlled preload size) and a new Wi-Fi-only toggle. The Android-side
gating for Wi-Fi-only lives in TorrentStreamManager.startStream (see the
next commit); this piece wires the active profile's torrentWifiOnly flag
through the startTorrentStream headless effect.
torrentMaxConnections and torrentCachePreset are intentionally left
unexposed: the underlying rqbit torrent engine has no per-torrent
connection-count cap or disk-cache-size knob to back them, so a toggle
for either would be a no-op.
These profile fields (notificationsEnabled, automaticUpdates, subtitleShadow,
trailerOnHomeHeroEnabled/DelaySeconds) already drove real behavior but had no
visible Settings UI on TV or mobile. Also gates the background update-check
loop in MainActivity behind automaticUpdates, and wires a TV "Check for
updates" action mirroring the existing mobile flow.
The Kotlin innertube client had drifted onto deprecated client configs
(ANDROID_TESTSUITE, TVHTML5_SIMPLY_EMBEDDED_PLAYER) that YouTube no
longer accepts. Route through FluxaStreamingNative's new
resolveYoutubeTrailerJson instead, so there's one maintained resolver
shared with the desktop app rather than two independent copies to keep
working. Public API (TrailerResolveResult/TrailerResult/SubtitleInfo)
is unchanged, so callers need no changes.
Add a reusable shimmer skeleton (FluxaSkeleton) and use it for the
category results grid's initial load, replacing the lone centered
spinner with placeholder poster cards that match the grid layout.
Give back, search, cast, picture-in-picture, external-player, and
calendar month-navigation buttons localized accessibility labels so
TalkBack announces their action. Adds the supporting common.* i18n keys.
Strip inline, block, and doc comments across the ui package to comply
with the project's no-comments rule. Preserves noinspection directives
and string literals containing // (URLs).
lastEpisodeName is saved as "S1, E3: Name" but the prefix-stripping
regex only matched "S1: E3" and "S1 E3" forms, never the comma form the
app itself writes. The unstripped title then tripped the duplicate-code
guard in continueWatchingEpisodeLabel, which discarded it and left the
bare "S1, E3". The regex now handles the comma form, and a title that
already carries the code is shown as-is instead of dropped.
Embedded MKV font attachments now reach libass reliably. The font scan
was disabled for the local torrent proxy, which is where all torrent
playback lives, so styled fansub tracks fell back to system fonts.
Font sourcing is now layered: a persistent PrefixCaptureBuffer harvests
attachments from the bytes ExoPlayer already downloads (the old capture
died with each ExtractorInput ExoPlayer recreated per load), and an HTTP
scan locates the Attachments element via the SeekHead and fetches it
exactly instead of guessing with an 8 MB prefix. The scan waits for the
player's own connection to open before touching the proxy, backs off
hard between retries, and aborts once fonts exist from any source.
Fonts that arrive after renderer creation recreate it and replay
buffered events. Truncated attachments are dropped instead of being fed
to FreeType half-parsed.
Two compositing bugs caused the sheared "fake italic" glyphs: coverage
rows are packed 4-byte aligned but ALPHA_8 copyPixelsFromBuffer consumes
bitmap rowBytes, so odd-width glyphs skewed one pixel per row; and the
glyph cache was keyed by the native ASS_Image bitmap pointer, which
libass reuses across frames, serving stale glyphs. Rows are repacked
when strides differ and the cache is keyed by content hash.
Startup cost is now near zero: renderer creation moved off ExoPlayer's
loader thread onto the libass HandlerThread, and XDG_CACHE_HOME points
at app storage so fontconfig's /system/fonts scan (~2 s) is cached and
subsequent inits take ~80 ms. libass diagnostics stream to logcat via
ass_set_message_cb.
Five architectural changes:
1. PTS-driven clock: VideoFrameMetadataListener on ExoPlayer delivers the
exact presentation timestamp of each video frame. LibassRenderThread
drives rendering at that PTS rather than polling currentPosition.
2. Translucent SurfaceView: NativeLibassSubtitleView becomes a SurfaceView
with PixelFormat.TRANSLUCENT and setZOrderMediaOverlay(true). Its surface
is composited by the hardware layer between the video surface and the Compose
window, with zero main-thread involvement during playback.
3. Single HandlerThread ownership: LibassRenderThread owns the renderer handle.
All operations (render, addEvent, clearEvents, surface lifecycle, teardown)
are Handler messages to this thread. The ReentrantLock is gone — no
contention because there is no sharing.
4. ALPHA_8 glyph patches via lockHardwareCanvas: nativeRenderImages returns the
ASS_Image list as flat int/byte arrays instead of blending into a full ARGB
frame. Kotlin draws each glyph as a tiny ALPHA_8 bitmap on the hardware
canvas with a tinted Paint. Glyph bitmaps are cached by libass bitmap pointer,
so a static or moving subtitle re-draws cached textures at new positions and
uploads nothing.
5. detect_change gating: nativeRenderImages returns -1 when detect_change == 0.
Kotlin skips lockHardwareCanvas entirely — zero work when the frame is
unchanged. Paused playback is fully idle.
LibassEventRelay routes setHeader, addEvent, clearEvents through the thread.
relay.activeRenderer remains a StateFlow<NativeLibassRenderer?> for the existing
PlayerPlaybackSurface observer. Tests updated to drain the HandlerThread between
async relay operations and direct render() calls.
Pre-extraction is skipped for non-local streams so embeddedNativeAssTracks
is always empty, which meant embeddedSubtitle was always null, which meant
the relay renderer was never assigned as the active renderer. Relay renderer
should win whenever there is no external subtitle selected.
- HomeSearchFocusStateHolder: guard against null list fields in core state snapshot
to avoid NPE on first-run or partial state
- StreamSourceUi: skip appending quality label when the filename already contains it
- MobileDetailHero: accept featuredTrailer parameter and fall back to it when no
actively selected trailer exists, so the hero auto-plays on open
- SearchSharedComponents: SearchEmptyState now accepts an optional icon and centers
its title text
- MobileSettingsCategoryDetail: switch from loadedCs3ApiNames to
loadedCs3CatalogFeedOptions StateFlow for per-catalog feed management
- FluxaAndroidHeadlessEnvironment: import ScraperActor and related CS3 types;
pass requestedVideoId from effect payload into stream loading
- build.gradle.kts: update Nuvio Supabase URL default and add publishable key default
- Remove AvatarLibrary/AvatarProvider usage and the LazyRow character grid from
both mobile and TV profile edit screens
- Wire ActivityResultContracts.GetContent for photo picking via system file chooser
- Add openSettings hook to CloudStream Plugin base class
- Add player_surface_crop_animator_tag and player_surface_crop_target_tag to ids.xml
for animated crop-to-fill tracking in PlayerPlaybackSurface
- Add outlined icon variants (Home, Explore, DateRange, VideoLibrary, Settings)
to FluxaIcons for the unselected state
- MobileBottomNavItem now carries a label string; the nav bar renders it beneath
each icon using AppStrings so it respects the active language
- Slightly darken nav background and inactive tint for legibility
- Define TvNavActions data class and TvNavDestination enum in TvHomeNavRail
- Thread TvNavActions into Explore, Search, Watchlist, and Settings so those
screens can render the nav rail or top bar themselves instead of being bare
- TvExploreScreen, TvSearchScreen, and TvSettingsComponents adapt their layout
based on safeTvNavLayout == "top": rail gutter 126dp vs top-bar gutter 56dp,
content top padding 40dp vs 108dp
- Settings screen: add "general" and "appearance" tabs, rename "downloads" to
"content", reorder to match new tab structure; apply rail/top gutter
- MainActivity back-navigation: if activeProfile is null and profile list is
empty, navigate to Login instead of looping on Home
- Account for pixelWidthHeightRatio when computing exoVideoAspectRatio so
anamorphic streams fill correctly
- Replace gestureState.pinchBelowFitAccum with a snappedToFillThisGesture flag:
one pinch-out snaps to fill, subsequent movement cycles the zoom overlay
- Clamp fillScale to MIN_MANUAL_FILL_SCALE (1.34) so landscape content always
fully covers the screen before the crop overlay appears
- ExoPlayerEngine.setZoomed now sets VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING
vs SCALE_TO_FIT instead of being a no-op
- Wire iptPqc2UseHdr and iptPqc2PreDecide from resolved stream data into the
engine request so the IPT PQ C2 tone-map effect receives its configuration
- On ExoPlayer error during CS3 playback, try the next available stream URL
automatically (round-robin, skipping already-failed URLs stored in
failedAutoFallbackUrls) before showing the error overlay
- Stall watchdog passes the same fallback path so buffering timeouts also
cycle to the next link on CS3 content
- Episode metadata line format: use "S1, E2: Title" separator and handle
Video objects that already embed season/episode without needing a catalog fetch
- Add Cs3CatalogFeedDescriptor and per-catalog feed keys (cs3CatalogFeedKey) so
each CS3 catalog gets its own home row instead of one per plugin
- Build CS3 metadata feed options from per-catalog descriptors in buildCs3MetadataFeedOptions
- HomeBillboardLoader now fetches CS3 feeds alongside stremio addon feeds for
billboard pool population
- Stream.kt: send Cloudstream referer header lowercase ("referer") to match how
Media3 passes custom request properties
- HomeContentFormatters: skip cs3: video IDs in parseShortVideoId to avoid
incorrect episode line parsing
- HomeLayoutHelpers: include poster as fallback in mobileHeroArtworkCandidates
- Return UpdateReport(updatedPlugins, failedPlugins) from auto-updater so failures
are tracked separately from successes
- Show a distinct notification when one or more plugins fail to update, using
separate notification IDs so the success and failure toasts don't stomp each other
- Add installKey() to Plugin for stable deduplication by (name, repositoryUrl) pair;
use it in isPluginInstalled to avoid duplicate installs on re-run
- Add i18n keys notification.plugins_update_failed_title/body in en-US and tr-TR
CastMemberCard, TrailerCard, and SimilarContentCard now build ImageRequests with
memoryCacheKey and diskCacheKey so images survive across recompositions and
LazyRow recycling without being re-fetched.
When the user picks a different season on the TV detail screen, the episode
LazyRow now scrolls back to position 0 so the first episode of the new season
is in view instead of leaving the indicator wherever it was in the previous
season's list.
Consecutive same-direction presses ramp up the jump amount: first 2 presses use
the profile seek interval, presses 3-7 triple it, and presses 8+ sextuple it.
Direction change or focus loss resets the counter. This lets quick taps do fine
scrubbing while holding the key covers large spans fast.
While the home catalog is loading and no rows are available yet, both the mobile
and TV home screens now show 4 animated skeleton shelf placeholders instead of a
blank screen. The pulse animation matches the explore screen skeleton.