mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
Compare commits
10 Commits
71a9a8b98e
...
0f0c86dcf9
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f0c86dcf9 | |||
| 557679861a | |||
| 64b1032b74 | |||
| 44219d00d8 | |||
| cff0b60b31 | |||
| 9610124af0 | |||
| 185a0c8a85 | |||
| dfaf1079ac | |||
| 65693a9220 | |||
| 58087bb4a5 |
@@ -6,7 +6,6 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -55,62 +54,65 @@ internal fun TvIconButton(
|
||||
targetValue = if (isFocused) scheme.primary else Color.Transparent,
|
||||
label = "border"
|
||||
)
|
||||
val shape = RoundedCornerShape(50)
|
||||
val iconSize = (size - 24).coerceAtLeast(0).dp
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.graphicsLayer {
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
val buttonModifier = modifier
|
||||
.graphicsLayer {
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
}
|
||||
.alpha(if (enabled) 1f else 0.4f)
|
||||
.border(
|
||||
width = if (isFocused) 2.dp else 0.dp,
|
||||
color = borderColor,
|
||||
shape = shape
|
||||
)
|
||||
.clip(shape)
|
||||
.background(
|
||||
if (isFocused) scheme.primary.copy(alpha = 0.5f)
|
||||
else scheme.background.copy(alpha = 0.65f)
|
||||
)
|
||||
.focusProperties { canFocus = enabled }
|
||||
.semantics {
|
||||
if (!enabled) {
|
||||
disabled()
|
||||
}
|
||||
.alpha(if (enabled) 1f else 0.4f)
|
||||
.widthIn(min = if (label == null) size.dp else 104.dp)
|
||||
.height(size.dp)
|
||||
.border(
|
||||
width = if (isFocused) 2.dp else 0.dp,
|
||||
color = borderColor,
|
||||
shape = RoundedCornerShape(50)
|
||||
)
|
||||
.clip(RoundedCornerShape(50))
|
||||
.background(
|
||||
if (isFocused) scheme.primary.copy(alpha = 0.5f)
|
||||
else scheme.background.copy(alpha = 0.65f)
|
||||
)
|
||||
.focusProperties { canFocus = enabled }
|
||||
.semantics {
|
||||
if (!enabled) {
|
||||
disabled()
|
||||
}
|
||||
}
|
||||
.onFocusChanged { isFocused = it.isFocused }
|
||||
.clickable(enabled = enabled) { onClick() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (label == null) {
|
||||
}
|
||||
.onFocusChanged { isFocused = it.isFocused }
|
||||
.clickable(enabled = enabled) { onClick() }
|
||||
|
||||
if (label == null) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = contentDescription,
|
||||
tint = scheme.onBackground,
|
||||
modifier = buttonModifier
|
||||
.size(size.dp)
|
||||
.padding(12.dp)
|
||||
.size(iconSize)
|
||||
)
|
||||
} else {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = buttonModifier
|
||||
.widthIn(min = 104.dp)
|
||||
.height(size.dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = contentDescription,
|
||||
tint = scheme.onBackground,
|
||||
modifier = Modifier.padding(8.dp)
|
||||
modifier = Modifier.size(iconSize)
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
color = scheme.onBackground,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
} else {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = contentDescription,
|
||||
tint = scheme.onBackground,
|
||||
modifier = Modifier.size(28.dp)
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
color = scheme.onBackground,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,21 +258,21 @@ private fun TvPlayerBottomSection(
|
||||
icon = Icons.Outlined.SkipPrevious,
|
||||
contentDescription = "Previous",
|
||||
onClick = onPrevious,
|
||||
size = 64,
|
||||
size = 56,
|
||||
modifier = expandPlaylistModifier
|
||||
)
|
||||
TvIconButton(
|
||||
icon = Icons.Outlined.Replay10,
|
||||
contentDescription = "Seek backward 10 seconds",
|
||||
onClick = { onSeekRelative(-10_000) },
|
||||
size = 64,
|
||||
size = 56,
|
||||
modifier = expandPlaylistModifier
|
||||
)
|
||||
TvIconButton(
|
||||
icon = if (uiState.isPlaying) Icons.Outlined.Pause else Icons.Outlined.PlayArrow,
|
||||
contentDescription = if (uiState.isPlaying) "Pause" else "Play",
|
||||
onClick = onPlayPause,
|
||||
size = 72,
|
||||
size = 64,
|
||||
modifier = expandPlaylistModifier
|
||||
.focusRequester(focusRequester)
|
||||
.testTag(TvPlayerPlayPauseButtonTag)
|
||||
@@ -281,14 +281,14 @@ private fun TvPlayerBottomSection(
|
||||
icon = Icons.Outlined.Forward30,
|
||||
contentDescription = "Seek forward 30 seconds",
|
||||
onClick = { onSeekRelative(30_000) },
|
||||
size = 64,
|
||||
size = 56,
|
||||
modifier = expandPlaylistModifier
|
||||
)
|
||||
TvIconButton(
|
||||
icon = Icons.Outlined.SkipNext,
|
||||
contentDescription = "Next",
|
||||
onClick = onNext,
|
||||
size = 64,
|
||||
size = 56,
|
||||
modifier = expandPlaylistModifier
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||
import androidx.compose.animation.SharedTransitionLayout
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
@@ -39,8 +41,11 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
import hu.bbara.purefin.data.SessionBootstrapper
|
||||
import hu.bbara.purefin.jellyfin.JellyfinAuthInterceptor
|
||||
import hu.bbara.purefin.data.UserSessionRepository
|
||||
import hu.bbara.purefin.navigation.LocalHomeMediaSharedBoundsKey
|
||||
import hu.bbara.purefin.navigation.LocalNavigationBackStack
|
||||
import hu.bbara.purefin.navigation.LocalNavigationManager
|
||||
import hu.bbara.purefin.navigation.LocalSetHomeMediaSharedBoundsKey
|
||||
import hu.bbara.purefin.navigation.LocalSharedTransitionScope
|
||||
import hu.bbara.purefin.navigation.NavigationCommand
|
||||
import hu.bbara.purefin.navigation.NavigationManager
|
||||
import hu.bbara.purefin.navigation.Route
|
||||
@@ -131,6 +136,7 @@ class PurefinActivity : ComponentActivity() {
|
||||
return (applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
fun MainApp(
|
||||
userSessionRepository: UserSessionRepository,
|
||||
@@ -154,6 +160,7 @@ class PurefinActivity : ComponentActivity() {
|
||||
if (isLoggedIn) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val backStack = rememberNavBackStack(Route.Home) as NavBackStack<Route>
|
||||
var homeMediaSharedBoundsKey by remember { mutableStateOf<String?>(null) }
|
||||
val appEntryProvider =
|
||||
entryProvider {
|
||||
entryBuilders.forEach { builder -> builder() }
|
||||
@@ -172,45 +179,52 @@ class PurefinActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalNavigationManager provides navigationManager,
|
||||
LocalNavigationBackStack provides backStack.toList()
|
||||
) {
|
||||
NavDisplay(
|
||||
backStack = backStack,
|
||||
onBack = { navigationManager.pop() },
|
||||
modifier = Modifier.fillMaxSize().background(backgroundDark),
|
||||
transitionSpec = {
|
||||
fadeIn(
|
||||
animationSpec = tween(
|
||||
durationMillis = 140
|
||||
)
|
||||
) togetherWith
|
||||
fadeOut(animationSpec = tween(durationMillis = 140))
|
||||
},
|
||||
popTransitionSpec = {
|
||||
fadeIn(
|
||||
animationSpec = tween(
|
||||
durationMillis = 140
|
||||
)
|
||||
) togetherWith
|
||||
fadeOut(animationSpec = tween(durationMillis = 140))
|
||||
},
|
||||
predictivePopTransitionSpec = { _ ->
|
||||
fadeIn(
|
||||
animationSpec = tween(
|
||||
durationMillis = 140
|
||||
)
|
||||
) togetherWith
|
||||
fadeOut(animationSpec = tween(durationMillis = 140))
|
||||
},
|
||||
entryDecorators =
|
||||
listOf(
|
||||
rememberSaveableStateHolderNavEntryDecorator(),
|
||||
rememberViewModelStoreNavEntryDecorator(),
|
||||
),
|
||||
entryProvider = appEntryProvider
|
||||
)
|
||||
SharedTransitionLayout {
|
||||
CompositionLocalProvider(
|
||||
LocalNavigationManager provides navigationManager,
|
||||
LocalNavigationBackStack provides backStack.toList(),
|
||||
LocalSharedTransitionScope provides this,
|
||||
LocalHomeMediaSharedBoundsKey provides homeMediaSharedBoundsKey,
|
||||
LocalSetHomeMediaSharedBoundsKey provides { key ->
|
||||
homeMediaSharedBoundsKey = key
|
||||
}
|
||||
) {
|
||||
NavDisplay(
|
||||
backStack = backStack,
|
||||
onBack = { navigationManager.pop() },
|
||||
modifier = Modifier.fillMaxSize().background(backgroundDark),
|
||||
transitionSpec = {
|
||||
fadeIn(
|
||||
animationSpec = tween(
|
||||
durationMillis = 140
|
||||
)
|
||||
) togetherWith
|
||||
fadeOut(animationSpec = tween(durationMillis = 140))
|
||||
},
|
||||
popTransitionSpec = {
|
||||
fadeIn(
|
||||
animationSpec = tween(
|
||||
durationMillis = 140
|
||||
)
|
||||
) togetherWith
|
||||
fadeOut(animationSpec = tween(durationMillis = 140))
|
||||
},
|
||||
predictivePopTransitionSpec = { _ ->
|
||||
fadeIn(
|
||||
animationSpec = tween(
|
||||
durationMillis = 140
|
||||
)
|
||||
) togetherWith
|
||||
fadeOut(animationSpec = tween(durationMillis = 140))
|
||||
},
|
||||
entryDecorators =
|
||||
listOf(
|
||||
rememberSaveableStateHolderNavEntryDecorator(),
|
||||
rememberViewModelStoreNavEntryDecorator(),
|
||||
),
|
||||
entryProvider = appEntryProvider
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LoginScreen()
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package hu.bbara.purefin.navigation
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibilityScope
|
||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||
import androidx.compose.animation.SharedTransitionScope
|
||||
import androidx.compose.runtime.ProvidableCompositionLocal
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
@@ -8,3 +11,15 @@ val LocalNavigationManager: ProvidableCompositionLocal<NavigationManager> =
|
||||
staticCompositionLocalOf { error("NavigationManager not provided") }
|
||||
|
||||
val LocalNavigationBackStack = compositionLocalOf<List<Route>> { emptyList() }
|
||||
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
val LocalSharedTransitionScope = compositionLocalOf<SharedTransitionScope?> { null }
|
||||
|
||||
val LocalNavSharedAnimatedVisibilityScope =
|
||||
compositionLocalOf<AnimatedVisibilityScope?> { null }
|
||||
|
||||
val LocalHomeMediaSharedBoundsKey = compositionLocalOf<String?> { null }
|
||||
|
||||
val LocalSetHomeMediaSharedBoundsKey = staticCompositionLocalOf<(String) -> Unit> { {} }
|
||||
|
||||
const val HOME_SEARCH_SHARED_BOUNDS_KEY = "home_search_shared_bounds"
|
||||
|
||||
@@ -1,25 +1,51 @@
|
||||
package hu.bbara.purefin.navigation
|
||||
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.navigation3.runtime.EntryProviderScope
|
||||
import androidx.navigation3.ui.LocalNavAnimatedContentScope
|
||||
import hu.bbara.purefin.ui.screen.library.LibraryScreen
|
||||
import hu.bbara.purefin.ui.screen.login.LoginScreen
|
||||
import hu.bbara.purefin.ui.screen.AppScreen
|
||||
import hu.bbara.purefin.ui.screen.episode.EpisodeScreen
|
||||
import hu.bbara.purefin.ui.screen.home.components.search.HomeSearchFullScreen
|
||||
import hu.bbara.purefin.ui.screen.movie.MovieScreen
|
||||
import hu.bbara.purefin.ui.screen.series.SeriesScreen
|
||||
|
||||
fun EntryProviderScope<Route>.appRouteEntryBuilder() {
|
||||
entry<Route.Home> {
|
||||
AppScreen()
|
||||
CompositionLocalProvider(
|
||||
LocalNavSharedAnimatedVisibilityScope provides LocalNavAnimatedContentScope.current
|
||||
) {
|
||||
AppScreen()
|
||||
}
|
||||
}
|
||||
entry<Route.HomeSearchRoute> {
|
||||
CompositionLocalProvider(
|
||||
LocalNavSharedAnimatedVisibilityScope provides LocalNavAnimatedContentScope.current
|
||||
) {
|
||||
HomeSearchFullScreen()
|
||||
}
|
||||
}
|
||||
entry<Route.MovieRoute> {
|
||||
MovieScreen(movie = it.item)
|
||||
CompositionLocalProvider(
|
||||
LocalNavSharedAnimatedVisibilityScope provides LocalNavAnimatedContentScope.current
|
||||
) {
|
||||
MovieScreen(movie = it.item)
|
||||
}
|
||||
}
|
||||
entry<Route.SeriesRoute> {
|
||||
SeriesScreen(series = it.item)
|
||||
CompositionLocalProvider(
|
||||
LocalNavSharedAnimatedVisibilityScope provides LocalNavAnimatedContentScope.current
|
||||
) {
|
||||
SeriesScreen(series = it.item)
|
||||
}
|
||||
}
|
||||
entry<Route.EpisodeRoute> {
|
||||
EpisodeScreen(episode = it.item)
|
||||
CompositionLocalProvider(
|
||||
LocalNavSharedAnimatedVisibilityScope provides LocalNavAnimatedContentScope.current
|
||||
) {
|
||||
EpisodeScreen(episode = it.item)
|
||||
}
|
||||
}
|
||||
entry<Route.LibraryRoute> {
|
||||
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()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -77,6 +77,7 @@ fun AppScreen(
|
||||
onProfileClick = {},
|
||||
onSettingsClick = {},
|
||||
onLogoutClick = viewModel::logout,
|
||||
onSearchClick = viewModel::openSearch,
|
||||
selectedTab = selectedTab,
|
||||
onTabSelected = onTabSelected,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
@@ -86,6 +87,7 @@ fun AppScreen(
|
||||
LibrariesScreen(
|
||||
items = libraries,
|
||||
onLibrarySelected = { item -> viewModel.onLibrarySelected(item.id, item.name) },
|
||||
onSearchClick = viewModel::openSearch,
|
||||
selectedTab = selectedTab,
|
||||
onTabSelected = onTabSelected,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
|
||||
@@ -34,6 +34,8 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
||||
import hu.bbara.purefin.ui.common.media.MediaHero
|
||||
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.image.ImageUrlBuilder
|
||||
import hu.bbara.purefin.navigation.EpisodeDto
|
||||
@@ -117,17 +119,20 @@ private fun EpisodeScreenInternal(
|
||||
onDownloadClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val isHomeMediaTransitionActive = isHomeMediaSharedBoundsTransitionActive()
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
topBar = {
|
||||
EpisodeTopBar(
|
||||
shortcut = topBarShortcut,
|
||||
onBack = onBack,
|
||||
onSeriesClick = onSeriesClick,
|
||||
modifier = Modifier
|
||||
)
|
||||
if (!isHomeMediaTransitionActive) {
|
||||
EpisodeTopBar(
|
||||
shortcut = topBarShortcut,
|
||||
onBack = onBack,
|
||||
onSeriesClick = onSeriesClick,
|
||||
modifier = Modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
@@ -136,15 +141,17 @@ private fun EpisodeScreenInternal(
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
EpisodeHeroSection(episode = episode)
|
||||
EpisodeDetails(
|
||||
episode = episode,
|
||||
downloadState = downloadState,
|
||||
onDownloadClick = onDownloadClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = innerPadding.calculateBottomPadding())
|
||||
)
|
||||
if (!isHomeMediaTransitionActive) {
|
||||
EpisodeDetails(
|
||||
episode = episode,
|
||||
downloadState = downloadState,
|
||||
onDownloadClick = onDownloadClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = innerPadding.calculateBottomPadding())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,6 +167,7 @@ private fun EpisodeHeroSection(
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.homeMediaSharedBoundsDestination()
|
||||
.fillMaxWidth()
|
||||
.height(sectionHeight)
|
||||
) {
|
||||
|
||||
@@ -1,26 +1,18 @@
|
||||
package hu.bbara.purefin.ui.screen.home
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import hu.bbara.purefin.ui.model.LibraryUiModel
|
||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||
import hu.bbara.purefin.ui.screen.AppBottomBar
|
||||
import hu.bbara.purefin.ui.screen.home.components.HomeContent
|
||||
import hu.bbara.purefin.ui.screen.home.components.HomeTopBar
|
||||
import hu.bbara.purefin.ui.screen.home.components.search.HomeSearchOverlay
|
||||
import java.util.UUID
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun HomeScreen(
|
||||
libraries: List<LibraryUiModel>,
|
||||
@@ -35,12 +27,11 @@ fun HomeScreen(
|
||||
onProfileClick: () -> Unit,
|
||||
onSettingsClick: () -> Unit,
|
||||
onLogoutClick: () -> Unit,
|
||||
onSearchClick: () -> Unit,
|
||||
selectedTab: Int,
|
||||
onTabSelected: (Int) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var isSearchVisible by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier
|
||||
.fillMaxSize(),
|
||||
@@ -48,7 +39,7 @@ fun HomeScreen(
|
||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||
topBar = {
|
||||
HomeTopBar(
|
||||
onSearchClick = { isSearchVisible = true },
|
||||
onSearchClick = onSearchClick,
|
||||
onProfileClick = onProfileClick,
|
||||
onSettingsClick = onSettingsClick,
|
||||
onLogoutClick = onLogoutClick
|
||||
@@ -61,37 +52,18 @@ fun HomeScreen(
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
HomeContent(
|
||||
libraries = libraries,
|
||||
libraryContent = libraryContent,
|
||||
suggestions = suggestions,
|
||||
continueWatching = continueWatching,
|
||||
nextUp = nextUp,
|
||||
isRefreshing = isRefreshing,
|
||||
onRefresh = onRefresh,
|
||||
onMediaSelected = onMediaSelected,
|
||||
onLibrarySelected = onLibrarySelected,
|
||||
onBrowseLibrariesClick = { onTabSelected(1) },
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
)
|
||||
HomeSearchOverlay(
|
||||
visible = isSearchVisible,
|
||||
topPadding = innerPadding.calculateTopPadding(),
|
||||
onDismiss = { isSearchVisible = false },
|
||||
onMovieSelected = {
|
||||
isSearchVisible = false
|
||||
//TODO use MediaUiModel as well
|
||||
//onMovieSelected(it)
|
||||
},
|
||||
onSeriesSelected = {
|
||||
isSearchVisible = false
|
||||
//onSeriesSelected(it)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
)
|
||||
}
|
||||
HomeContent(
|
||||
libraries = libraries,
|
||||
libraryContent = libraryContent,
|
||||
suggestions = suggestions,
|
||||
continueWatching = continueWatching,
|
||||
nextUp = nextUp,
|
||||
isRefreshing = isRefreshing,
|
||||
onRefresh = onRefresh,
|
||||
onMediaSelected = onMediaSelected,
|
||||
onLibrarySelected = onLibrarySelected,
|
||||
onBrowseLibrariesClick = { onTabSelected(1) },
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
package hu.bbara.purefin.ui.screen.home.components
|
||||
|
||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||
import androidx.compose.animation.SharedTransitionScope
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Search
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.IconButtonColors
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
@@ -18,6 +27,9 @@ import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import hu.bbara.purefin.navigation.HOME_SEARCH_SHARED_BOUNDS_KEY
|
||||
import hu.bbara.purefin.navigation.LocalNavSharedAnimatedVisibilityScope
|
||||
import hu.bbara.purefin.navigation.LocalSharedTransitionScope
|
||||
import hu.bbara.purefin.ui.common.image.PurefinLogo
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -58,3 +70,51 @@ fun DefaultTopBar(
|
||||
modifier = Modifier.padding(end = 12.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
fun DefaultTopBarSearchButton(
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
val sharedTransitionScope = LocalSharedTransitionScope.current
|
||||
val animatedVisibilityScope = LocalNavSharedAnimatedVisibilityScope.current
|
||||
|
||||
val searchButtonModifier = if (
|
||||
sharedTransitionScope != null &&
|
||||
animatedVisibilityScope != null
|
||||
) {
|
||||
with(sharedTransitionScope) {
|
||||
Modifier
|
||||
.sharedBounds(
|
||||
sharedContentState = rememberSharedContentState(
|
||||
key = HOME_SEARCH_SHARED_BOUNDS_KEY
|
||||
),
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
resizeMode = SharedTransitionScope.ResizeMode.scaleToBounds()
|
||||
)
|
||||
.size(50.dp)
|
||||
}
|
||||
} else {
|
||||
Modifier.size(50.dp)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = onClick,
|
||||
colors = IconButtonColors(
|
||||
containerColor = scheme.surface,
|
||||
contentColor = scheme.onSurface,
|
||||
disabledContainerColor = scheme.surface,
|
||||
disabledContentColor = scheme.onSurface
|
||||
),
|
||||
modifier = searchButtonModifier,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Search,
|
||||
contentDescription = "Search",
|
||||
modifier = Modifier.size(30.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||
import hu.bbara.purefin.ui.model.LibraryUiModel
|
||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||
import hu.bbara.purefin.ui.screen.home.components.continuewatching.ContinueWatchingSection
|
||||
import hu.bbara.purefin.ui.screen.home.components.featured.SuggestionsSection
|
||||
import hu.bbara.purefin.ui.screen.home.components.library.LibraryPosterSection
|
||||
|
||||
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Person
|
||||
import androidx.compose.material.icons.outlined.Search
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -35,22 +34,7 @@ fun HomeTopBar(
|
||||
|
||||
DefaultTopBar()
|
||||
{
|
||||
IconButton(
|
||||
onClick = onSearchClick,
|
||||
colors = IconButtonColors(
|
||||
containerColor = scheme.surface,
|
||||
contentColor = scheme.onSurface,
|
||||
disabledContainerColor = scheme.surface,
|
||||
disabledContentColor = scheme.onSurface
|
||||
),
|
||||
modifier = Modifier.size(50.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Search,
|
||||
contentDescription = "Search",
|
||||
modifier = Modifier.size(30.dp),
|
||||
)
|
||||
}
|
||||
DefaultTopBarSearchButton(onClick = onSearchClick)
|
||||
Spacer(modifier = Modifier.size(12.dp))
|
||||
IconButton(
|
||||
onClick = { isProfileMenuExpanded = true },
|
||||
@@ -97,4 +81,4 @@ fun HomeTopBar(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,17 +26,23 @@ import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||
import hu.bbara.purefin.ui.common.bar.MediaProgressBar
|
||||
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
|
||||
internal fun ContinueWatchingCard(
|
||||
item: MediaUiModel,
|
||||
sharedBoundsKey: String,
|
||||
onMediaSelected: (MediaUiModel) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
val onClick = rememberHomeMediaSharedBoundsClick(sharedBoundsKey) {
|
||||
onMediaSelected(item)
|
||||
}
|
||||
|
||||
Card(
|
||||
onClick = { onMediaSelected(item) },
|
||||
onClick = onClick,
|
||||
shape = RoundedCornerShape(26.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = scheme.surfaceContainer),
|
||||
modifier = modifier.width(280.dp)
|
||||
@@ -44,6 +50,7 @@ internal fun ContinueWatchingCard(
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.homeMediaSharedBoundsSource(sharedBoundsKey)
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(16f / 9f)
|
||||
.background(scheme.surfaceContainer)
|
||||
|
||||
@@ -3,16 +3,19 @@ package hu.bbara.purefin.ui.screen.home.components.continuewatching
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
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.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||
import hu.bbara.purefin.ui.common.header.SectionHeader
|
||||
import hu.bbara.purefin.ui.common.media.homeMediaSharedBoundsKey
|
||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||
|
||||
@Composable
|
||||
fun ContinueWatchingSection(
|
||||
@@ -33,15 +36,17 @@ fun ContinueWatchingSection(
|
||||
modifier = modifier
|
||||
) {
|
||||
SectionHeader(title = "Continue Watching")
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = listState
|
||||
) {
|
||||
items(items = items, key = { item -> item.id }) { item ->
|
||||
itemsIndexed(items = items, key = { _, item -> item.id }) { index, item ->
|
||||
ContinueWatchingCard(
|
||||
item = item,
|
||||
sharedBoundsKey = homeMediaSharedBoundsKey("continue-$index", item.id),
|
||||
onMediaSelected = onMediaSelected
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,18 +26,25 @@ import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||
import hu.bbara.purefin.ui.common.bar.MediaProgressBar
|
||||
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
|
||||
internal fun SuggestionCard(
|
||||
item: MediaUiModel,
|
||||
sharedBoundsKey: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
val description = item.description.trim()
|
||||
val onCardClick = rememberHomeMediaSharedBoundsClick(
|
||||
sharedBoundsKey = sharedBoundsKey,
|
||||
onClick = onClick
|
||||
)
|
||||
|
||||
ElevatedCard(
|
||||
onClick = onClick,
|
||||
onClick = onCardClick,
|
||||
colors = CardDefaults.elevatedCardColors(containerColor = scheme.surfaceContainerLow),
|
||||
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 1.dp),
|
||||
shape = RoundedCornerShape(30.dp),
|
||||
@@ -45,6 +52,7 @@ internal fun SuggestionCard(
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.homeMediaSharedBoundsSource(sharedBoundsKey)
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(16f / 11f)
|
||||
) {
|
||||
|
||||
@@ -22,6 +22,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.ui.common.media.homeMediaSharedBoundsKey
|
||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||
|
||||
@Composable
|
||||
@@ -49,9 +50,11 @@ fun SuggestionsSection(
|
||||
pageSpacing = 16.dp,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) { page ->
|
||||
val item = items[page]
|
||||
SuggestionCard(
|
||||
item = items[page],
|
||||
onClick = { onItemOpen(items[page]) }
|
||||
item = item,
|
||||
sharedBoundsKey = homeMediaSharedBoundsKey("suggestion-$page", item.id),
|
||||
onClick = { onItemOpen(item) }
|
||||
)
|
||||
}
|
||||
if (items.size > 1) {
|
||||
|
||||
@@ -25,6 +25,8 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.ui.common.badge.WatchStateBadge
|
||||
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.MediaUiModel
|
||||
import hu.bbara.purefin.ui.model.MovieUiModel
|
||||
@@ -32,13 +34,17 @@ import hu.bbara.purefin.ui.model.MovieUiModel
|
||||
@Composable
|
||||
internal fun HomeBrowseCard(
|
||||
uiModel: MediaUiModel,
|
||||
sharedBoundsKey: String,
|
||||
onMediaSelected: (MediaUiModel) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
val onClick = rememberHomeMediaSharedBoundsClick(sharedBoundsKey) {
|
||||
onMediaSelected(uiModel)
|
||||
}
|
||||
|
||||
Card(
|
||||
onClick = { onMediaSelected(uiModel) },
|
||||
onClick = onClick,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = scheme.surfaceContainer),
|
||||
modifier = modifier.width(188.dp)
|
||||
@@ -46,6 +52,7 @@ internal fun HomeBrowseCard(
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.homeMediaSharedBoundsSource(sharedBoundsKey)
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(16f / 10f)
|
||||
.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp))
|
||||
|
||||
@@ -3,17 +3,20 @@ package hu.bbara.purefin.ui.screen.home.components.library
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
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.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||
import hu.bbara.purefin.ui.model.LibraryUiModel
|
||||
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.MediaUiModel
|
||||
|
||||
@Composable
|
||||
fun LibraryPosterSection(
|
||||
@@ -40,15 +43,20 @@ fun LibraryPosterSection(
|
||||
actionLabel = "See all",
|
||||
onActionClick = { onLibrarySelected(library) }
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = listState
|
||||
) {
|
||||
items(items = items, key = { item -> item.id }) { item ->
|
||||
itemsIndexed(items = items, key = { _, item -> item.id }) { index, item ->
|
||||
HomeBrowseCard(
|
||||
uiModel = item,
|
||||
sharedBoundsKey = homeMediaSharedBoundsKey(
|
||||
origin = "library-${library.id}-$index",
|
||||
mediaId = item.id
|
||||
),
|
||||
onMediaSelected = onMediaSelected
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,17 +24,23 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||
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
|
||||
internal fun NextUpCard(
|
||||
uiModel: MediaUiModel,
|
||||
sharedBoundsKey: String,
|
||||
onMediaSelected: (MediaUiModel) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
val onClick = rememberHomeMediaSharedBoundsClick(sharedBoundsKey) {
|
||||
onMediaSelected(uiModel)
|
||||
}
|
||||
|
||||
Card(
|
||||
onClick = { onMediaSelected(uiModel) },
|
||||
onClick = onClick,
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = scheme.surfaceContainer),
|
||||
modifier = modifier.width(256.dp)
|
||||
@@ -42,6 +48,7 @@ internal fun NextUpCard(
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.homeMediaSharedBoundsSource(sharedBoundsKey)
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(16f / 10f)
|
||||
.background(scheme.surface)
|
||||
|
||||
@@ -3,16 +3,19 @@ package hu.bbara.purefin.ui.screen.home.components.nextup
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
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.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||
import hu.bbara.purefin.ui.common.header.SectionHeader
|
||||
import hu.bbara.purefin.ui.common.media.homeMediaSharedBoundsKey
|
||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||
|
||||
@Composable
|
||||
fun NextUpSection(
|
||||
@@ -33,15 +36,17 @@ fun NextUpSection(
|
||||
modifier = modifier
|
||||
) {
|
||||
SectionHeader(title = "Next Up")
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = listState
|
||||
) {
|
||||
items(items = items, key = { item -> item.id }) { item ->
|
||||
itemsIndexed(items = items, key = { _, item -> item.id }) { index, item ->
|
||||
NextUpCard(
|
||||
uiModel = item,
|
||||
sharedBoundsKey = homeMediaSharedBoundsKey("next-up-$index", item.id),
|
||||
onMediaSelected = onMediaSelected
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package hu.bbara.purefin.ui.screen.home.components.search
|
||||
|
||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||
import androidx.compose.animation.SharedTransitionScope
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -51,17 +55,41 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||
import hu.bbara.purefin.ui.common.image.PurefinLogo
|
||||
import hu.bbara.purefin.navigation.HOME_SEARCH_SHARED_BOUNDS_KEY
|
||||
import hu.bbara.purefin.navigation.LocalNavSharedAnimatedVisibilityScope
|
||||
import hu.bbara.purefin.navigation.LocalSharedTransitionScope
|
||||
import hu.bbara.purefin.ui.theme.AppTheme
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@OptIn(ExperimentalLayoutApi::class, ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
fun HomeSearchFullScreen(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
val sharedTransitionScope = LocalSharedTransitionScope.current
|
||||
val animatedVisibilityScope = LocalNavSharedAnimatedVisibilityScope.current
|
||||
val sharedBoundsModifier = if (
|
||||
sharedTransitionScope != null &&
|
||||
animatedVisibilityScope != null
|
||||
) {
|
||||
with(sharedTransitionScope) {
|
||||
Modifier.sharedBounds(
|
||||
sharedContentState = rememberSharedContentState(
|
||||
key = HOME_SEARCH_SHARED_BOUNDS_KEY
|
||||
),
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
resizeMode = SharedTransitionScope.ResizeMode.scaleToBounds()
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.then(sharedBoundsModifier)
|
||||
.fillMaxSize()
|
||||
.background(scheme.background)
|
||||
.statusBarsPadding()
|
||||
|
||||
@@ -15,6 +15,7 @@ import hu.bbara.purefin.ui.screen.library.components.LibraryTopBar
|
||||
fun LibrariesScreen(
|
||||
items: List<LibraryUiModel>,
|
||||
onLibrarySelected: (LibraryUiModel) -> Unit,
|
||||
onSearchClick: () -> Unit,
|
||||
selectedTab: Int,
|
||||
onTabSelected: (Int) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
@@ -25,7 +26,7 @@ fun LibrariesScreen(
|
||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||
topBar = {
|
||||
LibraryTopBar(
|
||||
onSearchClick = {},
|
||||
onSearchClick = onSearchClick,
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
|
||||
@@ -1,39 +1,14 @@
|
||||
package hu.bbara.purefin.ui.screen.library.components
|
||||
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Search
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.IconButtonColors
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.ui.screen.home.components.DefaultTopBar
|
||||
import hu.bbara.purefin.ui.screen.home.components.DefaultTopBarSearchButton
|
||||
|
||||
@Composable
|
||||
fun LibraryTopBar(
|
||||
onSearchClick: () -> Unit,
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
|
||||
DefaultTopBar {
|
||||
IconButton(
|
||||
onClick = onSearchClick,
|
||||
colors = IconButtonColors(
|
||||
containerColor = scheme.surface,
|
||||
contentColor = scheme.onSurface,
|
||||
disabledContainerColor = scheme.surface,
|
||||
disabledContentColor = scheme.onSurface
|
||||
),
|
||||
modifier = Modifier.size(50.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Search,
|
||||
contentDescription = "Search",
|
||||
modifier = Modifier.size(30.dp),
|
||||
)
|
||||
}
|
||||
DefaultTopBarSearchButton(onClick = onSearchClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ import hu.bbara.purefin.model.Movie
|
||||
import hu.bbara.purefin.navigation.MovieDto
|
||||
import hu.bbara.purefin.ui.common.media.MediaHero
|
||||
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.MovieTopBar
|
||||
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
||||
@@ -94,14 +96,18 @@ private fun MovieScreenInternal(
|
||||
onBack: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val isHomeMediaTransitionActive = isHomeMediaSharedBoundsTransitionActive()
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
topBar = {
|
||||
MovieTopBar(
|
||||
onBack = onBack,
|
||||
modifier = Modifier
|
||||
)
|
||||
if (!isHomeMediaTransitionActive) {
|
||||
MovieTopBar(
|
||||
onBack = onBack,
|
||||
modifier = Modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
@@ -110,15 +116,17 @@ private fun MovieScreenInternal(
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
MediaHeroSection(movie = movie)
|
||||
MovieDetails(
|
||||
movie = movie,
|
||||
downloadState = downloadState,
|
||||
onDownloadClick = onDownloadClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = innerPadding.calculateBottomPadding())
|
||||
)
|
||||
if (!isHomeMediaTransitionActive) {
|
||||
MovieDetails(
|
||||
movie = movie,
|
||||
downloadState = downloadState,
|
||||
onDownloadClick = onDownloadClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = innerPadding.calculateBottomPadding())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,6 +142,7 @@ fun MediaHeroSection(
|
||||
|
||||
Box (
|
||||
modifier = modifier
|
||||
.homeMediaSharedBoundsDestination()
|
||||
.height(sectionHeight)
|
||||
) {
|
||||
MediaHero(
|
||||
|
||||
@@ -32,16 +32,13 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.ui.AspectRatioFrameLayout
|
||||
import androidx.media3.ui.PlayerView
|
||||
import androidx.media3.ui.SubtitleView
|
||||
import hu.bbara.purefin.player.viewmodel.PlayerViewModel
|
||||
import hu.bbara.purefin.ui.common.button.PurefinTextButton
|
||||
import hu.bbara.purefin.ui.common.visual.EmptyValueTimedVisibility
|
||||
import hu.bbara.purefin.ui.common.visual.ValueChangeTimedVisibility
|
||||
import hu.bbara.purefin.ui.screen.player.components.PersistentOverlayContainer
|
||||
@@ -50,6 +47,7 @@ import hu.bbara.purefin.ui.screen.player.components.PlayerControlsOverlay
|
||||
import hu.bbara.purefin.ui.screen.player.components.PlayerGesturesLayer
|
||||
import hu.bbara.purefin.ui.screen.player.components.PlayerLoadingErrorEndCard
|
||||
import hu.bbara.purefin.ui.screen.player.components.PlayerQueuePanel
|
||||
import hu.bbara.purefin.ui.screen.player.components.SkipSegmentButton
|
||||
import hu.bbara.purefin.ui.screen.player.components.rememberPersistentOverlayController
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.roundToInt
|
||||
@@ -216,16 +214,7 @@ fun PlayerScreen(
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(end = 24.dp, bottom = 24.dp)
|
||||
) {
|
||||
PurefinTextButton(
|
||||
onClick = { viewModel.skipActiveSegment() },
|
||||
modifier = Modifier.padding(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Skip",
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
)
|
||||
}
|
||||
SkipSegmentButton(onClick = { viewModel.skipActiveSegment() })
|
||||
}
|
||||
|
||||
PlayerLoadingErrorEndCard(
|
||||
|
||||
@@ -10,7 +10,11 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.ArrowBack
|
||||
import androidx.compose.material.icons.outlined.Cast
|
||||
@@ -22,6 +26,9 @@ import androidx.compose.material.icons.outlined.PlaylistPlay
|
||||
import androidx.compose.material.icons.outlined.Replay10
|
||||
import androidx.compose.material.icons.outlined.SkipNext
|
||||
import androidx.compose.material.icons.outlined.SkipPrevious
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -35,12 +42,10 @@ import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import hu.bbara.purefin.player.model.PlayerUiState
|
||||
import hu.bbara.purefin.player.model.TrackOption
|
||||
import hu.bbara.purefin.ui.common.button.GhostIconButton
|
||||
import hu.bbara.purefin.ui.common.button.PurefinIconButton
|
||||
import hu.bbara.purefin.ui.common.button.PurefinTextButton
|
||||
|
||||
@Composable
|
||||
fun PlayerControlsOverlay(
|
||||
@@ -174,26 +179,6 @@ private fun BottomSection(
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
Column(modifier = modifier) {
|
||||
if (uiState.activeSkippableSegmentEndMs != null) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
PurefinTextButton(
|
||||
onClick = onSkipSegment,
|
||||
modifier = Modifier.padding(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Skip",
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -279,7 +264,7 @@ private fun BottomSection(
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.align(Alignment.CenterEnd),
|
||||
modifier = Modifier.align(Alignment.CenterStart),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
@@ -302,11 +287,48 @@ private fun BottomSection(
|
||||
overlayController = overlayController
|
||||
)
|
||||
}
|
||||
if (uiState.activeSkippableSegmentEndMs != null) {
|
||||
SkipSegmentButton(
|
||||
onClick = onSkipSegment,
|
||||
modifier = Modifier.align(Alignment.CenterEnd)
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SkipSegmentButton(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
|
||||
FilledTonalButton(
|
||||
onClick = onClick,
|
||||
modifier = modifier.heightIn(min = 44.dp),
|
||||
shape = RoundedCornerShape(50),
|
||||
colors = ButtonDefaults.filledTonalButtonColors(
|
||||
containerColor = scheme.secondary.copy(alpha = 0.92f),
|
||||
contentColor = scheme.onSecondary
|
||||
),
|
||||
contentPadding = ButtonDefaults.ButtonWithIconContentPadding
|
||||
) {
|
||||
Text(
|
||||
text = "Skip",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.SkipNext,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatTime(positionMs: Long): String {
|
||||
val totalSeconds = positionMs / 1000
|
||||
val seconds = (totalSeconds % 60).toInt()
|
||||
|
||||
@@ -41,6 +41,8 @@ import hu.bbara.purefin.model.Series
|
||||
import hu.bbara.purefin.navigation.SeriesDto
|
||||
import hu.bbara.purefin.ui.common.media.MediaHero
|
||||
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.EpisodeCarousel
|
||||
import hu.bbara.purefin.ui.screen.series.components.SeasonTabs
|
||||
@@ -103,6 +105,7 @@ private fun SeriesScreenInternal(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
val isHomeMediaTransitionActive = isHomeMediaSharedBoundsTransitionActive()
|
||||
|
||||
fun getDefaultSeason() : Season {
|
||||
for (season in series.seasons) {
|
||||
@@ -131,10 +134,12 @@ private fun SeriesScreenInternal(
|
||||
modifier = modifier,
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
topBar = {
|
||||
SeriesTopBar(
|
||||
onBack = onBack,
|
||||
modifier = Modifier
|
||||
)
|
||||
if (!isHomeMediaTransitionActive) {
|
||||
SeriesTopBar(
|
||||
onBack = onBack,
|
||||
modifier = Modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
@@ -143,48 +148,50 @@ private fun SeriesScreenInternal(
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
SeriesHeroSection(series = series)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = innerPadding.calculateBottomPadding())
|
||||
) {
|
||||
SeriesActionButtons(
|
||||
nextUpEpisode = nextUpEpisode,
|
||||
seriesDownloadState = seriesDownloadState,
|
||||
selectedSeason = selectedSeason,
|
||||
seasonDownloadState = seasonDownloadState,
|
||||
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
|
||||
if (!isHomeMediaTransitionActive) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = innerPadding.calculateBottomPadding())
|
||||
) {
|
||||
SeriesActionButtons(
|
||||
nextUpEpisode = nextUpEpisode,
|
||||
seriesDownloadState = seriesDownloadState,
|
||||
selectedSeason = selectedSeason,
|
||||
seasonDownloadState = seasonDownloadState,
|
||||
onDownloadOptionSelected = { option ->
|
||||
onDownloadOptionSelected(option, selectedSeason)
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
CastRow(cast = series.cast)
|
||||
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))
|
||||
CastRow(cast = series.cast)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,6 +209,7 @@ private fun SeriesHeroSection(
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.homeMediaSharedBoundsDestination()
|
||||
.fillMaxWidth()
|
||||
.height(sectionHeight)
|
||||
) {
|
||||
|
||||
@@ -19,6 +19,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
internal fun CircularIconButton(
|
||||
@@ -47,6 +48,7 @@ internal fun CircularIconButton(
|
||||
targetValue = if (isFocused) focusedBackgroundColor else containerColor,
|
||||
label = "background"
|
||||
)
|
||||
val iconSize = (size - 24.dp).coerceAtLeast(0.dp)
|
||||
|
||||
FilledIconButton(
|
||||
onClick = onClick,
|
||||
@@ -67,7 +69,8 @@ internal fun CircularIconButton(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = contentDescription
|
||||
contentDescription = contentDescription,
|
||||
modifier = Modifier.size(iconSize)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,6 +251,10 @@ class AppViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun openSearch() {
|
||||
navigationManager.navigate(Route.HomeSearchRoute)
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
viewModelScope.launch {
|
||||
userSessionRepository.setLoggedIn(false)
|
||||
|
||||
@@ -8,6 +8,9 @@ sealed interface Route : NavKey {
|
||||
@Serializable
|
||||
data object Home : Route
|
||||
|
||||
@Serializable
|
||||
data object HomeSearchRoute : Route
|
||||
|
||||
@Serializable
|
||||
data class MovieRoute(val item: MovieDto) : Route
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ class EpisodeUiModel : MediaUiModel {
|
||||
constructor(episode: Episode) {
|
||||
id = episode.id
|
||||
primaryText = episode.seriesName
|
||||
secondaryText = "${episode.seasonIndex} x ${episode.index} : ${episode.title}"
|
||||
secondaryText = "S${episode.seasonIndex.toString().padStart(2, '0')}xE${episode.index.toString().padStart(2, '0')} : ${episode.title}"
|
||||
description = episode.synopsis
|
||||
prefixImageUrl = episode.imageUrlPrefix
|
||||
progress = (episode.progress?.toFloat() ?: 0f) / 100f
|
||||
|
||||
Reference in New Issue
Block a user