From 67efc26bab980fe38158bc224e12b9b3c127ac02 Mon Sep 17 00:00:00 2001 From: rhenwinch Date: Sun, 6 Oct 2024 13:35:39 +0800 Subject: [PATCH] feat(player-ui): update provider link loading state dialog --- .../core/ui/player/BasePlayerViewModel.kt | 4 +- .../feature/mobile/player/PlayerScreen.kt | 79 +++++--- .../player/controls/common/BasePopupScreen.kt | 49 +++++ .../provider/ProviderResourceStateScreen.kt | 174 ++++++++++++++++++ .../controls/episodes/EpisodesScreen.kt | 111 +++++------ .../feature/tv/player/PlayerScreen.kt | 2 +- 6 files changed, 321 insertions(+), 98 deletions(-) create mode 100644 feature/mobile/player/src/main/java/com/flixclusive/feature/mobile/player/controls/common/BasePopupScreen.kt create mode 100644 feature/mobile/player/src/main/java/com/flixclusive/feature/mobile/player/controls/dialogs/provider/ProviderResourceStateScreen.kt diff --git a/core/ui/player/src/main/java/com/flixclusive/core/ui/player/BasePlayerViewModel.kt b/core/ui/player/src/main/java/com/flixclusive/core/ui/player/BasePlayerViewModel.kt index 4f923b99..189ea41c 100644 --- a/core/ui/player/src/main/java/com/flixclusive/core/ui/player/BasePlayerViewModel.kt +++ b/core/ui/player/src/main/java/com/flixclusive/core/ui/player/BasePlayerViewModel.kt @@ -430,7 +430,7 @@ abstract class BasePlayerViewModel( } } - loadSourceData(episode) + loadMediaLinks(episode) } } @@ -439,7 +439,7 @@ abstract class BasePlayerViewModel( * * @param episodeToWatch an optional parameter for the episode to watch if film to be watched is a [TvShow] */ - fun loadSourceData( + fun loadMediaLinks( episodeToWatch: Episode? = null ) { if (loadLinksFromNewProviderJob?.isActive == true || loadLinksJob?.isActive == true) { diff --git a/feature/mobile/player/src/main/java/com/flixclusive/feature/mobile/player/PlayerScreen.kt b/feature/mobile/player/src/main/java/com/flixclusive/feature/mobile/player/PlayerScreen.kt index ec0335e7..dceb60c0 100644 --- a/feature/mobile/player/src/main/java/com/flixclusive/feature/mobile/player/PlayerScreen.kt +++ b/feature/mobile/player/src/main/java/com/flixclusive/feature/mobile/player/PlayerScreen.kt @@ -6,7 +6,10 @@ import android.os.Build.VERSION.SDK_INT import android.view.KeyEvent import androidx.activity.ComponentActivity import androidx.annotation.OptIn +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -37,7 +40,6 @@ import androidx.media3.common.util.UnstableApi import com.flixclusive.core.ui.common.navigation.navigator.PlayerScreenNavigator import com.flixclusive.core.ui.common.util.noIndicationClickable import com.flixclusive.core.ui.mobile.ListenKeyEvents -import com.flixclusive.core.ui.mobile.component.provider.ProviderResourceStateDialog import com.flixclusive.core.ui.mobile.rememberPipMode import com.flixclusive.core.ui.mobile.util.toggleSystemBars import com.flixclusive.core.ui.player.PLAYER_CONTROL_VISIBILITY_TIMEOUT @@ -55,6 +57,7 @@ import com.flixclusive.core.ui.player.util.updatePiPParams import com.flixclusive.core.util.android.getActivity import com.flixclusive.domain.provider.CachedLinks import com.flixclusive.feature.mobile.player.controls.PlayerControls +import com.flixclusive.feature.mobile.player.controls.dialogs.provider.ProviderResourceStateScreen import com.flixclusive.feature.mobile.player.util.BrightnessManager import com.flixclusive.feature.mobile.player.util.LocalBrightnessManager import com.flixclusive.feature.mobile.player.util.PlayerPipReceiver @@ -82,12 +85,12 @@ internal fun PlayerScreen( val brightnessManager = remember { BrightnessManager(context) } val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val dialogState by viewModel.dialogState.collectAsStateWithLifecycle() + val providerState by viewModel.dialogState.collectAsStateWithLifecycle() val appSettings by viewModel.appSettings.collectAsStateWithLifecycle() val watchHistoryItem by viewModel.watchHistoryItem.collectAsStateWithLifecycle() - val sourceData = viewModel.cachedLinks + val mediaData = viewModel.cachedLinks val providers by viewModel.providers.collectAsStateWithLifecycle(initialValue = emptyList()) val seasonData by viewModel.season.collectAsStateWithLifecycle() val currentSelectedEpisode by viewModel.currentSelectedEpisode.collectAsStateWithLifecycle() @@ -158,10 +161,10 @@ internal fun PlayerScreen( * * */ LaunchedEffect(Unit) { - if (sourceData.watchId.isEmpty() || sourceData.providerName.isEmpty()) { + if (mediaData.watchId.isEmpty() || mediaData.providerName.isEmpty()) { when(args.film) { is TvShow -> onEpisodeClick(args.episodeToPlay) - is Movie -> viewModel.loadSourceData() + is Movie -> viewModel.loadMediaLinks() else -> throw IllegalStateException("Invalid film instance [${args.film.filmType}]: ${args.film}") } } @@ -283,8 +286,8 @@ internal fun PlayerScreen( ObserveNewLinksAndSubtitles( selectedSourceLink = uiState.selectedSourceLink, currentPlayerTitle = currentPlayerTitle, - newLinks = sourceData.streams, - newSubtitles = sourceData.subtitles, + newLinks = mediaData.streams, + newSubtitles = mediaData.subtitles, getSavedTimeForCurrentSourceData = { viewModel.getSavedTimeForSourceData(currentSelectedEpisode).first } @@ -346,7 +349,7 @@ internal fun PlayerScreen( = getSavedTimeForSourceData(currentSelectedEpisode) player.initialize() - sourceData.run { + mediaData.run { val getPossibleSourceLink = streams .getOrNull(uiState.selectedSourceLink) ?: streams.getOrNull(0) @@ -389,7 +392,7 @@ internal fun PlayerScreen( isDoubleTapping = isDoubleTapping, isEpisodesSheetOpened = isEpisodesSheetOpened, isAudiosAndSubtitlesDialogOpened = isAudiosAndSubtitlesDialogOpened, - servers = sourceData.streams, + servers = mediaData.streams, isPlayerSettingsDialogOpened = isPlayerSettingsDialogOpened, isServersDialogOpened = isServersDialogOpened, watchHistoryItem = watchHistoryItem, @@ -413,7 +416,7 @@ internal fun PlayerScreen( toggleVideoTimeReverse = viewModel::toggleVideoTimeReverse, showControls = { showControls(it) }, lockControls = { viewModel.areControlsLocked = it }, - addSubtitle = { sourceData.subtitles.add(index = 0, element = it) }, + addSubtitle = { mediaData.subtitles.add(index = 0, element = it) }, onEpisodeClick = { viewModel.run { updateWatchHistory( @@ -457,30 +460,46 @@ internal fun PlayerScreen( } } - if (!dialogState.isIdle) { - LaunchedEffect(Unit) { - viewModel.player.run { - if (isPlaying) { - pause() - playWhenReady = true + + AnimatedVisibility( + visible = !providerState.isIdle, + enter = fadeIn(), + exit = fadeOut(), + ) { + with(viewModel) { + DisposableEffect(Unit) { + if (player.isPlaying) { + player.pause() + player.playWhenReady = true + } + + onDispose { + scrapingJob?.cancel() + scrapingJob = null } } - } - ProviderResourceStateDialog( - state = dialogState, - onConsumeDialog = { - scrapingJob?.cancel() - scrapingJob = null - viewModel.onConsumePlayerDialog() + ProviderResourceStateScreen( + state = providerState, + servers = mediaData.streams, + onSkipLoading = { + updateWatchHistory( + currentTime = player.currentPosition, + duration = player.duration + ) - if (viewModel.player.playWhenReady) { - viewModel.player.play() + onEpisodeClick() + }, + onClose = { + scrapingJob?.cancel() + scrapingJob = null + onConsumePlayerDialog() + + if (player.playWhenReady) { + player.play() + } } - } - ) - } else { - scrapingJob?.cancel() - scrapingJob = null + ) + } } } \ No newline at end of file diff --git a/feature/mobile/player/src/main/java/com/flixclusive/feature/mobile/player/controls/common/BasePopupScreen.kt b/feature/mobile/player/src/main/java/com/flixclusive/feature/mobile/player/controls/common/BasePopupScreen.kt new file mode 100644 index 00000000..a94f4222 --- /dev/null +++ b/feature/mobile/player/src/main/java/com/flixclusive/feature/mobile/player/controls/common/BasePopupScreen.kt @@ -0,0 +1,49 @@ +package com.flixclusive.feature.mobile.player.controls.common + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.flixclusive.core.ui.common.util.noIndicationClickable + +@Composable +internal fun BasePopupScreen( + modifier: Modifier = Modifier, + onDismiss: () -> Unit, + content: @Composable ColumnScope.() -> Unit +) { + BackHandler { + onDismiss() + } + + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(0.9F)) + .then(modifier) + ) { + // Block touches + Box( + modifier = Modifier + .fillMaxSize() + .noIndicationClickable { } + ) + + Column( + modifier = Modifier + .fillMaxSize() + .align(Alignment.TopStart), + verticalArrangement = Arrangement.spacedBy(15.dp), + ) { + content() + } + } +} \ No newline at end of file diff --git a/feature/mobile/player/src/main/java/com/flixclusive/feature/mobile/player/controls/dialogs/provider/ProviderResourceStateScreen.kt b/feature/mobile/player/src/main/java/com/flixclusive/feature/mobile/player/controls/dialogs/provider/ProviderResourceStateScreen.kt new file mode 100644 index 00000000..311025b5 --- /dev/null +++ b/feature/mobile/player/src/main/java/com/flixclusive/feature/mobile/player/controls/dialogs/provider/ProviderResourceStateScreen.kt @@ -0,0 +1,174 @@ +package com.flixclusive.feature.mobile.player.controls.dialogs.provider + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.SizeTransform +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +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.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.flixclusive.core.locale.R +import com.flixclusive.core.theme.FlixclusiveTheme +import com.flixclusive.core.ui.common.GradientLinearProgressIndicator +import com.flixclusive.core.ui.common.provider.MediaLinkResourceState +import com.flixclusive.feature.mobile.player.controls.common.BasePopupScreen +import com.flixclusive.feature.mobile.player.controls.common.EnlargedTouchableButton +import com.flixclusive.model.provider.link.Stream + +@Composable +internal fun ProviderResourceStateScreen( + modifier: Modifier = Modifier, + state: MediaLinkResourceState, + servers: List, + onSkipLoading: () -> Unit, + onClose: () -> Unit, +) { + val canSkipLoading by remember { + derivedStateOf { + state.isLoading && servers.isNotEmpty() + } + } + + LaunchedEffect(key1 = state) { + if (state is MediaLinkResourceState.Success) { + onClose() + } + } + + BasePopupScreen( + modifier = modifier, + onDismiss = onClose, + ) { + Box( + modifier = Modifier + .fillMaxSize(), + ) { + Box( + modifier = Modifier + .padding(start = 10.dp, top = 10.dp), + contentAlignment = Alignment.CenterStart + ) { + EnlargedTouchableButton( + iconId = com.flixclusive.core.ui.common.R.drawable.round_close_24, + contentDescription = stringResource(id = R.string.close_label), + size = 45.dp, + onClick = onClose + ) + } + + ProgressHeader( + state = state, + canSkipLoading = canSkipLoading, + onSkipLoading = onSkipLoading, + modifier = Modifier + .align(Alignment.Center) + ) + } + } +} + +@Composable +private fun ProgressHeader( + modifier: Modifier = Modifier, + state: MediaLinkResourceState, + canSkipLoading: Boolean, + onSkipLoading: () -> Unit, +) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + ) { + AnimatedContent( + targetState = state, + label = "", + transitionSpec = { + if (targetState > initialState) { + fadeIn() + slideInHorizontally { it } togetherWith + fadeOut() + slideOutHorizontally { -it } + } else { + fadeIn() + slideInHorizontally { -it } + fadeIn() togetherWith + fadeOut() + slideOutHorizontally { it } + }.using( + SizeTransform(clip = false) + ) + }, + ) { + Text( + text = it.message.asString().trim(), + style = MaterialTheme.typography.labelLarge.copy( + fontSize = 16.sp + ) + ) + } + + AnimatedVisibility( + visible = state.isLoading, + enter = fadeIn(), + exit = scaleOut() + fadeOut() + ) { + GradientLinearProgressIndicator( + modifier = Modifier.fillMaxWidth(0.3F), + colors = listOf( + MaterialTheme.colorScheme.primary, + MaterialTheme.colorScheme.tertiary, + ) + ) + } + + if (canSkipLoading) { + TextButton( + onClick = onSkipLoading, + shape = MaterialTheme.shapes.extraSmall, + contentPadding = PaddingValues(horizontal = 16.dp), + modifier = Modifier + .height(30.dp) + ) { + Text( + text = stringResource(id = R.string.skip_loading_message), + style = MaterialTheme.typography.labelMedium + ) + } + } + } +} + +@Preview(device = "spec:parent=pixel_5,orientation=landscape") +@Composable +private fun ProviderResourceStateScreenPreview() { + FlixclusiveTheme { + Surface { + ProviderResourceStateScreen( + state = MediaLinkResourceState.Fetching(), + onClose = {}, + servers = emptyList(), + onSkipLoading = {} + ) + } + } +} \ No newline at end of file diff --git a/feature/mobile/player/src/main/java/com/flixclusive/feature/mobile/player/controls/episodes/EpisodesScreen.kt b/feature/mobile/player/src/main/java/com/flixclusive/feature/mobile/player/controls/episodes/EpisodesScreen.kt index c5a34768..3b769800 100644 --- a/feature/mobile/player/src/main/java/com/flixclusive/feature/mobile/player/controls/episodes/EpisodesScreen.kt +++ b/feature/mobile/player/src/main/java/com/flixclusive/feature/mobile/player/controls/episodes/EpisodesScreen.kt @@ -1,10 +1,7 @@ package com.flixclusive.feature.mobile.player.controls.episodes import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -18,7 +15,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -26,17 +22,17 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.flixclusive.core.theme.FlixclusiveTheme -import com.flixclusive.core.ui.common.util.noIndicationClickable import com.flixclusive.core.network.util.Resource +import com.flixclusive.core.theme.FlixclusiveTheme +import com.flixclusive.feature.mobile.player.controls.common.BasePopupScreen import com.flixclusive.feature.mobile.player.controls.common.EnlargedTouchableButton import com.flixclusive.feature.mobile.player.controls.episodes.composables.EpisodesRow import com.flixclusive.feature.mobile.player.controls.episodes.composables.SeasonsRow import com.flixclusive.model.database.WatchHistoryItem import com.flixclusive.model.film.common.tv.Episode import com.flixclusive.model.film.common.tv.Season -import com.flixclusive.core.ui.common.R as UiCommonR import com.flixclusive.core.locale.R as LocaleR +import com.flixclusive.core.ui.common.R as UiCommonR @Composable internal fun EpisodesScreen( @@ -53,72 +49,57 @@ internal fun EpisodesScreen( mutableIntStateOf(seasonData.data?.number ?: currentEpisodeSelected.season) } - Box( - modifier = modifier - .fillMaxSize() - .background(Color.Black.copy(0.9F)) + BasePopupScreen( + modifier = modifier, + onDismiss = onClose ) { - // Block touches - Box( - modifier = Modifier - .fillMaxSize() - .noIndicationClickable { } - ) - - Column( + Row( modifier = Modifier - .fillMaxSize() - .align(Alignment.TopStart), - verticalArrangement = Arrangement.spacedBy(15.dp), + .padding(start = 10.dp, top = 10.dp), + verticalAlignment = Alignment.CenterVertically ) { - Row( - modifier = Modifier - .padding(start = 10.dp, top = 10.dp), - verticalAlignment = Alignment.CenterVertically - ) { - EnlargedTouchableButton( - iconId = UiCommonR.drawable.round_close_24, - contentDescription = stringResource(id = LocaleR.string.close_label), - size = 45.dp, - onClick = onClose - ) + EnlargedTouchableButton( + iconId = UiCommonR.drawable.round_close_24, + contentDescription = stringResource(id = LocaleR.string.close_label), + size = 45.dp, + onClick = onClose + ) - when (seasonData) { - is Resource.Failure -> Unit - Resource.Loading -> Unit - is Resource.Success -> { - Text( - text = seasonData.data?.name ?: "Season ${seasonData.data?.number}", - style = MaterialTheme.typography.headlineSmall.copy( - fontWeight = FontWeight.Bold, - fontSize = 22.sp - ), - modifier = Modifier - .padding(start = 8.dp) - ) - } + when (seasonData) { + is Resource.Failure -> Unit + Resource.Loading -> Unit + is Resource.Success -> { + Text( + text = seasonData.data?.name ?: "Season ${seasonData.data?.number}", + style = MaterialTheme.typography.headlineSmall.copy( + fontWeight = FontWeight.Bold, + fontSize = 22.sp + ), + modifier = Modifier + .padding(start = 8.dp) + ) } } + } - SeasonsRow( - availableSeasons = availableSeasons, - currentSeasonSelected = selectedSeason, - onSeasonChange = { - selectedSeason = it - onSeasonChange(it) - } - ) + SeasonsRow( + availableSeasons = availableSeasons, + currentSeasonSelected = selectedSeason, + onSeasonChange = { + selectedSeason = it + onSeasonChange(it) + } + ) - EpisodesRow( - seasonData = seasonData, - selectedSeason = selectedSeason, - currentEpisodeSelected = currentEpisodeSelected, - watchHistoryItem = watchHistoryItem, - onSeasonChange = onSeasonChange, - onEpisodeClick = onEpisodeClick, - onClose = onClose - ) - } + EpisodesRow( + seasonData = seasonData, + selectedSeason = selectedSeason, + currentEpisodeSelected = currentEpisodeSelected, + watchHistoryItem = watchHistoryItem, + onSeasonChange = onSeasonChange, + onEpisodeClick = onEpisodeClick, + onClose = onClose + ) } } diff --git a/feature/tv/player/src/main/kotlin/com/flixclusive/feature/tv/player/PlayerScreen.kt b/feature/tv/player/src/main/kotlin/com/flixclusive/feature/tv/player/PlayerScreen.kt index 8908234d..a56b9d25 100644 --- a/feature/tv/player/src/main/kotlin/com/flixclusive/feature/tv/player/PlayerScreen.kt +++ b/feature/tv/player/src/main/kotlin/com/flixclusive/feature/tv/player/PlayerScreen.kt @@ -121,7 +121,7 @@ fun PlayerScreen( episodeToWatch = episodeToPlay ) } else { - viewModel.loadSourceData() + viewModel.loadMediaLinks() } }