mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
feat(app): add shared bounds key functionality for home media navigation
This commit is contained in:
@@ -41,8 +41,10 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||||||
import hu.bbara.purefin.data.SessionBootstrapper
|
import hu.bbara.purefin.data.SessionBootstrapper
|
||||||
import hu.bbara.purefin.jellyfin.JellyfinAuthInterceptor
|
import hu.bbara.purefin.jellyfin.JellyfinAuthInterceptor
|
||||||
import hu.bbara.purefin.data.UserSessionRepository
|
import hu.bbara.purefin.data.UserSessionRepository
|
||||||
|
import hu.bbara.purefin.navigation.LocalHomeMediaSharedBoundsKey
|
||||||
import hu.bbara.purefin.navigation.LocalNavigationBackStack
|
import hu.bbara.purefin.navigation.LocalNavigationBackStack
|
||||||
import hu.bbara.purefin.navigation.LocalNavigationManager
|
import hu.bbara.purefin.navigation.LocalNavigationManager
|
||||||
|
import hu.bbara.purefin.navigation.LocalSetHomeMediaSharedBoundsKey
|
||||||
import hu.bbara.purefin.navigation.LocalSharedTransitionScope
|
import hu.bbara.purefin.navigation.LocalSharedTransitionScope
|
||||||
import hu.bbara.purefin.navigation.NavigationCommand
|
import hu.bbara.purefin.navigation.NavigationCommand
|
||||||
import hu.bbara.purefin.navigation.NavigationManager
|
import hu.bbara.purefin.navigation.NavigationManager
|
||||||
@@ -158,6 +160,7 @@ class PurefinActivity : ComponentActivity() {
|
|||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
val backStack = rememberNavBackStack(Route.Home) as NavBackStack<Route>
|
val backStack = rememberNavBackStack(Route.Home) as NavBackStack<Route>
|
||||||
|
var homeMediaSharedBoundsKey by remember { mutableStateOf<String?>(null) }
|
||||||
val appEntryProvider =
|
val appEntryProvider =
|
||||||
entryProvider {
|
entryProvider {
|
||||||
entryBuilders.forEach { builder -> builder() }
|
entryBuilders.forEach { builder -> builder() }
|
||||||
@@ -180,7 +183,11 @@ class PurefinActivity : ComponentActivity() {
|
|||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalNavigationManager provides navigationManager,
|
LocalNavigationManager provides navigationManager,
|
||||||
LocalNavigationBackStack provides backStack.toList(),
|
LocalNavigationBackStack provides backStack.toList(),
|
||||||
LocalSharedTransitionScope provides this
|
LocalSharedTransitionScope provides this,
|
||||||
|
LocalHomeMediaSharedBoundsKey provides homeMediaSharedBoundsKey,
|
||||||
|
LocalSetHomeMediaSharedBoundsKey provides { key ->
|
||||||
|
homeMediaSharedBoundsKey = key
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
NavDisplay(
|
NavDisplay(
|
||||||
backStack = backStack,
|
backStack = backStack,
|
||||||
|
|||||||
@@ -18,4 +18,8 @@ val LocalSharedTransitionScope = compositionLocalOf<SharedTransitionScope?> { nu
|
|||||||
val LocalNavSharedAnimatedVisibilityScope =
|
val LocalNavSharedAnimatedVisibilityScope =
|
||||||
compositionLocalOf<AnimatedVisibilityScope?> { null }
|
compositionLocalOf<AnimatedVisibilityScope?> { null }
|
||||||
|
|
||||||
|
val LocalHomeMediaSharedBoundsKey = compositionLocalOf<String?> { null }
|
||||||
|
|
||||||
|
val LocalSetHomeMediaSharedBoundsKey = staticCompositionLocalOf<(String) -> Unit> { {} }
|
||||||
|
|
||||||
const val HOME_SEARCH_SHARED_BOUNDS_KEY = "home_search_shared_bounds"
|
const val HOME_SEARCH_SHARED_BOUNDS_KEY = "home_search_shared_bounds"
|
||||||
|
|||||||
@@ -27,13 +27,25 @@ fun EntryProviderScope<Route>.appRouteEntryBuilder() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
entry<Route.MovieRoute> {
|
entry<Route.MovieRoute> {
|
||||||
MovieScreen(movie = it.item)
|
CompositionLocalProvider(
|
||||||
|
LocalNavSharedAnimatedVisibilityScope provides LocalNavAnimatedContentScope.current
|
||||||
|
) {
|
||||||
|
MovieScreen(movie = it.item)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
entry<Route.SeriesRoute> {
|
entry<Route.SeriesRoute> {
|
||||||
SeriesScreen(series = it.item)
|
CompositionLocalProvider(
|
||||||
|
LocalNavSharedAnimatedVisibilityScope provides LocalNavAnimatedContentScope.current
|
||||||
|
) {
|
||||||
|
SeriesScreen(series = it.item)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
entry<Route.EpisodeRoute> {
|
entry<Route.EpisodeRoute> {
|
||||||
EpisodeScreen(episode = it.item)
|
CompositionLocalProvider(
|
||||||
|
LocalNavSharedAnimatedVisibilityScope provides LocalNavAnimatedContentScope.current
|
||||||
|
) {
|
||||||
|
EpisodeScreen(episode = it.item)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
entry<Route.LibraryRoute> {
|
entry<Route.LibraryRoute> {
|
||||||
LibraryScreen(library = it.library)
|
LibraryScreen(library = it.library)
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package hu.bbara.purefin.ui.common.media
|
||||||
|
|
||||||
|
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||||
|
import androidx.compose.animation.SharedTransitionScope
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import hu.bbara.purefin.navigation.LocalHomeMediaSharedBoundsKey
|
||||||
|
import hu.bbara.purefin.navigation.LocalNavSharedAnimatedVisibilityScope
|
||||||
|
import hu.bbara.purefin.navigation.LocalSetHomeMediaSharedBoundsKey
|
||||||
|
import hu.bbara.purefin.navigation.LocalSharedTransitionScope
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
fun homeMediaSharedBoundsKey(origin: String, mediaId: UUID): String =
|
||||||
|
"home_media_${origin}_$mediaId"
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||||
|
@Composable
|
||||||
|
fun Modifier.homeMediaSharedBoundsSource(sharedBoundsKey: String): Modifier {
|
||||||
|
val selectedKey = LocalHomeMediaSharedBoundsKey.current
|
||||||
|
return homeMediaSharedBounds(sharedBoundsKey.takeIf { it == selectedKey })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Modifier.homeMediaSharedBoundsDestination(): Modifier =
|
||||||
|
homeMediaSharedBounds(LocalHomeMediaSharedBoundsKey.current)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||||
|
@Composable
|
||||||
|
fun isHomeMediaSharedBoundsTransitionActive(): Boolean {
|
||||||
|
val sharedBoundsKey = LocalHomeMediaSharedBoundsKey.current
|
||||||
|
val sharedTransitionScope = LocalSharedTransitionScope.current
|
||||||
|
return sharedBoundsKey != null && sharedTransitionScope?.isTransitionActive == true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun rememberHomeMediaSharedBoundsClick(
|
||||||
|
sharedBoundsKey: String,
|
||||||
|
onClick: () -> Unit
|
||||||
|
): () -> Unit {
|
||||||
|
val selectSharedBoundsKey = LocalSetHomeMediaSharedBoundsKey.current
|
||||||
|
return remember(sharedBoundsKey, onClick, selectSharedBoundsKey) {
|
||||||
|
{
|
||||||
|
selectSharedBoundsKey(sharedBoundsKey)
|
||||||
|
onClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun Modifier.homeMediaSharedBounds(sharedBoundsKey: String?): Modifier {
|
||||||
|
val sharedTransitionScope = LocalSharedTransitionScope.current
|
||||||
|
val animatedVisibilityScope = LocalNavSharedAnimatedVisibilityScope.current
|
||||||
|
|
||||||
|
if (
|
||||||
|
sharedBoundsKey == null ||
|
||||||
|
sharedTransitionScope == null ||
|
||||||
|
animatedVisibilityScope == null
|
||||||
|
) {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
return with(sharedTransitionScope) {
|
||||||
|
this@homeMediaSharedBounds.sharedBounds(
|
||||||
|
sharedContentState = rememberSharedContentState(key = sharedBoundsKey),
|
||||||
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut(),
|
||||||
|
resizeMode = SharedTransitionScope.ResizeMode.scaleToBounds()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,6 +34,8 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||||||
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
||||||
import hu.bbara.purefin.ui.common.media.MediaHero
|
import hu.bbara.purefin.ui.common.media.MediaHero
|
||||||
import hu.bbara.purefin.ui.common.media.MediaMetadataFlowRow
|
import hu.bbara.purefin.ui.common.media.MediaMetadataFlowRow
|
||||||
|
import hu.bbara.purefin.ui.common.media.homeMediaSharedBoundsDestination
|
||||||
|
import hu.bbara.purefin.ui.common.media.isHomeMediaSharedBoundsTransitionActive
|
||||||
import hu.bbara.purefin.download.DownloadState
|
import hu.bbara.purefin.download.DownloadState
|
||||||
import hu.bbara.purefin.image.ImageUrlBuilder
|
import hu.bbara.purefin.image.ImageUrlBuilder
|
||||||
import hu.bbara.purefin.navigation.EpisodeDto
|
import hu.bbara.purefin.navigation.EpisodeDto
|
||||||
@@ -117,17 +119,20 @@ private fun EpisodeScreenInternal(
|
|||||||
onDownloadClick: () -> Unit,
|
onDownloadClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val isHomeMediaTransitionActive = isHomeMediaSharedBoundsTransitionActive()
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
containerColor = MaterialTheme.colorScheme.background,
|
containerColor = MaterialTheme.colorScheme.background,
|
||||||
topBar = {
|
topBar = {
|
||||||
EpisodeTopBar(
|
if (!isHomeMediaTransitionActive) {
|
||||||
shortcut = topBarShortcut,
|
EpisodeTopBar(
|
||||||
onBack = onBack,
|
shortcut = topBarShortcut,
|
||||||
onSeriesClick = onSeriesClick,
|
onBack = onBack,
|
||||||
modifier = Modifier
|
onSeriesClick = onSeriesClick,
|
||||||
)
|
modifier = Modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
Column(
|
Column(
|
||||||
@@ -136,15 +141,17 @@ private fun EpisodeScreenInternal(
|
|||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
EpisodeHeroSection(episode = episode)
|
EpisodeHeroSection(episode = episode)
|
||||||
EpisodeDetails(
|
if (!isHomeMediaTransitionActive) {
|
||||||
episode = episode,
|
EpisodeDetails(
|
||||||
downloadState = downloadState,
|
episode = episode,
|
||||||
onDownloadClick = onDownloadClick,
|
downloadState = downloadState,
|
||||||
modifier = Modifier
|
onDownloadClick = onDownloadClick,
|
||||||
.fillMaxWidth()
|
modifier = Modifier
|
||||||
.padding(horizontal = 16.dp)
|
.fillMaxWidth()
|
||||||
.padding(bottom = innerPadding.calculateBottomPadding())
|
.padding(horizontal = 16.dp)
|
||||||
)
|
.padding(bottom = innerPadding.calculateBottomPadding())
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,6 +167,7 @@ private fun EpisodeHeroSection(
|
|||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
.homeMediaSharedBoundsDestination()
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(sectionHeight)
|
.height(sectionHeight)
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -26,17 +26,23 @@ import androidx.compose.ui.unit.dp
|
|||||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||||
import hu.bbara.purefin.ui.common.bar.MediaProgressBar
|
import hu.bbara.purefin.ui.common.bar.MediaProgressBar
|
||||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
|
import hu.bbara.purefin.ui.common.media.homeMediaSharedBoundsSource
|
||||||
|
import hu.bbara.purefin.ui.common.media.rememberHomeMediaSharedBoundsClick
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ContinueWatchingCard(
|
internal fun ContinueWatchingCard(
|
||||||
item: MediaUiModel,
|
item: MediaUiModel,
|
||||||
|
sharedBoundsKey: String,
|
||||||
onMediaSelected: (MediaUiModel) -> Unit,
|
onMediaSelected: (MediaUiModel) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
val onClick = rememberHomeMediaSharedBoundsClick(sharedBoundsKey) {
|
||||||
|
onMediaSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
onClick = { onMediaSelected(item) },
|
onClick = onClick,
|
||||||
shape = RoundedCornerShape(26.dp),
|
shape = RoundedCornerShape(26.dp),
|
||||||
colors = CardDefaults.cardColors(containerColor = scheme.surfaceContainer),
|
colors = CardDefaults.cardColors(containerColor = scheme.surfaceContainer),
|
||||||
modifier = modifier.width(280.dp)
|
modifier = modifier.width(280.dp)
|
||||||
@@ -44,6 +50,7 @@ internal fun ContinueWatchingCard(
|
|||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.homeMediaSharedBoundsSource(sharedBoundsKey)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.aspectRatio(16f / 9f)
|
.aspectRatio(16f / 9f)
|
||||||
.background(scheme.surfaceContainer)
|
.background(scheme.surfaceContainer)
|
||||||
|
|||||||
@@ -7,13 +7,14 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.ui.common.header.SectionHeader
|
import hu.bbara.purefin.ui.common.header.SectionHeader
|
||||||
|
import hu.bbara.purefin.ui.common.media.homeMediaSharedBoundsKey
|
||||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -42,9 +43,10 @@ fun ContinueWatchingSection(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
state = listState
|
state = listState
|
||||||
) {
|
) {
|
||||||
items(items = items, key = { item -> item.id }) { item ->
|
itemsIndexed(items = items, key = { _, item -> item.id }) { index, item ->
|
||||||
ContinueWatchingCard(
|
ContinueWatchingCard(
|
||||||
item = item,
|
item = item,
|
||||||
|
sharedBoundsKey = homeMediaSharedBoundsKey("continue-$index", item.id),
|
||||||
onMediaSelected = onMediaSelected
|
onMediaSelected = onMediaSelected
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,18 +26,25 @@ import androidx.compose.ui.unit.dp
|
|||||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||||
import hu.bbara.purefin.ui.common.bar.MediaProgressBar
|
import hu.bbara.purefin.ui.common.bar.MediaProgressBar
|
||||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
|
import hu.bbara.purefin.ui.common.media.homeMediaSharedBoundsSource
|
||||||
|
import hu.bbara.purefin.ui.common.media.rememberHomeMediaSharedBoundsClick
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SuggestionCard(
|
internal fun SuggestionCard(
|
||||||
item: MediaUiModel,
|
item: MediaUiModel,
|
||||||
|
sharedBoundsKey: String,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
val scheme = MaterialTheme.colorScheme
|
||||||
val description = item.description.trim()
|
val description = item.description.trim()
|
||||||
|
val onCardClick = rememberHomeMediaSharedBoundsClick(
|
||||||
|
sharedBoundsKey = sharedBoundsKey,
|
||||||
|
onClick = onClick
|
||||||
|
)
|
||||||
|
|
||||||
ElevatedCard(
|
ElevatedCard(
|
||||||
onClick = onClick,
|
onClick = onCardClick,
|
||||||
colors = CardDefaults.elevatedCardColors(containerColor = scheme.surfaceContainerLow),
|
colors = CardDefaults.elevatedCardColors(containerColor = scheme.surfaceContainerLow),
|
||||||
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 1.dp),
|
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 1.dp),
|
||||||
shape = RoundedCornerShape(30.dp),
|
shape = RoundedCornerShape(30.dp),
|
||||||
@@ -45,6 +52,7 @@ internal fun SuggestionCard(
|
|||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.homeMediaSharedBoundsSource(sharedBoundsKey)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.aspectRatio(16f / 11f)
|
.aspectRatio(16f / 11f)
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import hu.bbara.purefin.ui.common.media.homeMediaSharedBoundsKey
|
||||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -49,9 +50,11 @@ fun SuggestionsSection(
|
|||||||
pageSpacing = 16.dp,
|
pageSpacing = 16.dp,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) { page ->
|
) { page ->
|
||||||
|
val item = items[page]
|
||||||
SuggestionCard(
|
SuggestionCard(
|
||||||
item = items[page],
|
item = item,
|
||||||
onClick = { onItemOpen(items[page]) }
|
sharedBoundsKey = homeMediaSharedBoundsKey("suggestion-$page", item.id),
|
||||||
|
onClick = { onItemOpen(item) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (items.size > 1) {
|
if (items.size > 1) {
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.ui.common.badge.WatchStateBadge
|
import hu.bbara.purefin.ui.common.badge.WatchStateBadge
|
||||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
|
import hu.bbara.purefin.ui.common.media.homeMediaSharedBoundsSource
|
||||||
|
import hu.bbara.purefin.ui.common.media.rememberHomeMediaSharedBoundsClick
|
||||||
import hu.bbara.purefin.ui.model.EpisodeUiModel
|
import hu.bbara.purefin.ui.model.EpisodeUiModel
|
||||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||||
import hu.bbara.purefin.ui.model.MovieUiModel
|
import hu.bbara.purefin.ui.model.MovieUiModel
|
||||||
@@ -32,13 +34,17 @@ import hu.bbara.purefin.ui.model.MovieUiModel
|
|||||||
@Composable
|
@Composable
|
||||||
internal fun HomeBrowseCard(
|
internal fun HomeBrowseCard(
|
||||||
uiModel: MediaUiModel,
|
uiModel: MediaUiModel,
|
||||||
|
sharedBoundsKey: String,
|
||||||
onMediaSelected: (MediaUiModel) -> Unit,
|
onMediaSelected: (MediaUiModel) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
val onClick = rememberHomeMediaSharedBoundsClick(sharedBoundsKey) {
|
||||||
|
onMediaSelected(uiModel)
|
||||||
|
}
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
onClick = { onMediaSelected(uiModel) },
|
onClick = onClick,
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
colors = CardDefaults.cardColors(containerColor = scheme.surfaceContainer),
|
colors = CardDefaults.cardColors(containerColor = scheme.surfaceContainer),
|
||||||
modifier = modifier.width(188.dp)
|
modifier = modifier.width(188.dp)
|
||||||
@@ -46,6 +52,7 @@ internal fun HomeBrowseCard(
|
|||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.homeMediaSharedBoundsSource(sharedBoundsKey)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.aspectRatio(16f / 10f)
|
.aspectRatio(16f / 10f)
|
||||||
.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp))
|
.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp))
|
||||||
|
|||||||
@@ -7,13 +7,14 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.ui.common.header.SectionHeader
|
import hu.bbara.purefin.ui.common.header.SectionHeader
|
||||||
|
import hu.bbara.purefin.ui.common.media.homeMediaSharedBoundsKey
|
||||||
import hu.bbara.purefin.ui.model.LibraryUiModel
|
import hu.bbara.purefin.ui.model.LibraryUiModel
|
||||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||||
|
|
||||||
@@ -49,9 +50,13 @@ fun LibraryPosterSection(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
state = listState
|
state = listState
|
||||||
) {
|
) {
|
||||||
items(items = items, key = { item -> item.id }) { item ->
|
itemsIndexed(items = items, key = { _, item -> item.id }) { index, item ->
|
||||||
HomeBrowseCard(
|
HomeBrowseCard(
|
||||||
uiModel = item,
|
uiModel = item,
|
||||||
|
sharedBoundsKey = homeMediaSharedBoundsKey(
|
||||||
|
origin = "library-${library.id}-$index",
|
||||||
|
mediaId = item.id
|
||||||
|
),
|
||||||
onMediaSelected = onMediaSelected
|
onMediaSelected = onMediaSelected
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,17 +24,23 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
|
import hu.bbara.purefin.ui.common.media.homeMediaSharedBoundsSource
|
||||||
|
import hu.bbara.purefin.ui.common.media.rememberHomeMediaSharedBoundsClick
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun NextUpCard(
|
internal fun NextUpCard(
|
||||||
uiModel: MediaUiModel,
|
uiModel: MediaUiModel,
|
||||||
|
sharedBoundsKey: String,
|
||||||
onMediaSelected: (MediaUiModel) -> Unit,
|
onMediaSelected: (MediaUiModel) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
val onClick = rememberHomeMediaSharedBoundsClick(sharedBoundsKey) {
|
||||||
|
onMediaSelected(uiModel)
|
||||||
|
}
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
onClick = { onMediaSelected(uiModel) },
|
onClick = onClick,
|
||||||
shape = RoundedCornerShape(24.dp),
|
shape = RoundedCornerShape(24.dp),
|
||||||
colors = CardDefaults.cardColors(containerColor = scheme.surfaceContainer),
|
colors = CardDefaults.cardColors(containerColor = scheme.surfaceContainer),
|
||||||
modifier = modifier.width(256.dp)
|
modifier = modifier.width(256.dp)
|
||||||
@@ -42,6 +48,7 @@ internal fun NextUpCard(
|
|||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.homeMediaSharedBoundsSource(sharedBoundsKey)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.aspectRatio(16f / 10f)
|
.aspectRatio(16f / 10f)
|
||||||
.background(scheme.surface)
|
.background(scheme.surface)
|
||||||
|
|||||||
@@ -7,13 +7,14 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.ui.common.header.SectionHeader
|
import hu.bbara.purefin.ui.common.header.SectionHeader
|
||||||
|
import hu.bbara.purefin.ui.common.media.homeMediaSharedBoundsKey
|
||||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -42,9 +43,10 @@ fun NextUpSection(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
state = listState
|
state = listState
|
||||||
) {
|
) {
|
||||||
items(items = items, key = { item -> item.id }) { item ->
|
itemsIndexed(items = items, key = { _, item -> item.id }) { index, item ->
|
||||||
NextUpCard(
|
NextUpCard(
|
||||||
uiModel = item,
|
uiModel = item,
|
||||||
|
sharedBoundsKey = homeMediaSharedBoundsKey("next-up-$index", item.id),
|
||||||
onMediaSelected = onMediaSelected
|
onMediaSelected = onMediaSelected
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ import hu.bbara.purefin.model.Movie
|
|||||||
import hu.bbara.purefin.navigation.MovieDto
|
import hu.bbara.purefin.navigation.MovieDto
|
||||||
import hu.bbara.purefin.ui.common.media.MediaHero
|
import hu.bbara.purefin.ui.common.media.MediaHero
|
||||||
import hu.bbara.purefin.ui.common.media.MediaMetadataFlowRow
|
import hu.bbara.purefin.ui.common.media.MediaMetadataFlowRow
|
||||||
|
import hu.bbara.purefin.ui.common.media.homeMediaSharedBoundsDestination
|
||||||
|
import hu.bbara.purefin.ui.common.media.isHomeMediaSharedBoundsTransitionActive
|
||||||
import hu.bbara.purefin.ui.screen.movie.components.MovieDetails
|
import hu.bbara.purefin.ui.screen.movie.components.MovieDetails
|
||||||
import hu.bbara.purefin.ui.screen.movie.components.MovieTopBar
|
import hu.bbara.purefin.ui.screen.movie.components.MovieTopBar
|
||||||
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
||||||
@@ -94,14 +96,18 @@ private fun MovieScreenInternal(
|
|||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val isHomeMediaTransitionActive = isHomeMediaSharedBoundsTransitionActive()
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
containerColor = MaterialTheme.colorScheme.background,
|
containerColor = MaterialTheme.colorScheme.background,
|
||||||
topBar = {
|
topBar = {
|
||||||
MovieTopBar(
|
if (!isHomeMediaTransitionActive) {
|
||||||
onBack = onBack,
|
MovieTopBar(
|
||||||
modifier = Modifier
|
onBack = onBack,
|
||||||
)
|
modifier = Modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
Column(
|
Column(
|
||||||
@@ -110,15 +116,17 @@ private fun MovieScreenInternal(
|
|||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
MediaHeroSection(movie = movie)
|
MediaHeroSection(movie = movie)
|
||||||
MovieDetails(
|
if (!isHomeMediaTransitionActive) {
|
||||||
movie = movie,
|
MovieDetails(
|
||||||
downloadState = downloadState,
|
movie = movie,
|
||||||
onDownloadClick = onDownloadClick,
|
downloadState = downloadState,
|
||||||
modifier = Modifier
|
onDownloadClick = onDownloadClick,
|
||||||
.fillMaxWidth()
|
modifier = Modifier
|
||||||
.padding(horizontal = 16.dp)
|
.fillMaxWidth()
|
||||||
.padding(bottom = innerPadding.calculateBottomPadding())
|
.padding(horizontal = 16.dp)
|
||||||
)
|
.padding(bottom = innerPadding.calculateBottomPadding())
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,6 +142,7 @@ fun MediaHeroSection(
|
|||||||
|
|
||||||
Box (
|
Box (
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
.homeMediaSharedBoundsDestination()
|
||||||
.height(sectionHeight)
|
.height(sectionHeight)
|
||||||
) {
|
) {
|
||||||
MediaHero(
|
MediaHero(
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ import hu.bbara.purefin.model.Series
|
|||||||
import hu.bbara.purefin.navigation.SeriesDto
|
import hu.bbara.purefin.navigation.SeriesDto
|
||||||
import hu.bbara.purefin.ui.common.media.MediaHero
|
import hu.bbara.purefin.ui.common.media.MediaHero
|
||||||
import hu.bbara.purefin.ui.common.media.MediaSynopsis
|
import hu.bbara.purefin.ui.common.media.MediaSynopsis
|
||||||
|
import hu.bbara.purefin.ui.common.media.homeMediaSharedBoundsDestination
|
||||||
|
import hu.bbara.purefin.ui.common.media.isHomeMediaSharedBoundsTransitionActive
|
||||||
import hu.bbara.purefin.ui.screen.series.components.CastRow
|
import hu.bbara.purefin.ui.screen.series.components.CastRow
|
||||||
import hu.bbara.purefin.ui.screen.series.components.EpisodeCarousel
|
import hu.bbara.purefin.ui.screen.series.components.EpisodeCarousel
|
||||||
import hu.bbara.purefin.ui.screen.series.components.SeasonTabs
|
import hu.bbara.purefin.ui.screen.series.components.SeasonTabs
|
||||||
@@ -103,6 +105,7 @@ private fun SeriesScreenInternal(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
val isHomeMediaTransitionActive = isHomeMediaSharedBoundsTransitionActive()
|
||||||
|
|
||||||
fun getDefaultSeason() : Season {
|
fun getDefaultSeason() : Season {
|
||||||
for (season in series.seasons) {
|
for (season in series.seasons) {
|
||||||
@@ -131,10 +134,12 @@ private fun SeriesScreenInternal(
|
|||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
containerColor = MaterialTheme.colorScheme.background,
|
containerColor = MaterialTheme.colorScheme.background,
|
||||||
topBar = {
|
topBar = {
|
||||||
SeriesTopBar(
|
if (!isHomeMediaTransitionActive) {
|
||||||
onBack = onBack,
|
SeriesTopBar(
|
||||||
modifier = Modifier
|
onBack = onBack,
|
||||||
)
|
modifier = Modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
Column(
|
Column(
|
||||||
@@ -143,48 +148,50 @@ private fun SeriesScreenInternal(
|
|||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
SeriesHeroSection(series = series)
|
SeriesHeroSection(series = series)
|
||||||
Column(
|
if (!isHomeMediaTransitionActive) {
|
||||||
modifier = Modifier
|
Column(
|
||||||
.fillMaxWidth()
|
modifier = Modifier
|
||||||
.padding(horizontal = 16.dp)
|
.fillMaxWidth()
|
||||||
.padding(bottom = innerPadding.calculateBottomPadding())
|
.padding(horizontal = 16.dp)
|
||||||
) {
|
.padding(bottom = innerPadding.calculateBottomPadding())
|
||||||
SeriesActionButtons(
|
) {
|
||||||
nextUpEpisode = nextUpEpisode,
|
SeriesActionButtons(
|
||||||
seriesDownloadState = seriesDownloadState,
|
nextUpEpisode = nextUpEpisode,
|
||||||
selectedSeason = selectedSeason,
|
seriesDownloadState = seriesDownloadState,
|
||||||
seasonDownloadState = seasonDownloadState,
|
selectedSeason = selectedSeason,
|
||||||
onDownloadOptionSelected = { option ->
|
seasonDownloadState = seasonDownloadState,
|
||||||
onDownloadOptionSelected(option, selectedSeason)
|
onDownloadOptionSelected = { option ->
|
||||||
}
|
onDownloadOptionSelected(option, selectedSeason)
|
||||||
)
|
}
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
MediaSynopsis(
|
|
||||||
synopsis = series.synopsis,
|
|
||||||
bodyColor = scheme.onSurface,
|
|
||||||
bodyFontSize = 13.sp,
|
|
||||||
bodyLineHeight = null,
|
|
||||||
titleSpacing = 8.dp
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
SeasonTabs(
|
|
||||||
seasons = series.seasons,
|
|
||||||
selectedSeason = selectedSeason,
|
|
||||||
onSelect = { selectedSeasonId = it.id }
|
|
||||||
)
|
|
||||||
EpisodeCarousel(
|
|
||||||
episodes = selectedSeason.episodes,
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
if(series.cast.isNotEmpty()) {
|
|
||||||
Text(
|
|
||||||
text = "Cast",
|
|
||||||
color = scheme.onBackground,
|
|
||||||
fontSize = 18.sp,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
CastRow(cast = series.cast)
|
MediaSynopsis(
|
||||||
|
synopsis = series.synopsis,
|
||||||
|
bodyColor = scheme.onSurface,
|
||||||
|
bodyFontSize = 13.sp,
|
||||||
|
bodyLineHeight = null,
|
||||||
|
titleSpacing = 8.dp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
SeasonTabs(
|
||||||
|
seasons = series.seasons,
|
||||||
|
selectedSeason = selectedSeason,
|
||||||
|
onSelect = { selectedSeasonId = it.id }
|
||||||
|
)
|
||||||
|
EpisodeCarousel(
|
||||||
|
episodes = selectedSeason.episodes,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
if(series.cast.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = "Cast",
|
||||||
|
color = scheme.onBackground,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
CastRow(cast = series.cast)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -202,6 +209,7 @@ private fun SeriesHeroSection(
|
|||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
.homeMediaSharedBoundsDestination()
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(sectionHeight)
|
.height(sectionHeight)
|
||||||
) {
|
) {
|
||||||
|
|||||||
Reference in New Issue
Block a user