Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
71 changes: 48 additions & 23 deletions app/src/main/kotlin/com/arflix/tv/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import android.view.ViewTreeObserver
import android.view.WindowManager
import com.arflix.tv.R
import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.FastOutSlowInEasing
Expand All @@ -27,6 +29,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
Expand Down Expand Up @@ -72,6 +75,7 @@ import com.arflix.tv.util.LocalAppLanguage
import com.arflix.tv.util.LAST_APP_LANGUAGE_KEY
import com.arflix.tv.util.detectDeviceType
import com.arflix.tv.util.deviceHasTouchScreen
import com.arflix.tv.util.findActivity
import com.arflix.tv.util.settingsDataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.stringPreferencesKey
Expand Down Expand Up @@ -239,30 +243,20 @@ class MainActivity : ComponentActivity() {
DeviceType.PHONE -> ActivityInfo.SCREEN_ORIENTATION_FULL_USER
}

// All devices use edge-to-edge (setDecorFitsSystemWindows=false).
// TV hides the bars; mobile keeps them visible and Compose handles
// insets via systemBarsPadding() in the root layout.
WindowCompat.setDecorFitsSystemWindows(window, false)
if (initialDeviceType == DeviceType.TV) {
WindowCompat.setDecorFitsSystemWindows(window, false)
WindowInsetsControllerCompat(window, window.decorView).apply {
systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
hide(WindowInsetsCompat.Type.systemBars())
}
} else {
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.dark(android.graphics.Color.TRANSPARENT),
navigationBarStyle = SystemBarStyle.dark(android.graphics.Color.TRANSPARENT)
)
// Clear any FLAG_FULLSCREEN the Leanback theme may have set
@Suppress("DEPRECATION")
window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
// Transparent bars — the dark app background shows through them.
// White (light) icons are used since the background is dark.
@Suppress("DEPRECATION")
window.statusBarColor = android.graphics.Color.TRANSPARENT
@Suppress("DEPRECATION")
window.navigationBarColor = android.graphics.Color.TRANSPARENT
WindowInsetsControllerCompat(window, window.decorView).apply {
show(WindowInsetsCompat.Type.systemBars())
isAppearanceLightStatusBars = false // white icons on dark bg
isAppearanceLightNavigationBars = false // white icons on dark bg
}
}

lifecycleScope.launch(kotlinx.coroutines.Dispatchers.IO) {
Expand Down Expand Up @@ -647,6 +641,25 @@ fun ArflixApp(
!currentRoute.contains("profile") &&
!currentRoute.contains("login")

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

val hostActivity = remember(context) { context.findActivity() }
LaunchedEffect(isPlayerRoute, isMobile) {
if (isMobile && !isPlayerRoute) {
val win = hostActivity?.window ?: (context as? ComponentActivity)?.window
if (win != null) {
@Suppress("DEPRECATION")
win.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
WindowInsetsControllerCompat(win, win.decorView).apply {
systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
show(WindowInsetsCompat.Type.systemBars())
isAppearanceLightStatusBars = false
isAppearanceLightNavigationBars = false
}
}
}
}

Column(
modifier = Modifier
.fillMaxSize()
Expand All @@ -664,11 +677,11 @@ fun ArflixApp(
)
}
)
// On mobile, push content between the status bar and navigation bar.
// On mobile, push content below the status bar (except player).
// Applied AFTER background so the gradient fills behind the bars.
// systemBarsPadding() reads live WindowInsets, so it automatically
// statusBarsPadding() reads live WindowInsets, so it automatically
// becomes 0 when the player hides the bars.
.then(if (isMobile) Modifier.systemBarsPadding() else Modifier)
.then(if (isMobile && !isPlayerRoute) Modifier.statusBarsPadding() else Modifier)
) {
Box(modifier = Modifier.weight(1f)) {
AppNavigation(
Expand Down Expand Up @@ -697,16 +710,28 @@ fun ArflixApp(
onExitApp = onExitApp
)
}
if (showBottomBar) {

if (isMobile && !isPlayerRoute) {
val bottomBarAlpha by androidx.compose.animation.core.animateFloatAsState(
targetValue = if (showBottomBar) 1f else 0f,
animationSpec = androidx.compose.animation.core.tween(250),
label = "bottom_bar_alpha"
)
AppBottomBar(
currentRoute = currentRoute,
onNavigate = { route ->
navController.navigate(route) {
popUpTo("home") { inclusive = false }
launchSingleTop = true
if (showBottomBar) {
navController.navigate(route) {
popUpTo("home") { inclusive = false }
launchSingleTop = true
}
}
},
modifier = Modifier.fillMaxWidth()
modifier = Modifier
.fillMaxWidth()
.graphicsLayer {
alpha = bottomBarAlpha
}
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ object SportsAddonCapabilities {
value.startsWith(SPORTS_EVENT_STATUS_PREFIX)
}

fun isSportsLockedStatus(status: String?): Boolean =
status?.startsWith(SPORTS_LOCKED_STATUS_PREFIX) == true

fun isSportsCategoryStatus(status: String?): Boolean =
status?.startsWith(SPORTS_STATUS_PREFIX) == true

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ data class MdbExternalRating(
val value: String
)

data class MdbWatchedSnapshot(
val movies: Set<Int>,
val episodes: Set<String>
)

@Singleton
class MdbListRepository @Inject constructor(
private val api: MdbListApi,
Expand Down Expand Up @@ -351,6 +356,37 @@ class MdbListRepository @Inject constructor(

// ===== Watched reads =====

suspend fun getWatchedSnapshot(): Result<MdbWatchedSnapshot> = withContext(Dispatchers.IO) {
val k = key() ?: return@withContext Result.failure(IllegalStateException("MDBList is not connected"))
try {
val movies = mutableSetOf<Int>()
val episodes = mutableSetOf<String>()
var offset = 0
val limit = 1000
while (true) {
val response = api.getWatched(k, limit = limit, offset = offset)
response.movies?.forEach { row ->
row.movie?.ids?.tmdb?.let(movies::add)
}
response.episodes?.forEach { row ->
val episode = row.episode ?: return@forEach
val showTmdb = episode.show?.ids?.tmdb ?: return@forEach
val season = episode.season ?: return@forEach
val number = episode.number ?: return@forEach
episodes.add("show_tmdb:$showTmdb:$season:$number")
}
if (response.pagination?.hasMore != true) break
offset += limit
}
Result.success(MdbWatchedSnapshot(movies, episodes))
} catch (e: kotlinx.coroutines.CancellationException) {
throw e
} catch (e: Exception) {
AppLogger.e(TAG, "watched snapshot fetch failed", e)
Result.failure(e)
}
}

suspend fun getWatchedMovies(): Set<Int> = withContext(Dispatchers.IO) {
val k = key() ?: return@withContext emptySet()
try {
Expand Down
Loading
Loading