diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e6331ee..bff2f03 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -86,21 +86,20 @@ android { dependencies { implementation("androidx.core:core-ktx:1.12.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:1.6.3") + + implementation("androidx.core:core-splashscreen:1.0.1") + implementation(platform("androidx.compose:compose-bom:2024.02.02")) - implementation("androidx.compose.ui:ui") - implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.material3:material3") implementation("androidx.tv:tv-material:1.0.0-alpha10") - implementation("androidx.core:core-splashscreen:1.0.1") implementation("io.coil-kt:coil-compose:2.6.0") implementation("androidx.navigation:navigation-compose:2.7.7") implementation("androidx.compose.material:material-icons-extended:1.6.3") - - implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:1.6.3") - + implementation("androidx.media3:media3-exoplayer:1.3.0") implementation("androidx.media3:media3-exoplayer-hls:1.3.0") implementation("androidx.media3:media3-exoplayer-dash:1.3.0") diff --git a/app/src/main/java/dev/khaled/leanstream/Navigator.kt b/app/src/main/java/dev/khaled/leanstream/Navigator.kt index 64111ad..654dc14 100644 --- a/app/src/main/java/dev/khaled/leanstream/Navigator.kt +++ b/app/src/main/java/dev/khaled/leanstream/Navigator.kt @@ -1,6 +1,7 @@ package dev.khaled.leanstream import androidx.compose.runtime.Composable +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController import androidx.navigation.NavType @@ -10,17 +11,18 @@ import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import dev.khaled.leanstream.channels.Channel import dev.khaled.leanstream.channels.ChannelPicker +import dev.khaled.leanstream.channels.ChannelViewModel import dev.khaled.leanstream.player.Player import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.cbor.Cbor import kotlinx.serialization.decodeFromHexString import kotlinx.serialization.encodeToHexString - @OptIn(ExperimentalSerializationApi::class) @Composable fun Navigator() { val navController = rememberNavController() + val channelViewModel: ChannelViewModel = viewModel() NavHost( navController = navController, @@ -28,16 +30,18 @@ fun Navigator() { ) { composable(route = Route.ChannelPicker.route) { - ChannelPicker { channel -> + ChannelPicker(channelViewModel) { channel -> navController.navigateSingleTop(Route.Player.launch(channel)) } } - composable(route = "${Route.Player.route}/{channel}", - arguments = listOf(navArgument("channel") { type = NavType.StringType })) { + composable( + route = "${Route.Player.route}/{channel}", + arguments = listOf(navArgument("channel") { type = NavType.StringType }) + ) { val serializedChannel = it.arguments?.getString("channel") ?: return@composable val channel = Cbor.decodeFromHexString(serializedChannel) - Player(channel = channel) { + Player(channel = channel, channelViewModel.channels) { navController.popBackStack() } } diff --git a/app/src/main/java/dev/khaled/leanstream/channels/ChannelPicker.kt b/app/src/main/java/dev/khaled/leanstream/channels/ChannelPicker.kt index 02c7231..877b5cf 100644 --- a/app/src/main/java/dev/khaled/leanstream/channels/ChannelPicker.kt +++ b/app/src/main/java/dev/khaled/leanstream/channels/ChannelPicker.kt @@ -1,7 +1,6 @@ package dev.khaled.leanstream.channels import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -71,7 +70,6 @@ fun ChannelPicker( var searchToggled by rememberSaveable { mutableStateOf(false) } - val interactionSource = remember { MutableInteractionSource() } Column { diff --git a/app/src/main/java/dev/khaled/leanstream/player/Player.kt b/app/src/main/java/dev/khaled/leanstream/player/Player.kt index 699d705..719254c 100644 --- a/app/src/main/java/dev/khaled/leanstream/player/Player.kt +++ b/app/src/main/java/dev/khaled/leanstream/player/Player.kt @@ -1,6 +1,7 @@ package dev.khaled.leanstream.player import android.content.Context +import android.net.Uri import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable @@ -10,6 +11,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.viewinterop.AndroidView import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata import androidx.media3.exoplayer.ExoPlayer import androidx.media3.ui.PlayerView import dev.khaled.leanstream.channels.Channel @@ -17,12 +19,20 @@ import dev.khaled.leanstream.player.controller.PlayerController @Composable -fun Player(channel: Channel, navigateBack: () -> Unit) { +fun Player(channel: Channel, playlist: List, navigateBack: () -> Unit) { val exoPlayer = rememberExoPlayer(LocalContext.current) - val mediaSource = remember { MediaItem.fromUri(channel.url) } LaunchedEffect(Unit) { - exoPlayer.setMediaItem(mediaSource) + playlist.forEach { + exoPlayer.addMediaItem( + MediaItem.Builder().setUri(it.url).setMediaMetadata( + MediaMetadata.Builder().setDisplayTitle(it.title) + .setArtworkUri(channel.icon.let { i -> Uri.parse(i) }).build() + ).build() + ) + } + + exoPlayer.seekTo(playlist.indexOf(channel), 0) exoPlayer.prepare() } @@ -37,10 +47,7 @@ fun Player(channel: Channel, navigateBack: () -> Unit) { PlayerController( - modifier = Modifier.fillMaxSize(), - channel = channel, - player = exoPlayer, - backHandler = navigateBack + modifier = Modifier.fillMaxSize(), player = exoPlayer, backHandler = navigateBack ) } diff --git a/app/src/main/java/dev/khaled/leanstream/player/controller/ChannelInfo.kt b/app/src/main/java/dev/khaled/leanstream/player/controller/ChannelInfo.kt index b3d4b9f..51f50ef 100644 --- a/app/src/main/java/dev/khaled/leanstream/player/controller/ChannelInfo.kt +++ b/app/src/main/java/dev/khaled/leanstream/player/controller/ChannelInfo.kt @@ -19,20 +19,20 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp +import androidx.media3.common.MediaMetadata import coil.compose.AsyncImage -import dev.khaled.leanstream.channels.Channel @Composable -fun ChannelInfo(channel: Channel) { +fun ChannelInfo(metadata: MediaMetadata) { Card { Row(Modifier.padding(8.dp), verticalAlignment = Alignment.CenterVertically) { Text( - text = channel.title ?: channel.url, + text = metadata.displayTitle.toString(), style = MaterialTheme.typography.titleLarge, ) Spacer(modifier = Modifier.width(8.dp)) AsyncImage( - model = channel.icon, + model = metadata.artworkUri, modifier = Modifier .size(48.dp) .clip(CircleShape) diff --git a/app/src/main/java/dev/khaled/leanstream/player/controller/PlayerController.kt b/app/src/main/java/dev/khaled/leanstream/player/controller/PlayerController.kt index f7da271..aff986a 100644 --- a/app/src/main/java/dev/khaled/leanstream/player/controller/PlayerController.kt +++ b/app/src/main/java/dev/khaled/leanstream/player/controller/PlayerController.kt @@ -6,13 +6,17 @@ import android.os.Looper import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.SkipNext +import androidx.compose.material.icons.rounded.SkipPrevious import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.FilledTonalIconButton +import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue @@ -21,6 +25,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusEvent import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp @@ -28,7 +33,6 @@ import androidx.media3.common.PlaybackException import androidx.media3.common.Player import androidx.media3.common.Player.STATE_BUFFERING import androidx.media3.exoplayer.ExoPlayer -import dev.khaled.leanstream.channels.Channel import dev.khaled.leanstream.conditional import dev.khaled.leanstream.isRunningOnTV import dev.khaled.leanstream.ui.ScreenOrientation @@ -36,7 +40,6 @@ import dev.khaled.leanstream.ui.ScreenOrientation @Composable fun PlayerController( modifier: Modifier = Modifier, - channel: Channel, player: ExoPlayer, backHandler: () -> Unit, ) { @@ -45,6 +48,7 @@ fun PlayerController( val isRunningOnTV = isRunningOnTV(LocalContext.current) var controllerVisible by remember { mutableStateOf(true) } + var isButtonFocused by remember { mutableStateOf(true) } var isPlaying by remember { mutableStateOf(true) } var isBuffering by remember { mutableStateOf(true) } @@ -53,7 +57,12 @@ fun PlayerController( val handler = remember { Handler(Looper.getMainLooper()) } val controllerVisibilityRunnable = - remember { Runnable { if (controllerVisible) controllerVisible = false } } + remember { + Runnable { + if (isButtonFocused) return@Runnable + if (controllerVisible) controllerVisible = false + } + } fun triggerHideController() = run { handler.removeCallbacks(controllerVisibilityRunnable) @@ -94,13 +103,10 @@ fun PlayerController( Box(modifier = Modifier .fillMaxSize() .conditional(!controllerVisible) { - clickable( - interactionSource = MutableInteractionSource(), - indication = null, - onClick = { - controllerVisible = true - triggerHideController() - }) + clickable { + controllerVisible = true + triggerHideController() + } }) @@ -133,12 +139,25 @@ fun PlayerController( modifier = Modifier .align(Alignment.BottomStart) .padding(16.dp) + .onFocusEvent { + isButtonFocused = it.hasFocus + if (!it.hasFocus) triggerHideController() + }, + verticalAlignment = Alignment.CenterVertically ) { if (!isRunningOnTV) ExtraControls(backHandler) Spacer(modifier = Modifier.weight(1f)) - ChannelInfo(channel = channel) + FilledTonalIconButton(onClick = { player.seekToPreviousMediaItem() }) { + Icon(Icons.Rounded.SkipPrevious, contentDescription = null) + } + + ChannelInfo(player.mediaMetadata) + + FilledTonalIconButton(onClick = { player.seekToNextMediaItem() }) { + Icon(Icons.Rounded.SkipNext, contentDescription = null) + } } } }