Compare commits

..

10 Commits

31 changed files with 568 additions and 318 deletions

View File

@@ -6,7 +6,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -55,22 +54,21 @@ internal fun TvIconButton(
targetValue = if (isFocused) scheme.primary else Color.Transparent, targetValue = if (isFocused) scheme.primary else Color.Transparent,
label = "border" label = "border"
) )
val shape = RoundedCornerShape(50)
val iconSize = (size - 24).coerceAtLeast(0).dp
Box( val buttonModifier = modifier
modifier = modifier
.graphicsLayer { .graphicsLayer {
scaleX = scale scaleX = scale
scaleY = scale scaleY = scale
} }
.alpha(if (enabled) 1f else 0.4f) .alpha(if (enabled) 1f else 0.4f)
.widthIn(min = if (label == null) size.dp else 104.dp)
.height(size.dp)
.border( .border(
width = if (isFocused) 2.dp else 0.dp, width = if (isFocused) 2.dp else 0.dp,
color = borderColor, color = borderColor,
shape = RoundedCornerShape(50) shape = shape
) )
.clip(RoundedCornerShape(50)) .clip(shape)
.background( .background(
if (isFocused) scheme.primary.copy(alpha = 0.5f) if (isFocused) scheme.primary.copy(alpha = 0.5f)
else scheme.background.copy(alpha = 0.65f) else scheme.background.copy(alpha = 0.65f)
@@ -82,27 +80,32 @@ internal fun TvIconButton(
} }
} }
.onFocusChanged { isFocused = it.isFocused } .onFocusChanged { isFocused = it.isFocused }
.clickable(enabled = enabled) { onClick() }, .clickable(enabled = enabled) { onClick() }
contentAlignment = Alignment.Center
) {
if (label == null) { if (label == null) {
Icon( Icon(
imageVector = icon, imageVector = icon,
contentDescription = contentDescription, contentDescription = contentDescription,
tint = scheme.onBackground, tint = scheme.onBackground,
modifier = Modifier.padding(8.dp) modifier = buttonModifier
.size(size.dp)
.padding(12.dp)
.size(iconSize)
) )
} else { } else {
Row( Row(
horizontalArrangement = Arrangement.spacedBy(6.dp), horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 16.dp) modifier = buttonModifier
.widthIn(min = 104.dp)
.height(size.dp)
.padding(horizontal = 16.dp)
) { ) {
Icon( Icon(
imageVector = icon, imageVector = icon,
contentDescription = contentDescription, contentDescription = contentDescription,
tint = scheme.onBackground, tint = scheme.onBackground,
modifier = Modifier.size(28.dp) modifier = Modifier.size(iconSize)
) )
Text( Text(
text = label, text = label,
@@ -113,4 +116,3 @@ internal fun TvIconButton(
} }
} }
} }
}

View File

@@ -258,21 +258,21 @@ private fun TvPlayerBottomSection(
icon = Icons.Outlined.SkipPrevious, icon = Icons.Outlined.SkipPrevious,
contentDescription = "Previous", contentDescription = "Previous",
onClick = onPrevious, onClick = onPrevious,
size = 64, size = 56,
modifier = expandPlaylistModifier modifier = expandPlaylistModifier
) )
TvIconButton( TvIconButton(
icon = Icons.Outlined.Replay10, icon = Icons.Outlined.Replay10,
contentDescription = "Seek backward 10 seconds", contentDescription = "Seek backward 10 seconds",
onClick = { onSeekRelative(-10_000) }, onClick = { onSeekRelative(-10_000) },
size = 64, size = 56,
modifier = expandPlaylistModifier modifier = expandPlaylistModifier
) )
TvIconButton( TvIconButton(
icon = if (uiState.isPlaying) Icons.Outlined.Pause else Icons.Outlined.PlayArrow, icon = if (uiState.isPlaying) Icons.Outlined.Pause else Icons.Outlined.PlayArrow,
contentDescription = if (uiState.isPlaying) "Pause" else "Play", contentDescription = if (uiState.isPlaying) "Pause" else "Play",
onClick = onPlayPause, onClick = onPlayPause,
size = 72, size = 64,
modifier = expandPlaylistModifier modifier = expandPlaylistModifier
.focusRequester(focusRequester) .focusRequester(focusRequester)
.testTag(TvPlayerPlayPauseButtonTag) .testTag(TvPlayerPlayPauseButtonTag)
@@ -281,14 +281,14 @@ private fun TvPlayerBottomSection(
icon = Icons.Outlined.Forward30, icon = Icons.Outlined.Forward30,
contentDescription = "Seek forward 30 seconds", contentDescription = "Seek forward 30 seconds",
onClick = { onSeekRelative(30_000) }, onClick = { onSeekRelative(30_000) },
size = 64, size = 56,
modifier = expandPlaylistModifier modifier = expandPlaylistModifier
) )
TvIconButton( TvIconButton(
icon = Icons.Outlined.SkipNext, icon = Icons.Outlined.SkipNext,
contentDescription = "Next", contentDescription = "Next",
onClick = onNext, onClick = onNext,
size = 64, size = 56,
modifier = expandPlaylistModifier modifier = expandPlaylistModifier
) )
} }

View File

@@ -6,6 +6,8 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.core.tween 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.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith import androidx.compose.animation.togetherWith
@@ -39,8 +41,11 @@ 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.NavigationCommand import hu.bbara.purefin.navigation.NavigationCommand
import hu.bbara.purefin.navigation.NavigationManager import hu.bbara.purefin.navigation.NavigationManager
import hu.bbara.purefin.navigation.Route import hu.bbara.purefin.navigation.Route
@@ -131,6 +136,7 @@ class PurefinActivity : ComponentActivity() {
return (applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0 return (applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0
} }
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable @Composable
fun MainApp( fun MainApp(
userSessionRepository: UserSessionRepository, userSessionRepository: UserSessionRepository,
@@ -154,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() }
@@ -172,9 +179,15 @@ class PurefinActivity : ComponentActivity() {
} }
} }
SharedTransitionLayout {
CompositionLocalProvider( CompositionLocalProvider(
LocalNavigationManager provides navigationManager, LocalNavigationManager provides navigationManager,
LocalNavigationBackStack provides backStack.toList() LocalNavigationBackStack provides backStack.toList(),
LocalSharedTransitionScope provides this,
LocalHomeMediaSharedBoundsKey provides homeMediaSharedBoundsKey,
LocalSetHomeMediaSharedBoundsKey provides { key ->
homeMediaSharedBoundsKey = key
}
) { ) {
NavDisplay( NavDisplay(
backStack = backStack, backStack = backStack,
@@ -212,6 +225,7 @@ class PurefinActivity : ComponentActivity() {
entryProvider = appEntryProvider entryProvider = appEntryProvider
) )
} }
}
} else { } else {
LoginScreen() LoginScreen()
} }

View File

@@ -1,5 +1,8 @@
package hu.bbara.purefin.navigation 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.ProvidableCompositionLocal
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.runtime.staticCompositionLocalOf
@@ -8,3 +11,15 @@ val LocalNavigationManager: ProvidableCompositionLocal<NavigationManager> =
staticCompositionLocalOf { error("NavigationManager not provided") } staticCompositionLocalOf { error("NavigationManager not provided") }
val LocalNavigationBackStack = compositionLocalOf<List<Route>> { emptyList() } 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"

View File

@@ -1,26 +1,52 @@
package hu.bbara.purefin.navigation package hu.bbara.purefin.navigation
import androidx.compose.runtime.CompositionLocalProvider
import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.ui.LocalNavAnimatedContentScope
import hu.bbara.purefin.ui.screen.library.LibraryScreen import hu.bbara.purefin.ui.screen.library.LibraryScreen
import hu.bbara.purefin.ui.screen.login.LoginScreen import hu.bbara.purefin.ui.screen.login.LoginScreen
import hu.bbara.purefin.ui.screen.AppScreen import hu.bbara.purefin.ui.screen.AppScreen
import hu.bbara.purefin.ui.screen.episode.EpisodeScreen 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.movie.MovieScreen
import hu.bbara.purefin.ui.screen.series.SeriesScreen import hu.bbara.purefin.ui.screen.series.SeriesScreen
fun EntryProviderScope<Route>.appRouteEntryBuilder() { fun EntryProviderScope<Route>.appRouteEntryBuilder() {
entry<Route.Home> { entry<Route.Home> {
CompositionLocalProvider(
LocalNavSharedAnimatedVisibilityScope provides LocalNavAnimatedContentScope.current
) {
AppScreen() AppScreen()
} }
}
entry<Route.HomeSearchRoute> {
CompositionLocalProvider(
LocalNavSharedAnimatedVisibilityScope provides LocalNavAnimatedContentScope.current
) {
HomeSearchFullScreen()
}
}
entry<Route.MovieRoute> { entry<Route.MovieRoute> {
CompositionLocalProvider(
LocalNavSharedAnimatedVisibilityScope provides LocalNavAnimatedContentScope.current
) {
MovieScreen(movie = it.item) MovieScreen(movie = it.item)
} }
}
entry<Route.SeriesRoute> { entry<Route.SeriesRoute> {
CompositionLocalProvider(
LocalNavSharedAnimatedVisibilityScope provides LocalNavAnimatedContentScope.current
) {
SeriesScreen(series = it.item) SeriesScreen(series = it.item)
} }
}
entry<Route.EpisodeRoute> { entry<Route.EpisodeRoute> {
CompositionLocalProvider(
LocalNavSharedAnimatedVisibilityScope provides LocalNavAnimatedContentScope.current
) {
EpisodeScreen(episode = it.item) EpisodeScreen(episode = it.item)
} }
}
entry<Route.LibraryRoute> { entry<Route.LibraryRoute> {
LibraryScreen(library = it.library) LibraryScreen(library = it.library)
} }

View File

@@ -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()
)
}
}

View File

@@ -77,6 +77,7 @@ fun AppScreen(
onProfileClick = {}, onProfileClick = {},
onSettingsClick = {}, onSettingsClick = {},
onLogoutClick = viewModel::logout, onLogoutClick = viewModel::logout,
onSearchClick = viewModel::openSearch,
selectedTab = selectedTab, selectedTab = selectedTab,
onTabSelected = onTabSelected, onTabSelected = onTabSelected,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
@@ -86,6 +87,7 @@ fun AppScreen(
LibrariesScreen( LibrariesScreen(
items = libraries, items = libraries,
onLibrarySelected = { item -> viewModel.onLibrarySelected(item.id, item.name) }, onLibrarySelected = { item -> viewModel.onLibrarySelected(item.id, item.name) },
onSearchClick = viewModel::openSearch,
selectedTab = selectedTab, selectedTab = selectedTab,
onTabSelected = onTabSelected, onTabSelected = onTabSelected,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()

View File

@@ -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,11 +119,13 @@ 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 = {
if (!isHomeMediaTransitionActive) {
EpisodeTopBar( EpisodeTopBar(
shortcut = topBarShortcut, shortcut = topBarShortcut,
onBack = onBack, onBack = onBack,
@@ -129,6 +133,7 @@ private fun EpisodeScreenInternal(
modifier = Modifier modifier = Modifier
) )
} }
}
) { innerPadding -> ) { innerPadding ->
Column( Column(
modifier = Modifier modifier = Modifier
@@ -136,6 +141,7 @@ private fun EpisodeScreenInternal(
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
EpisodeHeroSection(episode = episode) EpisodeHeroSection(episode = episode)
if (!isHomeMediaTransitionActive) {
EpisodeDetails( EpisodeDetails(
episode = episode, episode = episode,
downloadState = downloadState, downloadState = downloadState,
@@ -148,6 +154,7 @@ private fun EpisodeScreenInternal(
} }
} }
} }
}
@Composable @Composable
private fun EpisodeHeroSection( private fun EpisodeHeroSection(
@@ -160,6 +167,7 @@ private fun EpisodeHeroSection(
Box( Box(
modifier = modifier modifier = modifier
.homeMediaSharedBoundsDestination()
.fillMaxWidth() .fillMaxWidth()
.height(sectionHeight) .height(sectionHeight)
) { ) {

View File

@@ -1,26 +1,18 @@
package hu.bbara.purefin.ui.screen.home package hu.bbara.purefin.ui.screen.home
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable 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 androidx.compose.ui.Modifier
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
import hu.bbara.purefin.ui.screen.AppBottomBar import hu.bbara.purefin.ui.screen.AppBottomBar
import hu.bbara.purefin.ui.screen.home.components.HomeContent 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.HomeTopBar
import hu.bbara.purefin.ui.screen.home.components.search.HomeSearchOverlay
import java.util.UUID import java.util.UUID
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun HomeScreen( fun HomeScreen(
libraries: List<LibraryUiModel>, libraries: List<LibraryUiModel>,
@@ -35,12 +27,11 @@ fun HomeScreen(
onProfileClick: () -> Unit, onProfileClick: () -> Unit,
onSettingsClick: () -> Unit, onSettingsClick: () -> Unit,
onLogoutClick: () -> Unit, onLogoutClick: () -> Unit,
onSearchClick: () -> Unit,
selectedTab: Int, selectedTab: Int,
onTabSelected: (Int) -> Unit, onTabSelected: (Int) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
var isSearchVisible by rememberSaveable { mutableStateOf(false) }
Scaffold( Scaffold(
modifier = modifier modifier = modifier
.fillMaxSize(), .fillMaxSize(),
@@ -48,7 +39,7 @@ fun HomeScreen(
contentColor = MaterialTheme.colorScheme.onBackground, contentColor = MaterialTheme.colorScheme.onBackground,
topBar = { topBar = {
HomeTopBar( HomeTopBar(
onSearchClick = { isSearchVisible = true }, onSearchClick = onSearchClick,
onProfileClick = onProfileClick, onProfileClick = onProfileClick,
onSettingsClick = onSettingsClick, onSettingsClick = onSettingsClick,
onLogoutClick = onLogoutClick onLogoutClick = onLogoutClick
@@ -61,7 +52,6 @@ fun HomeScreen(
) )
} }
) { innerPadding -> ) { innerPadding ->
Box(modifier = Modifier.fillMaxSize()) {
HomeContent( HomeContent(
libraries = libraries, libraries = libraries,
libraryContent = libraryContent, libraryContent = libraryContent,
@@ -75,23 +65,5 @@ fun HomeScreen(
onBrowseLibrariesClick = { onTabSelected(1) }, onBrowseLibrariesClick = { onTabSelected(1) },
modifier = Modifier.padding(innerPadding) 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)
)
}
} }
} }

View File

@@ -1,11 +1,20 @@
package hu.bbara.purefin.ui.screen.home.components 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.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size 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.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.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar 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.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp 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 import hu.bbara.purefin.ui.common.image.PurefinLogo
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -58,3 +70,51 @@ fun DefaultTopBar(
modifier = Modifier.padding(end = 12.dp) 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),
)
}
}

View File

@@ -21,8 +21,8 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
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.model.MediaUiModel
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.screen.home.components.continuewatching.ContinueWatchingSection 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.featured.SuggestionsSection
import hu.bbara.purefin.ui.screen.home.components.library.LibraryPosterSection import hu.bbara.purefin.ui.screen.home.components.library.LibraryPosterSection

View File

@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Person import androidx.compose.material.icons.outlined.Person
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -35,22 +34,7 @@ fun HomeTopBar(
DefaultTopBar() DefaultTopBar()
{ {
IconButton( DefaultTopBarSearchButton(onClick = onSearchClick)
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),
)
}
Spacer(modifier = Modifier.size(12.dp)) Spacer(modifier = Modifier.size(12.dp))
IconButton( IconButton(
onClick = { isProfileMenuExpanded = true }, onClick = { isProfileMenuExpanded = true },

View File

@@ -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)

View File

@@ -3,16 +3,19 @@ package hu.bbara.purefin.ui.screen.home.components.continuewatching
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
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.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.model.MediaUiModel
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
@Composable @Composable
fun ContinueWatchingSection( fun ContinueWatchingSection(
@@ -33,15 +36,17 @@ fun ContinueWatchingSection(
modifier = modifier modifier = modifier
) { ) {
SectionHeader(title = "Continue Watching") SectionHeader(title = "Continue Watching")
Spacer(modifier = Modifier.height(8.dp))
LazyRow( LazyRow(
contentPadding = PaddingValues(horizontal = 16.dp), contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp),
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
) )
} }

View File

@@ -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)
) { ) {

View File

@@ -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) {

View File

@@ -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))

View File

@@ -3,17 +3,20 @@ package hu.bbara.purefin.ui.screen.home.components.library
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
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.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.model.MediaUiModel
import hu.bbara.purefin.ui.model.LibraryUiModel
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.MediaUiModel
@Composable @Composable
fun LibraryPosterSection( fun LibraryPosterSection(
@@ -40,15 +43,20 @@ fun LibraryPosterSection(
actionLabel = "See all", actionLabel = "See all",
onActionClick = { onLibrarySelected(library) } onActionClick = { onLibrarySelected(library) }
) )
Spacer(modifier = Modifier.height(8.dp))
LazyRow( LazyRow(
contentPadding = PaddingValues(horizontal = 16.dp), contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(14.dp), horizontalArrangement = Arrangement.spacedBy(14.dp),
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
) )
} }

View File

@@ -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)

View File

@@ -3,16 +3,19 @@ package hu.bbara.purefin.ui.screen.home.components.nextup
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
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.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.model.MediaUiModel
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
@Composable @Composable
fun NextUpSection( fun NextUpSection(
@@ -33,15 +36,17 @@ fun NextUpSection(
modifier = modifier modifier = modifier
) { ) {
SectionHeader(title = "Next Up") SectionHeader(title = "Next Up")
Spacer(modifier = Modifier.height(8.dp))
LazyRow( LazyRow(
contentPadding = PaddingValues(horizontal = 16.dp), contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(14.dp), horizontalArrangement = Arrangement.spacedBy(14.dp),
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
) )
} }

View File

@@ -1,5 +1,9 @@
package hu.bbara.purefin.ui.screen.home.components.search 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.BorderStroke
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -51,17 +55,41 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
import hu.bbara.purefin.ui.common.image.PurefinLogo 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 import hu.bbara.purefin.ui.theme.AppTheme
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class, ExperimentalSharedTransitionApi::class)
@Composable @Composable
fun HomeSearchFullScreen( fun HomeSearchFullScreen(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val scheme = MaterialTheme.colorScheme 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( Column(
modifier = modifier modifier = modifier
.then(sharedBoundsModifier)
.fillMaxSize() .fillMaxSize()
.background(scheme.background) .background(scheme.background)
.statusBarsPadding() .statusBarsPadding()

View File

@@ -15,6 +15,7 @@ import hu.bbara.purefin.ui.screen.library.components.LibraryTopBar
fun LibrariesScreen( fun LibrariesScreen(
items: List<LibraryUiModel>, items: List<LibraryUiModel>,
onLibrarySelected: (LibraryUiModel) -> Unit, onLibrarySelected: (LibraryUiModel) -> Unit,
onSearchClick: () -> Unit,
selectedTab: Int, selectedTab: Int,
onTabSelected: (Int) -> Unit, onTabSelected: (Int) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
@@ -25,7 +26,7 @@ fun LibrariesScreen(
contentColor = MaterialTheme.colorScheme.onBackground, contentColor = MaterialTheme.colorScheme.onBackground,
topBar = { topBar = {
LibraryTopBar( LibraryTopBar(
onSearchClick = {}, onSearchClick = onSearchClick,
) )
}, },
bottomBar = { bottomBar = {

View File

@@ -1,39 +1,14 @@
package hu.bbara.purefin.ui.screen.library.components 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.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.DefaultTopBar
import hu.bbara.purefin.ui.screen.home.components.DefaultTopBarSearchButton
@Composable @Composable
fun LibraryTopBar( fun LibraryTopBar(
onSearchClick: () -> Unit, onSearchClick: () -> Unit,
) { ) {
val scheme = MaterialTheme.colorScheme
DefaultTopBar { DefaultTopBar {
IconButton( DefaultTopBarSearchButton(onClick = onSearchClick)
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),
)
}
} }
} }

View File

@@ -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,15 +96,19 @@ 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 = {
if (!isHomeMediaTransitionActive) {
MovieTopBar( MovieTopBar(
onBack = onBack, onBack = onBack,
modifier = Modifier modifier = Modifier
) )
} }
}
) { innerPadding -> ) { innerPadding ->
Column( Column(
modifier = Modifier modifier = Modifier
@@ -110,6 +116,7 @@ private fun MovieScreenInternal(
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
MediaHeroSection(movie = movie) MediaHeroSection(movie = movie)
if (!isHomeMediaTransitionActive) {
MovieDetails( MovieDetails(
movie = movie, movie = movie,
downloadState = downloadState, downloadState = downloadState,
@@ -122,6 +129,7 @@ private fun MovieScreenInternal(
} }
} }
} }
}
@Composable @Composable
fun MediaHeroSection( fun MediaHeroSection(
@@ -134,6 +142,7 @@ fun MediaHeroSection(
Box ( Box (
modifier = modifier modifier = modifier
.homeMediaSharedBoundsDestination()
.height(sectionHeight) .height(sectionHeight)
) { ) {
MediaHero( MediaHero(

View File

@@ -32,16 +32,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.PlayerView import androidx.media3.ui.PlayerView
import androidx.media3.ui.SubtitleView import androidx.media3.ui.SubtitleView
import hu.bbara.purefin.player.viewmodel.PlayerViewModel 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.EmptyValueTimedVisibility
import hu.bbara.purefin.ui.common.visual.ValueChangeTimedVisibility import hu.bbara.purefin.ui.common.visual.ValueChangeTimedVisibility
import hu.bbara.purefin.ui.screen.player.components.PersistentOverlayContainer 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.PlayerGesturesLayer
import hu.bbara.purefin.ui.screen.player.components.PlayerLoadingErrorEndCard 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.PlayerQueuePanel
import hu.bbara.purefin.ui.screen.player.components.SkipSegmentButton
import hu.bbara.purefin.ui.screen.player.components.rememberPersistentOverlayController import hu.bbara.purefin.ui.screen.player.components.rememberPersistentOverlayController
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -216,16 +214,7 @@ fun PlayerScreen(
.align(Alignment.BottomEnd) .align(Alignment.BottomEnd)
.padding(end = 24.dp, bottom = 24.dp) .padding(end = 24.dp, bottom = 24.dp)
) { ) {
PurefinTextButton( SkipSegmentButton(onClick = { viewModel.skipActiveSegment() })
onClick = { viewModel.skipActiveSegment() },
modifier = Modifier.padding(8.dp)
) {
Text(
text = "Skip",
fontSize = 20.sp,
fontWeight = FontWeight.ExtraBold
)
}
} }
PlayerLoadingErrorEndCard( PlayerLoadingErrorEndCard(

View File

@@ -10,7 +10,11 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
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.layout.heightIn
import androidx.compose.foundation.layout.padding 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.Icons
import androidx.compose.material.icons.outlined.ArrowBack import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.Cast 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.Replay10
import androidx.compose.material.icons.outlined.SkipNext import androidx.compose.material.icons.outlined.SkipNext
import androidx.compose.material.icons.outlined.SkipPrevious 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.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable 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.graphics.Color
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp 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.PlayerUiState
import hu.bbara.purefin.player.model.TrackOption import hu.bbara.purefin.player.model.TrackOption
import hu.bbara.purefin.ui.common.button.GhostIconButton import hu.bbara.purefin.ui.common.button.GhostIconButton
import hu.bbara.purefin.ui.common.button.PurefinIconButton import hu.bbara.purefin.ui.common.button.PurefinIconButton
import hu.bbara.purefin.ui.common.button.PurefinTextButton
@Composable @Composable
fun PlayerControlsOverlay( fun PlayerControlsOverlay(
@@ -174,26 +179,6 @@ private fun BottomSection(
) { ) {
val scheme = MaterialTheme.colorScheme val scheme = MaterialTheme.colorScheme
Column(modifier = modifier) { 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( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -279,7 +264,7 @@ private fun BottomSection(
) )
} }
Row( Row(
modifier = Modifier.align(Alignment.CenterEnd), modifier = Modifier.align(Alignment.CenterStart),
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@@ -302,11 +287,48 @@ private fun BottomSection(
overlayController = overlayController overlayController = overlayController
) )
} }
if (uiState.activeSkippableSegmentEndMs != null) {
SkipSegmentButton(
onClick = onSkipSegment,
modifier = Modifier.align(Alignment.CenterEnd)
)
}
} }
Spacer(modifier = Modifier.height(12.dp)) 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 { private fun formatTime(positionMs: Long): String {
val totalSeconds = positionMs / 1000 val totalSeconds = positionMs / 1000
val seconds = (totalSeconds % 60).toInt() val seconds = (totalSeconds % 60).toInt()

View File

@@ -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,11 +134,13 @@ private fun SeriesScreenInternal(
modifier = modifier, modifier = modifier,
containerColor = MaterialTheme.colorScheme.background, containerColor = MaterialTheme.colorScheme.background,
topBar = { topBar = {
if (!isHomeMediaTransitionActive) {
SeriesTopBar( SeriesTopBar(
onBack = onBack, onBack = onBack,
modifier = Modifier modifier = Modifier
) )
} }
}
) { innerPadding -> ) { innerPadding ->
Column( Column(
modifier = Modifier modifier = Modifier
@@ -143,6 +148,7 @@ private fun SeriesScreenInternal(
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
SeriesHeroSection(series = series) SeriesHeroSection(series = series)
if (!isHomeMediaTransitionActive) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -190,6 +196,7 @@ private fun SeriesScreenInternal(
} }
} }
} }
}
@Composable @Composable
private fun SeriesHeroSection( private fun SeriesHeroSection(
@@ -202,6 +209,7 @@ private fun SeriesHeroSection(
Box( Box(
modifier = modifier modifier = modifier
.homeMediaSharedBoundsDestination()
.fillMaxWidth() .fillMaxWidth()
.height(sectionHeight) .height(sectionHeight)
) { ) {

View File

@@ -19,6 +19,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Composable @Composable
internal fun CircularIconButton( internal fun CircularIconButton(
@@ -47,6 +48,7 @@ internal fun CircularIconButton(
targetValue = if (isFocused) focusedBackgroundColor else containerColor, targetValue = if (isFocused) focusedBackgroundColor else containerColor,
label = "background" label = "background"
) )
val iconSize = (size - 24.dp).coerceAtLeast(0.dp)
FilledIconButton( FilledIconButton(
onClick = onClick, onClick = onClick,
@@ -67,7 +69,8 @@ internal fun CircularIconButton(
) { ) {
Icon( Icon(
imageVector = icon, imageVector = icon,
contentDescription = contentDescription contentDescription = contentDescription,
modifier = Modifier.size(iconSize)
) )
} }
} }

View File

@@ -251,6 +251,10 @@ class AppViewModel @Inject constructor(
} }
} }
fun openSearch() {
navigationManager.navigate(Route.HomeSearchRoute)
}
fun logout() { fun logout() {
viewModelScope.launch { viewModelScope.launch {
userSessionRepository.setLoggedIn(false) userSessionRepository.setLoggedIn(false)

View File

@@ -8,6 +8,9 @@ sealed interface Route : NavKey {
@Serializable @Serializable
data object Home : Route data object Home : Route
@Serializable
data object HomeSearchRoute : Route
@Serializable @Serializable
data class MovieRoute(val item: MovieDto) : Route data class MovieRoute(val item: MovieDto) : Route

View File

@@ -130,7 +130,7 @@ class EpisodeUiModel : MediaUiModel {
constructor(episode: Episode) { constructor(episode: Episode) {
id = episode.id id = episode.id
primaryText = episode.seriesName 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 description = episode.synopsis
prefixImageUrl = episode.imageUrlPrefix prefixImageUrl = episode.imageUrlPrefix
progress = (episode.progress?.toFloat() ?: 0f) / 100f progress = (episode.progress?.toFloat() ?: 0f) / 100f