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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Settings:
- Replace the single-level category state with a real back stack
(lifted into FluxaAppState) so back navigation goes up one level
instead of always jumping to the hub, with hardware back wired
through MainActivity.
- Scroll position is now keyed per category instead of shared/leaking
across pages, and nested pages show their own titles.
- Split Notifications and TMDB feature toggles out of Account into
their own pages instead of an accordion and an overloaded
TmdbAccountChanged action bundling unrelated fields.
- Downloads settings page no longer duplicates Library's live download
list; it links out to it instead.
- Add a lightweight settings search (SettingsSearch.kt) over a static
index of rows.
- TV renders Settings as a two-pane list-detail layout.
- Introduce SettingsGroupCard and restructure every settings page
from a flat row list into grouped inset cards, restyle the search
field and choice-picker dialog to match the app's black/white
aesthetic instead of default Material styling, and merge the
redundant avatar/account rows on the hub.
- Add-ons settings page now only launches the real add-ons screen
directly (dropped the redundant "Manage Add-ons" middle page and
the torrent preferences that lived there).
Add-ons store:
- Validate pasted manifest URLs by actually fetching them before
installing, instead of accepting any string that pattern-matched
as a manifest path; show an inline error on failure and a
confirmation dialog on success.
- Replace the bare "+" icon button with a labeled Add button.
Also strips emoji baked into a few i18n strings (CloudStream repo/
plugin headers, premiere notification) per project convention.
Sweeps addon repositories, watchlist/profile storage, Trakt/Nuvio
sync, discovery models, and player policy classes out of Android-only
packages into data/commonMain and player/commonMain, with thin
androidMain wrappers where platform APIs are still required.
Continue the shared CMP/KMP migration: remove the legacy app/ui/catalog
Screen.kt files now superseded by shared/commonMain equivalents (Detail,
Search, Settings, Login, Profile, Watchlist, AddonStore, and the full
legacy Tv* screen set), and thread a DeviceType through FluxaApp/
FluxaAppHost so Android TV routes Home/Search/Discover/Calendar/Library
through shared destinations instead of falling into the removed legacy
composables (which previously hit an unreachable error() case).
Adds TvCatalogHomeScreen wiring plus new TvSearchScreen/TvDiscoverScreen/
TvCalendarScreen/TvLibraryScreen as thin TV-safe wrappers around the
existing shared screens. Removes the orphaned shared player/PlayerScreen.kt,
which had no call sites since the player stays platform-native by design.
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.
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.
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.
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.
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).
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.
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.
- 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 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.
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.
- 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
mpv:
- Set ao=audiotrack explicitly so the AudioTrack output is guaranteed
(avoids openSLES which lacks encoded passthrough)
- Add audio-channels=7.1,5.1,stereo so mpv negotiates multichannel
instead of defaulting to auto-safe stereo downmix
- Build audio-spdif at init time by querying AudioManager output device
encodings; only advertises AC3/E-AC3/DTS/DTS-HD/TrueHD formats the
device actually reports
ExoPlayer:
- Guard setVideoEffects(emptyList()) behind a videoEffectsActive flag:
only call it when IptPqc2ToneMapEffect was actually installed, never
on a clean path — the empty-list call triggers the GL VideoFrameProcessor
pipeline in media3 1.10.1 which tone-maps HDR even for an empty effect list
- detectPreferredVideoMimeType now accepts Context and checks display +
decoder DV capability first; on fully DV-capable devices it returns
VIDEO_DOLBY_VISION so DV variant tracks are preferred rather than
deprioritized behind H.265/H.264