diff --git a/app/src/main/java/com/github/damontecres/dolphin/preferences/AppPreference.kt b/app/src/main/java/com/github/damontecres/dolphin/preferences/AppPreference.kt index ba065c6d..194fc0af 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/preferences/AppPreference.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/preferences/AppPreference.kt @@ -200,6 +200,38 @@ sealed interface AppPreference { summaryOff = R.string.hide, ) + val AutoPlayNextUp = + AppSwitchPreference( + title = R.string.auto_play_next, + defaultValue = true, + getter = { it.playbackPreferences.autoPlayNext }, + setter = { prefs, value -> + prefs.updatePlaybackPreferences { autoPlayNext = value } + }, + summaryOn = R.string.enabled, + summaryOff = R.string.disabled, + ) + + val AutoPlayNextDelay = + AppSliderPreference( + title = R.string.auto_play_next_delay, + defaultValue = 15, + min = 0, + max = 60, + interval = 5, + getter = { it.playbackPreferences.autoPlayNextDelaySeconds }, + setter = { prefs, value -> + prefs.updatePlaybackPreferences { autoPlayNextDelaySeconds = value } + }, + summarizer = { value -> + if (value == 0L) { + "Immediate" + } else { + "$value seconds" + } + }, + ) + val InstalledVersion = AppClickablePreference( title = R.string.installed_version, @@ -256,6 +288,8 @@ val basicPreferences = AppPreference.SkipForward, AppPreference.SkipBack, AppPreference.ControllerTimeout, + AppPreference.AutoPlayNextUp, + AppPreference.AutoPlayNextDelay, AppPreference.PlaybackDebugInfo, ), ), diff --git a/app/src/main/java/com/github/damontecres/dolphin/preferences/AppPreferencesSerializer.kt b/app/src/main/java/com/github/damontecres/dolphin/preferences/AppPreferencesSerializer.kt index 524b9651..850bafc9 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/preferences/AppPreferencesSerializer.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/preferences/AppPreferencesSerializer.kt @@ -26,6 +26,9 @@ class AppPreferencesSerializer controllerTimeoutMs = AppPreference.ControllerTimeout.defaultValue seekBarSteps = AppPreference.SeekBarSteps.defaultValue.toInt() showDebugInfo = AppPreference.PlaybackDebugInfo.defaultValue + autoPlayNext = AppPreference.AutoPlayNextUp.defaultValue + autoPlayNextDelaySeconds = + AppPreference.AutoPlayNextDelay.defaultValue }.build() homePagePreferences = HomePagePreferences diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/playback/NextUpEpisode.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/playback/NextUpEpisode.kt index a50ac433..0178e517 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/playback/NextUpEpisode.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/playback/NextUpEpisode.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.PlayArrow @@ -22,17 +23,22 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.tv.material3.Card import androidx.tv.material3.Icon import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import androidx.tv.material3.surfaceColorAtElevation +import coil3.compose.AsyncImage +import com.github.damontecres.dolphin.ui.AppColors import com.github.damontecres.dolphin.ui.PreviewTvSpec -import com.github.damontecres.dolphin.ui.cards.BannerCard import com.github.damontecres.dolphin.ui.theme.DolphinTheme import com.github.damontecres.dolphin.ui.tryRequestFocus +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds @Composable fun NextUpEpisode( @@ -40,6 +46,7 @@ fun NextUpEpisode( description: String?, imageUrl: String?, onClick: () -> Unit, + timeLeft: Duration?, modifier: Modifier = Modifier, aspectRatio: Float = 16f / 9, ) { @@ -67,28 +74,16 @@ fun NextUpEpisode( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(8.dp), ) { - Box { - BannerCard( - imageUrl = imageUrl, - onClick = onClick, - onLongClick = {}, - cardHeight = 100.dp, - aspectRatio = aspectRatio, - modifier = - Modifier - .focusRequester(focusRequester) - .align(Alignment.Center), - ) - Icon( - imageVector = Icons.Default.PlayArrow, - contentDescription = null, - tint = MaterialTheme.colorScheme.border, - modifier = - Modifier - .size(60.dp) - .align(Alignment.Center), - ) - } + NextUpCard( + imageUrl = imageUrl, + onClick = onClick, + timeLeft = timeLeft, + modifier = + Modifier + .fillMaxWidth(.4f) + .fillMaxHeight() + .focusRequester(focusRequester), + ) Column( verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxHeight(), @@ -114,6 +109,56 @@ fun NextUpEpisode( } } +@Composable +fun NextUpCard( + imageUrl: String?, + onClick: () -> Unit, + timeLeft: Duration?, + modifier: Modifier = Modifier, +) { + Card( + modifier = modifier, + onClick = onClick, + ) { + Box(modifier = Modifier.fillMaxSize()) { + AsyncImage( + model = imageUrl, + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.fillMaxSize(), + ) + if (timeLeft != null && timeLeft > Duration.ZERO) { + Box( + modifier = + Modifier + .align(Alignment.Center) + .background( + AppColors.TransparentBlack50, + shape = CircleShape, + ), + ) { + Text( + text = timeLeft.toString(), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(8.dp), + ) + } + } else { + Icon( + imageVector = Icons.Default.PlayArrow, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + modifier = + Modifier + .size(60.dp) + .align(Alignment.Center), + ) + } + } + } +} + @PreviewTvSpec @Composable fun NextUpEpisodePreview() { @@ -124,6 +169,7 @@ fun NextUpEpisodePreview() { imageUrl = "", onClick = {}, aspectRatio = 4f / 3, + timeLeft = 30.seconds, modifier = Modifier .padding(16.dp) diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackContent.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackContent.kt index b6561b8d..acb67742 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackContent.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackContent.kt @@ -62,8 +62,10 @@ import com.github.damontecres.dolphin.ui.components.LoadingPage import com.github.damontecres.dolphin.ui.nav.Destination import com.github.damontecres.dolphin.ui.nav.NavigationManager import com.github.damontecres.dolphin.ui.tryRequestFocus +import kotlinx.coroutines.delay import org.jellyfin.sdk.model.api.DeviceProfile import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds @OptIn(UnstableApi::class) @Composable @@ -322,12 +324,44 @@ fun PlaybackContent( .align(Alignment.BottomCenter), ) { nextUp?.let { + var autoPlayEnabled by + remember { + mutableStateOf( + preferences.appPreferences.playbackPreferences.autoPlayNext, + ) + } + var timeLeft by remember { + mutableLongStateOf( + preferences.appPreferences.playbackPreferences.autoPlayNextDelaySeconds, + ) + } + // TODO need extra back press for some reason + BackHandler(timeLeft > 0 && autoPlayEnabled) { + timeLeft = -1 + autoPlayEnabled = false + } + if (autoPlayEnabled) { + LaunchedEffect(Unit) { + if (timeLeft == 0L) { + viewModel.playUpNextEpisode() + } else { + while (timeLeft > 0) { + delay(1.seconds) + timeLeft-- + } + if (timeLeft == 0L && autoPlayEnabled) { + viewModel.playUpNextEpisode() + } + } + } + } NextUpEpisode( title = it.name, description = it.data.overview, imageUrl = it.imageUrl, aspectRatio = it.data.primaryImageAspectRatio?.toFloat() ?: (16f / 9), onClick = { viewModel.playUpNextEpisode() }, + timeLeft = if (autoPlayEnabled) timeLeft.seconds else null, modifier = Modifier .padding(8.dp) diff --git a/app/src/main/proto/DolphinDataStore.proto b/app/src/main/proto/DolphinDataStore.proto index 854ad834..30e000ba 100644 --- a/app/src/main/proto/DolphinDataStore.proto +++ b/app/src/main/proto/DolphinDataStore.proto @@ -9,6 +9,8 @@ message PlaybackPreferences { int64 controller_timeout_ms = 3; int32 seek_bar_steps = 4; bool show_debug_info = 5; + bool auto_play_next = 6; + int64 auto_play_next_delay_seconds = 7; } message HomePagePreferences{ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3d921620..5485eeea 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -45,4 +45,6 @@ Play theme music License information Show playback debug info + Delay before playing next up + Auto play next up