Fix interactive episode navigation buttons

This commit is contained in:
2026-03-24 21:34:38 +01:00
parent 5137fd9893
commit f85624b159
6 changed files with 82 additions and 12 deletions

View File

@@ -34,6 +34,7 @@ import dagger.hilt.android.AndroidEntryPoint
import hu.bbara.purefin.common.ui.PurefinWaitingScreen import hu.bbara.purefin.common.ui.PurefinWaitingScreen
import hu.bbara.purefin.core.data.client.JellyfinApiClient import hu.bbara.purefin.core.data.client.JellyfinApiClient
import hu.bbara.purefin.core.data.client.JellyfinAuthInterceptor import hu.bbara.purefin.core.data.client.JellyfinAuthInterceptor
import hu.bbara.purefin.core.data.navigation.LocalNavigationBackStack
import hu.bbara.purefin.core.data.navigation.LocalNavigationManager import hu.bbara.purefin.core.data.navigation.LocalNavigationManager
import hu.bbara.purefin.core.data.navigation.NavigationCommand import hu.bbara.purefin.core.data.navigation.NavigationCommand
import hu.bbara.purefin.core.data.navigation.NavigationManager import hu.bbara.purefin.core.data.navigation.NavigationManager
@@ -164,7 +165,10 @@ class TvActivity : ComponentActivity() {
} }
} }
CompositionLocalProvider(LocalNavigationManager provides navigationManager) { CompositionLocalProvider(
LocalNavigationManager provides navigationManager,
LocalNavigationBackStack provides backStack.toList()
) {
NavDisplay( NavDisplay(
backStack = backStack, backStack = backStack,
onBack = { navigationManager.pop() }, onBack = { navigationManager.pop() },

View File

@@ -34,6 +34,7 @@ import dagger.hilt.android.AndroidEntryPoint
import hu.bbara.purefin.common.ui.PurefinWaitingScreen import hu.bbara.purefin.common.ui.PurefinWaitingScreen
import hu.bbara.purefin.core.data.client.JellyfinApiClient import hu.bbara.purefin.core.data.client.JellyfinApiClient
import hu.bbara.purefin.core.data.client.JellyfinAuthInterceptor import hu.bbara.purefin.core.data.client.JellyfinAuthInterceptor
import hu.bbara.purefin.core.data.navigation.LocalNavigationBackStack
import hu.bbara.purefin.core.data.navigation.LocalNavigationManager import hu.bbara.purefin.core.data.navigation.LocalNavigationManager
import hu.bbara.purefin.core.data.navigation.NavigationCommand import hu.bbara.purefin.core.data.navigation.NavigationCommand
import hu.bbara.purefin.core.data.navigation.NavigationManager import hu.bbara.purefin.core.data.navigation.NavigationManager
@@ -165,7 +166,10 @@ class PurefinActivity : ComponentActivity() {
} }
} }
CompositionLocalProvider(LocalNavigationManager provides navigationManager) { CompositionLocalProvider(
LocalNavigationManager provides navigationManager,
LocalNavigationBackStack provides backStack.toList()
) {
NavDisplay( NavDisplay(
backStack = backStack, backStack = backStack,
onBack = { navigationManager.pop() }, onBack = { navigationManager.pop() },

View File

@@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.Add
@@ -49,9 +50,23 @@ import hu.bbara.purefin.core.model.Episode
import hu.bbara.purefin.feature.download.DownloadState import hu.bbara.purefin.feature.download.DownloadState
import hu.bbara.purefin.player.PlayerActivity import hu.bbara.purefin.player.PlayerActivity
internal sealed interface EpisodeTopBarShortcut {
val label: String
val onClick: () -> Unit
data class Series(override val onClick: () -> Unit) : EpisodeTopBarShortcut {
override val label: String = "Series"
}
data class Home(override val onClick: () -> Unit) : EpisodeTopBarShortcut {
override val label: String = "Home"
}
}
@Composable @Composable
internal fun EpisodeTopBar( internal fun EpisodeTopBar(
seriesTitle: String?, seriesTitle: String?,
shortcut: EpisodeTopBarShortcut?,
onBack: () -> Unit, onBack: () -> Unit,
onSeriesClick: () -> Unit, onSeriesClick: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
@@ -70,29 +85,50 @@ internal fun EpisodeTopBar(
contentDescription = "Back", contentDescription = "Back",
onClick = onBack onClick = onBack
) )
if (!seriesTitle.isNullOrBlank()) { when {
shortcut != null -> {
Box( Box(
modifier = Modifier modifier = Modifier
.weight(1f) .height(52.dp)
.padding(horizontal = 12.dp), .clickable(onClick = shortcut.onClick),
contentAlignment = Alignment.Center
) {
Text(
text = shortcut.label,
color = scheme.onBackground,
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.clip(CircleShape)
.background(scheme.background.copy(alpha = 0.65f))
.padding(horizontal = 16.dp, vertical = 10.dp)
)
}
}
!seriesTitle.isNullOrBlank() -> {
Box(
modifier = Modifier
.height(52.dp)
.clickable(onClick = onSeriesClick),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(
text = seriesTitle, text = seriesTitle,
color = scheme.onBackground, color = scheme.onBackground,
fontSize = 14.sp, fontSize = 16.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier modifier = Modifier
.clip(RoundedCornerShape(999.dp)) .clip(CircleShape)
.background(scheme.background.copy(alpha = 0.65f)) .background(scheme.background.copy(alpha = 0.65f))
.clickable(onClick = onSeriesClick)
.padding(horizontal = 16.dp, vertical = 10.dp) .padding(horizontal = 16.dp, vertical = 10.dp)
) )
} }
} else { }
Spacer(modifier = Modifier.weight(1f)) else -> Spacer(modifier = Modifier.weight(1f))
} }
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
GhostIconButton(icon = Icons.Outlined.Cast, contentDescription = "Cast", onClick = { }) GhostIconButton(icon = Icons.Outlined.Cast, contentDescription = "Cast", onClick = { })

View File

@@ -15,12 +15,16 @@ import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.common.ui.PurefinWaitingScreen import hu.bbara.purefin.common.ui.PurefinWaitingScreen
import hu.bbara.purefin.common.ui.components.MediaHero import hu.bbara.purefin.common.ui.components.MediaHero
import hu.bbara.purefin.core.data.navigation.EpisodeDto import hu.bbara.purefin.core.data.navigation.EpisodeDto
import hu.bbara.purefin.core.data.navigation.LocalNavigationBackStack
import hu.bbara.purefin.core.data.navigation.LocalNavigationManager
import hu.bbara.purefin.core.data.navigation.Route
import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Episode
import hu.bbara.purefin.feature.download.DownloadState import hu.bbara.purefin.feature.download.DownloadState
import hu.bbara.purefin.feature.shared.content.episode.EpisodeScreenViewModel import hu.bbara.purefin.feature.shared.content.episode.EpisodeScreenViewModel
@@ -31,6 +35,9 @@ fun EpisodeScreen(
viewModel: EpisodeScreenViewModel = hiltViewModel(), viewModel: EpisodeScreenViewModel = hiltViewModel(),
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val navigationManager = LocalNavigationManager.current
val backStack = LocalNavigationBackStack.current
val previousRoute = remember(backStack) { backStack.getOrNull(backStack.lastIndex - 1) }
LaunchedEffect(episode) { LaunchedEffect(episode) {
viewModel.selectEpisode( viewModel.selectEpisode(
@@ -69,6 +76,15 @@ fun EpisodeScreen(
EpisodeScreenInternal( EpisodeScreenInternal(
episode = episode.value!!, episode = episode.value!!,
seriesTitle = seriesTitle.value, seriesTitle = seriesTitle.value,
topBarShortcut = remember(previousRoute) {
when (previousRoute) {
is Route.SeriesRoute -> EpisodeTopBarShortcut.Home {
navigationManager.replaceAll(Route.Home)
}
Route.Home -> EpisodeTopBarShortcut.Series(viewModel::onSeriesClick)
else -> null
}
},
downloadState = downloadState.value, downloadState = downloadState.value,
onBack = viewModel::onBack, onBack = viewModel::onBack,
onSeriesClick = viewModel::onSeriesClick, onSeriesClick = viewModel::onSeriesClick,
@@ -81,6 +97,7 @@ fun EpisodeScreen(
private fun EpisodeScreenInternal( private fun EpisodeScreenInternal(
episode: Episode, episode: Episode,
seriesTitle: String?, seriesTitle: String?,
topBarShortcut: EpisodeTopBarShortcut?,
downloadState: DownloadState, downloadState: DownloadState,
onBack: () -> Unit, onBack: () -> Unit,
onSeriesClick: () -> Unit, onSeriesClick: () -> Unit,
@@ -94,6 +111,7 @@ private fun EpisodeScreenInternal(
topBar = { topBar = {
EpisodeTopBar( EpisodeTopBar(
seriesTitle = seriesTitle, seriesTitle = seriesTitle,
shortcut = topBarShortcut,
onBack = onBack, onBack = onBack,
onSeriesClick = onSeriesClick, onSeriesClick = onSeriesClick,
modifier = Modifier modifier = Modifier
@@ -123,3 +141,5 @@ private fun EpisodeScreenInternal(
} }
} }
} }

View File

@@ -1,6 +1,7 @@
package hu.bbara.purefin.core.data.navigation package hu.bbara.purefin.core.data.navigation
import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.runtime.staticCompositionLocalOf
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@@ -43,3 +44,5 @@ class DefaultNavigationManager : NavigationManager {
val LocalNavigationManager: ProvidableCompositionLocal<NavigationManager> = val LocalNavigationManager: ProvidableCompositionLocal<NavigationManager> =
staticCompositionLocalOf { error("NavigationManager not provided") } staticCompositionLocalOf { error("NavigationManager not provided") }
val LocalNavigationBackStack = compositionLocalOf<List<Route>> { emptyList() }

View File

@@ -5,6 +5,8 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import hu.bbara.purefin.core.data.AppContentRepository import hu.bbara.purefin.core.data.AppContentRepository
import hu.bbara.purefin.core.data.navigation.NavigationManager import hu.bbara.purefin.core.data.navigation.NavigationManager
import hu.bbara.purefin.core.data.navigation.Route
import hu.bbara.purefin.core.data.navigation.SeriesDto
import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Episode
import hu.bbara.purefin.feature.download.DownloadState import hu.bbara.purefin.feature.download.DownloadState
import hu.bbara.purefin.feature.download.MediaDownloadManager import hu.bbara.purefin.feature.download.MediaDownloadManager
@@ -50,7 +52,8 @@ class EpisodeScreenViewModel @Inject constructor(
} }
fun onSeriesClick() { fun onSeriesClick() {
navigationManager.pop() val seriesId = _seriesId.value ?: return
navigationManager.navigate(Route.SeriesRoute(SeriesDto(id = seriesId)))
} }
fun selectEpisode(seriesId: UUID, seasonId: UUID, episodeId: UUID) { fun selectEpisode(seriesId: UUID, seasonId: UUID, episodeId: UUID) {