diff --git a/app/src/main/java/dev/khaled/leanstream/Navigator.kt b/app/src/main/java/dev/khaled/leanstream/Navigator.kt index 654dc14..bebd6d4 100644 --- a/app/src/main/java/dev/khaled/leanstream/Navigator.kt +++ b/app/src/main/java/dev/khaled/leanstream/Navigator.kt @@ -10,9 +10,9 @@ import androidx.navigation.compose.composable 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.ChannelPickerScreen import dev.khaled.leanstream.channels.ChannelViewModel -import dev.khaled.leanstream.player.Player +import dev.khaled.leanstream.player.PlayerScreen import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.cbor.Cbor import kotlinx.serialization.decodeFromHexString @@ -30,7 +30,7 @@ fun Navigator() { ) { composable(route = Route.ChannelPicker.route) { - ChannelPicker(channelViewModel) { channel -> + ChannelPickerScreen(channelViewModel) { channel -> navController.navigateSingleTop(Route.Player.launch(channel)) } } @@ -41,7 +41,7 @@ fun Navigator() { ) { val serializedChannel = it.arguments?.getString("channel") ?: return@composable val channel = Cbor.decodeFromHexString(serializedChannel) - Player(channel = channel, channelViewModel.channels) { + PlayerScreen(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/ChannelPickerScreen.kt similarity index 84% rename from app/src/main/java/dev/khaled/leanstream/channels/ChannelPicker.kt rename to app/src/main/java/dev/khaled/leanstream/channels/ChannelPickerScreen.kt index 877b5cf..2883714 100644 --- a/app/src/main/java/dev/khaled/leanstream/channels/ChannelPicker.kt +++ b/app/src/main/java/dev/khaled/leanstream/channels/ChannelPickerScreen.kt @@ -1,5 +1,8 @@ package dev.khaled.leanstream.channels +import android.app.AlertDialog +import android.content.Context +import android.widget.Toast import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -38,12 +41,13 @@ import dev.khaled.leanstream.channels.item.ChannelsGrid import dev.khaled.leanstream.isRunningOnTV import dev.khaled.leanstream.playSoundEffectOnFocus import dev.khaled.leanstream.ui.Branding +import kotlinx.serialization.ExperimentalSerializationApi + @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ChannelPicker( - viewModel: ChannelViewModel = viewModel(), - openChannel: (channel: Channel) -> Unit +fun ChannelPickerScreen( + viewModel: ChannelViewModel = viewModel(), openChannel: (channel: Channel) -> Unit ) { val context = LocalContext.current val compactAppBar = remember { isRunningOnTV(context) } @@ -106,7 +110,7 @@ fun ChannelPicker( }) IconButton(onClick = { - + resetChannelsListConfirmation(context, viewModel) }, content = { Icon(Icons.Outlined.Settings, null) }) @@ -127,4 +131,14 @@ fun ChannelPicker( openChannel(it) } } +} + +@OptIn(ExperimentalSerializationApi::class) +fun resetChannelsListConfirmation(context: Context, viewModel: ChannelViewModel) { + AlertDialog.Builder(context).setTitle("Reset Channels") + .setMessage("Do you really want to remove imported channels?") + .setPositiveButton("Yes") { _, _ -> + viewModel.savePlaylistToDisk(context, emptyList()); + Toast.makeText(context, "Restart Application To Take Effect", Toast.LENGTH_SHORT).show() + }.setNegativeButton("No", null).show() } \ No newline at end of file diff --git a/app/src/main/java/dev/khaled/leanstream/channels/importer/ImportPlaylistPrompt.kt b/app/src/main/java/dev/khaled/leanstream/channels/importer/ImportPlaylistPrompt.kt index 0af4d2e..a8d5628 100644 --- a/app/src/main/java/dev/khaled/leanstream/channels/importer/ImportPlaylistPrompt.kt +++ b/app/src/main/java/dev/khaled/leanstream/channels/importer/ImportPlaylistPrompt.kt @@ -3,11 +3,12 @@ package dev.khaled.leanstream.channels.importer import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.VideoLibrary -import androidx.compose.material3.Button import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -24,7 +25,7 @@ fun ImportPlaylistPrompt(result: () -> Unit) { var showDialog by remember { mutableStateOf(false) } Column( - horizontalAlignment = Alignment.CenterHorizontally + modifier = Modifier.padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Icon( Icons.Rounded.VideoLibrary, modifier = Modifier.size(64.dp), contentDescription = null @@ -32,7 +33,9 @@ fun ImportPlaylistPrompt(result: () -> Unit) { Spacer(modifier = Modifier.height(8.dp)) Text(text = "No entries found. Import a playlist (.m3u8) below") Spacer(modifier = Modifier.height(8.dp)) - Button(onClick = { showDialog = true }, modifier = Modifier.playSoundEffectOnFocus()) { + OutlinedButton( + onClick = { showDialog = true }, modifier = Modifier.playSoundEffectOnFocus() + ) { Text(text = "Import") } } diff --git a/app/src/main/java/dev/khaled/leanstream/channels/item/ChannelGridItem.kt b/app/src/main/java/dev/khaled/leanstream/channels/item/ChannelGridItem.kt index 3cb8bac..d779fc9 100644 --- a/app/src/main/java/dev/khaled/leanstream/channels/item/ChannelGridItem.kt +++ b/app/src/main/java/dev/khaled/leanstream/channels/item/ChannelGridItem.kt @@ -51,7 +51,7 @@ fun GridItem( val context = LocalContext.current val focusRequester = remember { FocusRequester() } val isTouchScreen = remember { !isRunningOnTV(context) } - val showChannelTitle = isTouchScreen //TODO + val showChannelTitle = isTouchScreen || channel.icon.isNullOrBlank() //TODO Card( onClick = { onClick.invoke(channel) }, diff --git a/app/src/main/java/dev/khaled/leanstream/player/Player.kt b/app/src/main/java/dev/khaled/leanstream/player/PlayerScreen.kt similarity index 69% rename from app/src/main/java/dev/khaled/leanstream/player/Player.kt rename to app/src/main/java/dev/khaled/leanstream/player/PlayerScreen.kt index 719254c..eb2de13 100644 --- a/app/src/main/java/dev/khaled/leanstream/player/Player.kt +++ b/app/src/main/java/dev/khaled/leanstream/player/PlayerScreen.kt @@ -19,7 +19,7 @@ import dev.khaled.leanstream.player.controller.PlayerController @Composable -fun Player(channel: Channel, playlist: List, navigateBack: () -> Unit) { +fun PlayerScreen(channel: Channel, playlist: List, navigateBack: () -> Unit) { val exoPlayer = rememberExoPlayer(LocalContext.current) LaunchedEffect(Unit) { @@ -27,7 +27,7 @@ fun Player(channel: Channel, playlist: List, navigateBack: () -> Unit) exoPlayer.addMediaItem( MediaItem.Builder().setUri(it.url).setMediaMetadata( MediaMetadata.Builder().setDisplayTitle(it.title) - .setArtworkUri(channel.icon.let { i -> Uri.parse(i) }).build() + .setArtworkUri(it.icon?.let { i -> Uri.parse(i) }).build() ).build() ) } @@ -58,24 +58,3 @@ private fun rememberExoPlayer(context: Context) = remember { playWhenReady = true } } - -//private fun Modifier.dPadEvents( -// exoPlayer: ExoPlayer, -// videoPlayerState: VideoPlayerState, -// pulseState: VideoPlayerPulseState -//): Modifier = this.handleDPadKeyEvents( -// onLeft = { -// exoPlayer.seekBack() -// pulseState.setType(BACK) -// }, -// onRight = { -// exoPlayer.seekForward() -// pulseState.setType(FORWARD) -// }, -// onUp = { videoPlayerState.showControls() }, -// onDown = { videoPlayerState.showControls() }, -// onEnter = { -// exoPlayer.pause() -// videoPlayerState.showControls() -// } -//) \ No newline at end of file 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 aff986a..19bb1f6 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 @@ -29,6 +29,7 @@ import androidx.compose.ui.focus.onFocusEvent import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import androidx.media3.common.MediaItem import androidx.media3.common.PlaybackException import androidx.media3.common.Player import androidx.media3.common.Player.STATE_BUFFERING @@ -50,19 +51,20 @@ fun PlayerController( var controllerVisible by remember { mutableStateOf(true) } var isButtonFocused by remember { mutableStateOf(true) } + var mediaMetadata by remember { mutableStateOf(player.mediaMetadata) } + var isPlaying by remember { mutableStateOf(true) } var isBuffering by remember { mutableStateOf(true) } var error by remember { mutableStateOf(null) } val handler = remember { Handler(Looper.getMainLooper()) } - val controllerVisibilityRunnable = - remember { - Runnable { - if (isButtonFocused) return@Runnable - if (controllerVisible) controllerVisible = false - } + val controllerVisibilityRunnable = remember { + Runnable { + if (isButtonFocused) return@Runnable + if (controllerVisible) controllerVisible = false } + } fun triggerHideController() = run { handler.removeCallbacks(controllerVisibilityRunnable) @@ -88,13 +90,17 @@ fun PlayerController( super.onPlayerError(exception) error = exception.message controllerVisible = true + player.prepare() //TODO Auto Retry Toggle + } + + override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { + super.onMediaItemTransition(mediaItem, reason) + mediaItem?.let { mediaMetadata = it.mediaMetadata } } } player.addListener(listener) - onDispose { - player.removeListener(listener) - } + onDispose { player.removeListener(listener) } } @@ -142,23 +148,26 @@ fun PlayerController( .onFocusEvent { isButtonFocused = it.hasFocus if (!it.hasFocus) triggerHideController() - }, - verticalAlignment = Alignment.CenterVertically + }, verticalAlignment = Alignment.CenterVertically ) { if (!isRunningOnTV) ExtraControls(backHandler) Spacer(modifier = Modifier.weight(1f)) - FilledTonalIconButton(onClick = { player.seekToPreviousMediaItem() }) { + FilledTonalIconButton(enabled = player.hasPreviousMediaItem(), + onClick = { player.seekToPreviousMediaItem() }) { Icon(Icons.Rounded.SkipPrevious, contentDescription = null) } - ChannelInfo(player.mediaMetadata) + ChannelInfo(mediaMetadata) - FilledTonalIconButton(onClick = { player.seekToNextMediaItem() }) { + FilledTonalIconButton(enabled = player.hasNextMediaItem(), + onClick = { player.seekToNextMediaItem() }) { Icon(Icons.Rounded.SkipNext, contentDescription = null) } } + + } } }