Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
b533a5e
refactor(player): modularize player architecture and decouple subsystems
Himanth-reddy Aug 18, 2026
27cdd57
fix(player): implement triple-tap aspect ratio and double-tap seek in…
Himanth-reddy Aug 18, 2026
45f80ad
feat(player): wire PlayerEngine and persist playback settings
Himanth-reddy Aug 18, 2026
479a21b
feat(mobile): implement 2-stage drilldown subtitle picker sheet
Himanth-reddy Aug 18, 2026
d9ce3ff
fix(mobile): standardize full language names and polish subtitle sele…
Himanth-reddy Aug 18, 2026
eea2232
feat(mobile-player): bind system bars to controls visibility, refine …
Himanth-reddy Aug 26, 2026
f45c0ac
feat(player): add Auto, Fit to Screen, Stretch, Crop aspect ratio mod…
Himanth-reddy Aug 26, 2026
edf5e87
fix(mobile-player): overhaul error screen, add reload streams, disabl…
Himanth-reddy Aug 26, 2026
782f1dc
fix(mobile-player): prevent root menu flash when dismissing bottom sh…
Himanth-reddy Aug 26, 2026
6023d95
feat(mobile-player): switch to system auto-brightness when swiped dow…
Himanth-reddy Aug 26, 2026
ade1ab1
fix(mobile-player): allow single tap when locked to reveal unlock button
Himanth-reddy Aug 26, 2026
2730e0e
fix(mobile-player): add close button and disable gestures during init…
Himanth-reddy Aug 26, 2026
fe6c7e6
fix(mobile-player): use composite keys with index in lazy lists to av…
Himanth-reddy Aug 26, 2026
045868f
feat(player): implement on-device seek previews for mobile and tv mod…
Himanth-reddy Sep 2, 2026
4c25e06
fix(mobile-player): instant seek preview placeholder and live rendere…
Himanth-reddy Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 9 additions & 11 deletions app/src/main/kotlin/com/arflix/tv/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -634,12 +634,11 @@ fun ArflixApp(
// Hide bottom bar on player, profile selection, and login screens.
// TV route shows the bottom bar on mobile (touch devices) for easy navigation;
// the fullscreen IPTV player uses BackHandler to return to the guide.
val showBottomBar = isMobile && activeProfile != null &&
currentRoute != null &&
!iptvFullscreen &&
!currentRoute.contains("player") &&
!currentRoute.contains("profile") &&
!currentRoute.contains("login")
val isPlayerScreen = currentRoute?.startsWith("player") == true
val isFullscreenRoute = isPlayerScreen || iptvFullscreen
val isProfileOrLogin = currentRoute == Screen.ProfileSelection.route || currentRoute == Screen.Login.route
val showBottomBar = isMobile && activeProfile != null && currentRoute != null && !isFullscreenRoute && !isProfileOrLogin
val applySystemBarsPadding = isMobile && !isFullscreenRoute

val isPlayerRoute = iptvFullscreen || currentRoute?.contains("player") == true

Expand Down Expand Up @@ -677,11 +676,10 @@ fun ArflixApp(
)
}
)
// On mobile, push content below the status bar (except player).
// Applied AFTER background so the gradient fills behind the bars.
// statusBarsPadding() reads live WindowInsets, so it automatically
// becomes 0 when the player hides the bars.
.then(if (isMobile && !isPlayerRoute) Modifier.statusBarsPadding() else Modifier)
// On mobile, push non-player screens between the status bar and navigation bar.
// Player screens remain completely stable edge-to-edge without jumping when
// transient system bars appear or disappear.
.then(if (applySystemBarsPadding) Modifier.systemBarsPadding() else Modifier)
) {
Box(modifier = Modifier.weight(1f)) {
AppNavigation(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,8 +262,6 @@ fun DetailsScreen(
var showStreamSelector by remember { mutableStateOf(false) }
var showTrailerPlayer by remember { mutableStateOf(false) }
KeepScreenOn(active = showTrailerPlayer)
var pendingAutoPlayRequest by remember { mutableStateOf<PendingAutoPlayRequest?>(null) }
var autoPlayWaitTick by remember { mutableIntStateOf(0) }

// Episode Context Menu state
var showEpisodeContextMenu by remember { mutableStateOf(false) }
Expand All @@ -290,13 +288,34 @@ fun DetailsScreen(
tmdbSeason = tmdbSeason,
tmdbEpisode = tmdbEpisode
)
viewModel.loadStreams(imdbId, identity)
autoPlayWaitTick = 0
pendingAutoPlayRequest = PendingAutoPlayRequest(
identity = identity,
startPositionMs = startPositionMs,
requestedAtMs = SystemClock.elapsedRealtime()
)
val validStreams = uiState.streams.filter(::isAutoPlayableStream)
val minThreshold = minQualityThreshold(uiState.autoPlayMinQuality)
val selectedStream = bestAutoPlayStream(validStreams, minThreshold)

viewModel.recordPlayedEpisode(mediaId, identity)
if (selectedStream != null && !uiState.isLoadingStreams) {
onNavigateToPlayer(
mediaType,
mediaId,
identity,
imdbId ?: uiState.imdbId,
selectedStream.url?.takeIf { it.isNotBlank() },
selectedStream.addonId.takeIf { it.isNotBlank() },
selectedStream.source.takeIf { it.isNotBlank() },
startPositionMs
)
} else {
onNavigateToPlayer(
mediaType,
mediaId,
identity,
imdbId ?: uiState.imdbId,
null,
null,
null,
startPositionMs
)
}
}

// Spoiler blur setting
Expand Down Expand Up @@ -366,59 +385,6 @@ fun DetailsScreen(
suppressSelectUntilMs = SystemClock.elapsedRealtime() + 150L
}

LaunchedEffect(
pendingAutoPlayRequest,
uiState.isLoadingStreams,
uiState.streams,
uiState.autoPlayMinQuality,
autoPlayWaitTick
) {
val request = pendingAutoPlayRequest ?: return@LaunchedEffect

val validStreams = uiState.streams.filter(::isAutoPlayableStream)
val minThreshold = minQualityThreshold(uiState.autoPlayMinQuality)
val selectedStream = bestAutoPlayStream(validStreams, minThreshold)
val shouldWaitForSources = shouldWaitForAutoPlaySources(
isLoadingStreams = uiState.isLoadingStreams,
selectedStream = selectedStream,
elapsedMs = SystemClock.elapsedRealtime() - request.requestedAtMs
)

when {
selectedStream != null && !shouldWaitForSources -> {
val identity = request.identity
viewModel.recordPlayedEpisode(mediaId, identity)
onNavigateToPlayer(
mediaType,
mediaId,
identity,
uiState.imdbId,
selectedStream.url?.takeIf { it.isNotBlank() },
selectedStream.addonId.takeIf { it.isNotBlank() },
selectedStream.source.takeIf { it.isNotBlank() },
request.startPositionMs
)
pendingAutoPlayRequest = null
}
shouldWaitForSources -> {
delay(AUTOPLAY_SOURCE_RECHECK_MS)
autoPlayWaitTick += 1
}
uiState.isLoadingStreams -> Unit
validStreams.isNotEmpty() || uiState.streams.isNotEmpty() -> {
showStreamSelector = true
pendingAutoPlayRequest = null
}
else -> {
// When no streams found, show the StreamSelector with its
// friendly "no addons" / "no sources" empty state instead of
// navigating to the player which would show a scary error.
showStreamSelector = true
pendingAutoPlayRequest = null
}
}
}

// Place episode focus for whichever season is actually loaded. Keyed on currentSeason (which
// loadSeason updates atomically with episodes) so it also fixes the automatic season switch:
// moving to a different season focuses its first episode instead of leaving focus on the
Expand Down Expand Up @@ -543,7 +509,7 @@ fun DetailsScreen(
val ep = state.episodes.getOrNull(idx)
if (ep != null) {
episodeIndex = idx
if (isMobile || !state.autoPlaySingleSource) {
if (!state.autoPlaySingleSource) {
showStreamSelector = true
viewModel.loadStreams(state.imdbId, ep.identity)
} else {
Expand Down Expand Up @@ -1204,12 +1170,6 @@ private enum class FocusSection {
BUTTONS, EPISODES, SEASONS, RATINGS, CAST, REVIEWS, SIMILAR, COLLECTION
}

private data class PendingAutoPlayRequest(
val identity: EpisodeIdentity?,
val startPositionMs: Long?,
val requestedAtMs: Long
)

private fun handleLeft(
section: FocusSection,
buttonIdx: Int, episodeIdx: Int, ratingsIdx: Int, seasonIdx: Int, castIdx: Int, reviewIdx: Int, similarIdx: Int,
Expand Down
Loading
Loading