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.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
)
}
}
}
}

View File

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

View File

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

View File

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

View File

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

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 = {},
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()

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

View File

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

View File

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

View File

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

View File

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

View File

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

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.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
)
}

View File

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

View File

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

View File

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

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.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
)
}

View File

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

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.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
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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