Adds AppleAddonStoreSnapshot/AppleInstalledAddonSnapshot and update()
so Swift can push real addon list/error/added-name state back into
the shared Addon Store screen, plus
FluxaApple.setAddonStoreActionHandler/updateAddonStore following the
existing handler-setter pattern.
FluxaAppleAddonConfigurationStore gains a disabled-addon set and
enabledAddonUrls(); Home/Search/Discover/Detail now fetch from
enabledAddonUrls() instead of the raw configured list, so disabling an
addon in the store actually stops it from being queried.
Adds name/description/logo/version/configurable to
AppleAddonManifestSnapshot so the iOS Addon Store can show real addon
metadata instead of just catalog capability flags.
FluxaApplePluginsStartup routes ApplePluginsActionSnapshot actions
from the shared UI to FluxaApplePluginRepositoryManager and pushes
merged state back to Kotlin. FluxaIosApp constructs and registers it
alongside the other feature startups, restoring persisted repos on
launch.
Adds ApplePluginsSnapshot and update() so Swift can push merged
repository/scraper/settings-sheet state back into the shared Compose
UI, plus FluxaApple.setPluginsActionHandler/updatePlugins following
the existing setSearchHandler/updateSearch pattern. Also fixes
saveScraperSettings to actually forward the settings values.
Mirrors Android's PluginRepositoryManager: owns its own headless
runtime instance, dispatches repo/scraper actions, persists state via
UserDefaults, and exposes scraper settings layout and execution.
Fetches plugin repository manifests over URLSession and parses them
via coreInvoke, matching Android's FluxaAndroidHeadlessEnvironment
fetchPluginManifest behavior.
Implements the generated PluginHttpClient callback interface
synchronously via URLSession, with an SSRF guard (scheme allow-list
plus resolved-address blocklist) mirroring Android's PluginNetGuard.
Replaces the checked-in 6-function stub with actual uniffi-bindgen
output, exposing PluginHttpClient, PluginHttpRequest/Response,
executePluginScraper, getPluginScraperSettingsLayout, and coreInvoke
to Swift for the first time.
@JvmSuppressWildcards (TraktModels.kt) and java.util.Locale (PlayerOverlayCards.kt)
are JVM-only and don't resolve under Kotlin/Native. Neither was needed:
the wildcards annotation has no runtime effect Gson relies on, and
uppercase(Locale.ROOT) has a locale-agnostic uppercase() equivalent in
the common stdlib. Verified :shared:compileKotlinIosSimulatorArm64 now
succeeds (previously failed on master too, unrelated to plugin work).
Addons now only manages Stremio addons. CloudStream3 repo/plugin state
and the Android-only Nuvio scraper settings screen both move into a new
shared KMP feature (feature/plugins) with a real Android data source
combining PluginRepositoryManager and PluginManager, and an iOS stub
data source pending a follow-up Swift implementation.
rquickjs-sys has no prebuilt FFI bindings for iOS/tvOS targets, so
bindgen needs the matching SDK sysroot (iphoneos/iphonesimulator/
appletvos/appletvsimulator) and clang target triple per cross-compiled
target, or it silently uses the host's own headers and fails. Unverified
here — no Xcode/macOS available in this environment; needs confirming
on a real Mac before relying on it.
Scrapers are now grouped under their owning repository instead of two
separate flat lists, and the screen reuses AddonStoreScreen's visual
language (icon-circle back button, bordered repo cards, icon buttons)
instead of its own bespoke plain-text styling. Adds a refreshRepository
action reusing the existing re-fetch dispatch.
Wire executePluginScraper's updated scraper_id/scraper_settings_json
params through FluxaCoreUniFfi and PluginRepositoryManager, add
getSettingsLayout() to fetch and parse a scraper's onSettings() field
layout into small UI models, and add a gear-icon-triggered bottom
sheet (PluginScraperSettingsSheet) rendering header/info/text/select/
toggle fields, matching Nuvio's real settings schema.
PluginRepositoryManager's headless engine instance was in-memory only, so
installed plugin repositories and disabled/configured scrapers vanished
on every process restart. Now persists repository URLs and per-scraper
enabled/settings overrides to SharedPreferences, and replays them as
headless actions on startup (relying on the Rust-side fix that makes a
repository re-fetch preserve existing overrides instead of resetting to
manifest defaults).
Also adds updateScraperSettings(), used by the persistence replay and by
the upcoming per-scraper settings UI.
fluxa_core already emitted these for plugin-sourced streams, but Gson
silently dropped them since Stream had no matching properties. Renamed
the existing getHeaders() to resolveHeaders() to avoid a JVM signature
clash with the new headers property, and merged it into the raw
behaviorHints-derived headers.
Implements PluginHttpClient over OkHttp with SSRF guarding (PluginNetGuard),
wires the FetchPluginManifest effect, adds PluginRepositoryManager for
repository/scraper state, folds plugin-scraper streams into
StreamDiscoveryUseCase, and adds a Plugins settings screen.
fluxa-core's plugin-js-engine feature now generates rquickjs FFI
bindings via bindgen instead of relying on prebuilt binding files,
since rquickjs-sys ships none for any Android target. Export
LIBCLANG_PATH (from the NDK's own libclang) and
BINDGEN_EXTRA_CLANG_ARGS (NDK sysroot, matching clang resource-dir,
and the concrete clang target triple) alongside the existing
CC/AR/linker env vars in each buildFluxaCore<Abi> task, so bindgen
resolves Android headers instead of falling back to the host's.
StremioAddonManifestClient, StremioAddonResourceClient, and the trailer
effect in FluxaAndroidHeadlessEnvironment each hand-rolled the same
Request.Builder -> execute -> read status/body -> catch pattern. One
shared executor now backs all three call sites.
New setting under Settings > Appearance > Home Screen: when enabled,
Continue Watching items whose next episode/season hasn't released yet
move into their own Upcoming row instead of sitting in Continue
Watching with nothing to resume.
Classification runs async per up-next series item (no active playback
progress): HomeContinueWatchingCoordinator resolves the specific
lastVideoId episode via the existing getSeasonEpisodes fetch, checks
its release date through the new isEpisodeReleased FFI call, and
caches the result by id:lastVideoId. Once known, it triggers a
dynamic-rows refresh so the split applies without blocking the
initial Home paint.
HomeCatalogFeedCoordinator/HomeDynamicRowsCoordinator partition the
Continue Watching list into continue_watching/upcoming HomeCategory
rows via the new isUpcoming lambda, gated by
UserProfile.upcomingRowEnabled (resolved through the Rust safe-prefs
struct like every other appearance toggle). HomeCategoryPolicy's
isContinueWatchingCategory()-gated behavior (card layout, action-row
treatment, title passthrough) now also covers the upcoming row so it
renders with the same horizontal episode-card treatment.
CatalogCard's onLongClick had no discoverable remote equivalent — a
D-pad user had no visible way to know holding a button might do
something. Add a small focusable '+' badge on each search result card
on TV that triggers the same PosterActionSheet action as long-press.
Detail's season-picker ModalBottomSheet and Discover's filter
ModalBottomSheet both had trigger chips and row items styled with
plain .clickable and no focus feedback, plus no initial focus when the
sheet opened. Add focus rings to trigger/rows and request focus on the
selected row on open, same fix already applied to TvAuthScreen.
Library's collection-rename AlertDialog already used stock Material3
widgets with adequate focus support; just add initial focus to its
text field.
Reached from every 'see all' row on Home/Discover; had no deviceType
awareness at all. Add TV padding/type-scale/grid sizing in line with
the other Tv*Screen wrappers. Card focus rings come from the
CatalogCard fix already landed.
ProfileGridItem/AddProfileGridItem had no focus indicator and the
screen (the sole content right after login) never requested initial
focus, so a D-pad had nothing to land on. Add a focus ring to both and
request focus on the first profile when the list appears.
The per-addon action row (configure/refresh/move-up/move-down/remove)
packed 5 Material IconButtons with no way to tell which was focused.
Replace with a shared AddonIconButton that shows a clear focus state,
and add the same treatment to the back button, repo cards, and
CloudStream plugin rows.
SettingsRows.kt had zero focus styling on any interactive element
(toggle switch aside): choice/nav/action/connection rows, the stepper
+/- buttons, ordered-toggle checkbox and reorder buttons, color
swatches, and the secret-field reveal toggle. Add a shared
settingsFocusRing() modifier and apply it across the row primitives so
the whole settings tree gets consistent focus feedback in one place,
plus fix the TV category rail row and accent-color swatches in
SettingsScreen.kt directly.
CatalogCard.kt disabled default indication with nothing put back, so
every catalog grid on TV (Home, Search, Discover, Library,
CategoryResults, LibraryFolderDetail) was navigable but silently
invisible when focused. Add a border/scale focus ring at the shared
component level instead of duplicating it per-screen.
PlayerControlBtn/SeekIconButton/the play-pause circle in
PlayerControlsChrome.kt were focusable() on TV but never changed
appearance on focus. TrackItem in PlayerSidebarShell.kt (source/audio/
subtitle/episode picker) had the same problem. Both now show a clear
focus state.
showNavigationBar was hardcoded true with no per-destination gating, so
both the mobile bottom bar and the new TV sidebar rendered on top of
the pre-login Auth/ProfileList screens, where there's no session yet
to navigate Home/Library/etc into.
AuthScreen had no TV variant and no explicit focus handling, so a
remote's D-pad had nothing reliable to land on or move between during
login. TvAuthScreen mirrors the same AuthStage state machine but adds
visible focus rings on every interactive element, requests initial
focus on the primary action per stage, and centers the form in a
fixed-width column instead of mobile's edge-to-edge layout.
FluxaNavigationBar was a touch-oriented pill bar rendered unconditionally
for every device type, including TV, with no deviceType gate. Add
TvSidebarNav as a proper left-edge D-pad rail for TV and reserve
horizontal (not vertical) space for it; drop TvCatalogHomeScreen's
redundant top icon row now that the sidebar owns navigation.
androidFluxaPlatformServices was only built for DeviceType.Mobile, but
AppRoutesHost always force-unwraps it, so the TV build crashed with an
NPE on every launch.
Add a radial progressive blur (stronger in the center, tapering at
the edges) so the surface reads as a lensed bulge rather than a flat
uniform blur, layer a top-inner-glow-to-bottom-inner-shadow gradient
behind the icons for real inner depth, and brighten inactive
icons/labels to 78% white for readability.
Replace the single weak white tint with a proper dark scrim + faint
sheen (via HazeStyle's tints list) so busy content behind the bar
stops fighting with the icons, add a real drop shadow beneath the bar
for layered depth, brighten the top-edge highlight, give the selected
icon a two-layer accent-colored glow instead of a plain white blob,
and give the Home row extra bottom content inset (nav bar height + 20dp)
in Liquid Glass mode so the last row of text clears the bar instead of
sitting under it.
Lighter backdrop fill and overall effect alpha so more content bleeds
through, a vertical-gradient edge (white top highlight fading to a
dark bottom inner shadow) instead of a flat border, a soft radial
"lens" glow behind the selected icon, and brighter inactive icons
(68% white) for readability against the lighter glass.
Pulls in dev.chrisbanes.haze 1.7.2 (Android + iOS klibs, matching the
shared module's targets) and swaps the earlier translucency-only
fallback for genuine hardware-accelerated background blur: the screen
content is marked as a hazeSource, and the nav bar reads it through
hazeEffect with a tinted, noisy glass style.
New opt-in Appearance toggle (default off), mirroring how amoledMode
is wired through UserProfile and both settings data sources. Enabling
it swaps the nav bar's flat fill for a translucent frosted surface
with a soft top-light gradient and a hairline glass-edge border.
Both sources fed the same skip-segments pipeline and were always
toggled together in practice, so collapse them into a single
useSkipSegments flag on SettingsPlaybackUiModel. Both the Android and
Apple settings data sources still write/read the two underlying
useIntroDb/useAniSkip flags in lockstep, so the merge stays UI-level
and existing profiles keep working.
imdbapi.dev moved its base URL; also drops the now-dead
ImdbApiService.create() factory since HomeViewModel already gets its
instance through Hilt DI in NetworkModule.
Adds contentWarningsEnabled to the playback settings model, defaulting
to true, and gates both the parents-guide fetch and the overlay
display in PlayerScreen behind it.
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.
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.
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.
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.
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.
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.
- 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.
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.
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.