diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/custom/FloatingSearchButton.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/custom/FloatingSearchButton.kt new file mode 100644 index 00000000..8b12d96e --- /dev/null +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/custom/FloatingSearchButton.kt @@ -0,0 +1,157 @@ +package dev.datlag.burningseries.shared.ui.custom + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.selection.TextSelectionColors +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp +import dev.icerock.moko.resources.compose.stringResource +import dev.datlag.burningseries.shared.SharedRes +import dev.datlag.burningseries.shared.common.withIOContext +import kotlinx.coroutines.delay + +@Composable +fun FloatingSearchButton( + icon: ImageVector, + contentDescription: String?, + clearIcon: ImageVector, + closeIcon: ImageVector, + modifier: Modifier = Modifier, + onTextChange: (String) -> Unit +) { + val focusRequester = remember { FocusRequester() } + var opened by remember { mutableStateOf(false) } + + Surface( + color = MaterialTheme.colorScheme.primaryContainer, + modifier = modifier, + shape = FloatingActionButtonDefaults.shape, + shadowElevation = 6.dp, + onClick = { + if (!opened) { + opened = true + } else { + focusRequester.requestFocus() + } + } + ) { + AnimatedContent(targetState = opened) { expand -> + if (expand) { + SearchBar( + close = { + opened = false + }, + clearIcon = clearIcon, + closeIcon = closeIcon, + focusRequester = focusRequester, + onTextChange = onTextChange + ) + + LaunchedEffect(focusRequester) { + withIOContext { + delay(500) // wait till transition is done + } + focusRequester.requestFocus() + } + } else { + Box( + modifier = Modifier.size(56.dp), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = icon, + contentDescription = contentDescription + ) + } + } + } + } +} + +@Composable +private fun SearchBar( + focusRequester: FocusRequester, + close: () -> Unit, + clearIcon: ImageVector, + closeIcon: ImageVector, + onTextChange: (String) -> Unit +) { + var text by remember { mutableStateOf("") } + + TextField( + value = text, + onValueChange = { + text = it + onTextChange(text) + }, + modifier = Modifier.focusRequester(focusRequester), + placeholder = { + Text( + text = stringResource(SharedRes.strings.search), + style = MaterialTheme.typography.labelLarge + ) + }, + singleLine = true, + colors = searchTextFieldColors(), + keyboardOptions = KeyboardOptions(autoCorrect = false, imeAction = ImeAction.Search), + leadingIcon = { + IconButton( + onClick = close + ) { + Icon( + imageVector = closeIcon, + contentDescription = stringResource(SharedRes.strings.close) + ) + } + }, + trailingIcon = { + IconButton( + onClick = { + text = "" + onTextChange(text) + } + ) { + Icon( + imageVector = clearIcon, + contentDescription = stringResource(SharedRes.strings.clear) + ) + } + } + ) +} + +@Composable +private fun searchTextFieldColors( + contentColor: Color = LocalContentColor.current +): TextFieldColors { + return TextFieldDefaults.colors( + unfocusedTextColor = contentColor, + focusedTextColor = contentColor, + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + disabledContainerColor = Color.Transparent, + cursorColor = contentColor, + selectionColors = TextSelectionColors( + handleColor = contentColor, + backgroundColor = contentColor.copy(alpha = 0.3f) + ), + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + focusedLeadingIconColor = contentColor, + unfocusedLeadingIconColor = contentColor, + focusedTrailingIconColor = contentColor, + unfocusedTrailingIconColor = contentColor, + unfocusedPlaceholderColor = contentColor.copy(alpha = 0.5F), + focusedPlaceholderColor = contentColor.copy(alpha = 0.5F), + ) +} diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/custom/toolbar/CollapsingToolbarScaffold.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/custom/toolbar/CollapsingToolbarScaffold.kt index ab448dd9..fa192e79 100644 --- a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/custom/toolbar/CollapsingToolbarScaffold.kt +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/custom/toolbar/CollapsingToolbarScaffold.kt @@ -3,10 +3,13 @@ package dev.datlag.burningseries.shared.ui.custom.toolbar import androidx.compose.foundation.ScrollState import androidx.compose.foundation.gestures.ScrollableDefaults import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf @@ -26,6 +29,7 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntSize import dev.datlag.burningseries.model.common.safeCast import kotlin.math.max +import kotlin.math.roundToInt @Stable class CollapsingToolbarScaffoldState( @@ -71,6 +75,7 @@ interface CollapsingToolbarScaffoldScope { fun Modifier.align(alignment: Alignment): Modifier } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun CollapsingToolbarScaffold( modifier: Modifier, @@ -78,6 +83,7 @@ fun CollapsingToolbarScaffold( scrollStrategy: ScrollStrategy, enabled: Boolean = true, toolbarModifier: Modifier = Modifier, + toolbarPadding: Int = TopAppBarDefaults.windowInsets.asPaddingValues().calculateTopPadding().value.roundToInt(), toolbarClipToBounds: Boolean = true, toolbarScrollable: Boolean = false, toolbar: @Composable CollapsingToolbarScope.() -> Unit, @@ -131,13 +137,14 @@ fun CollapsingToolbarScaffold( ) val toolbarPlaceable = measurables[0].measure(toolbarConstraints) + val toolbarHeight = toolbarPlaceable.height + toolbarPadding val bodyConstraints = constraints.copy( minWidth = 0, minHeight = 0, maxHeight = when (scrollStrategy) { ScrollStrategy.ExitUntilCollapsed -> - (constraints.maxHeight - toolbarState.minHeight).coerceAtLeast(0) + (constraints.maxHeight - toolbarHeight).coerceAtLeast(0) ScrollStrategy.EnterAlways, ScrollStrategy.EnterAlwaysCollapsed -> constraints.maxHeight @@ -152,8 +159,6 @@ fun CollapsingToolbarScaffold( it.measure(bodyConstraints) } - val toolbarHeight = toolbarPlaceable.height - val width = max( toolbarPlaceable.width, bodyPlaceables.maxOfOrNull { it.width } ?: 0 diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/screen/initial/InitialScreen.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/screen/initial/InitialScreen.kt index 033a8f0c..f1d681b4 100644 --- a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/screen/initial/InitialScreen.kt +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/screen/initial/InitialScreen.kt @@ -99,7 +99,6 @@ private fun CompactScreen( Box(modifier = Modifier.fillMaxSize()) { val homeScrollEnabled by component.homeScrollEnabled.collectAsStateWithLifecycle() val favoriteScrollEnabled by component.favoriteScrollEnabled.collectAsStateWithLifecycle() - val searchScrollEnabled by component.searchScrollEnabled.collectAsStateWithLifecycle() Pages( pages = component.pages, @@ -112,8 +111,7 @@ private fun CompactScreen( val scrollEnabled = when (state.currentPage) { 0 -> homeScrollEnabled 1 -> favoriteScrollEnabled - 2 -> searchScrollEnabled - else -> homeScrollEnabled && favoriteScrollEnabled && searchScrollEnabled + else -> homeScrollEnabled && favoriteScrollEnabled } HorizontalPager( modifier = modifier, diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/screen/initial/InitialScreenComponent.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/screen/initial/InitialScreenComponent.kt index 4cbf8da4..a2b8ffcd 100644 --- a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/screen/initial/InitialScreenComponent.kt +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/screen/initial/InitialScreenComponent.kt @@ -43,11 +43,6 @@ class InitialScreenComponent( label = SharedRes.strings.favorites, unselectedIcon = Icons.Outlined.FavoriteBorder, selectedIcon = Icons.Filled.Favorite - ), - InitialComponent.PagerItem( - label = SharedRes.strings.search, - unselectedIcon = Icons.Outlined.Search, - selectedIcon = Icons.Filled.Search ) ) @@ -62,8 +57,7 @@ class InitialScreenComponent( Pages( items = listOf( View.Home(shortcutIntent), - View.Favorite, - View.Search + View.Favorite ), selectedIndex = when (shortcutIntent) { is Shortcut.Intent.SEARCH -> 2 @@ -114,14 +108,6 @@ class InitialScreenComponent( }, scrollEnabled = { favoriteScrollEnabled.value = it } ) - is View.Search -> SearchScreenComponent( - componentContext = componentContext, - di = di, - watchVideo = { schemeKey, series, episode, stream -> - watchVideo(schemeKey, series, episode, stream) - }, - scrollEnabled = { searchScrollEnabled.value = it } - ) } } diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/screen/initial/View.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/screen/initial/View.kt index c1c16ea5..4d352433 100644 --- a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/screen/initial/View.kt +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/screen/initial/View.kt @@ -13,7 +13,4 @@ sealed class View { @Serializable data object Favorite : View() - - @Serializable - data object Search : View() } \ No newline at end of file diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/screen/initial/home/HomeScreen.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/screen/initial/home/HomeScreen.kt index 68b2976c..8b771ff4 100644 --- a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/screen/initial/home/HomeScreen.kt +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/shared/ui/screen/initial/home/HomeScreen.kt @@ -5,6 +5,10 @@ import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.Search import androidx.compose.material3.* import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass @@ -28,6 +32,7 @@ import dev.datlag.burningseries.shared.common.lifecycle.collectAsStateWithLifecy import dev.datlag.burningseries.shared.common.localPadding import dev.datlag.burningseries.shared.other.StateSaver import dev.datlag.burningseries.shared.rememberIsTv +import dev.datlag.burningseries.shared.ui.custom.FloatingSearchButton import dev.datlag.burningseries.shared.ui.custom.VerticalScrollbar import dev.datlag.burningseries.shared.ui.custom.rememberScrollbarAdapter import dev.datlag.burningseries.shared.ui.custom.state.ErrorState @@ -126,86 +131,98 @@ private fun ExpandedView(home: Home, component: HomeComponent) { @Composable private fun MainView(home: Home, component: HomeComponent, modifier: Modifier = Modifier) { - Row( - modifier = modifier.padding(horizontal = 16.dp), - horizontalArrangement = Arrangement.spacedBy(2.dp) - ) { - val state = rememberLazyGridState( - initialFirstVisibleItemIndex = StateSaver.homeGridIndex, - initialFirstVisibleItemScrollOffset = StateSaver.homeGridOffset - ) - - LazyVerticalGrid( - columns = GridCells.Adaptive(400.dp), - modifier = Modifier.weight(1F).haze(state = LocalHaze.current), - verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - contentPadding = LocalPadding(), - state = state + Box(modifier = modifier) { + Row( + modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(2.dp) ) { - DeviceContent(component.release, component.onDeviceReachable) - header { - Row( - modifier = Modifier.padding(top = 16.dp).fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(16.dp) - ) { + val state = rememberLazyGridState( + initialFirstVisibleItemIndex = StateSaver.homeGridIndex, + initialFirstVisibleItemScrollOffset = StateSaver.homeGridOffset + ) + + LazyVerticalGrid( + columns = GridCells.Adaptive(400.dp), + modifier = Modifier.weight(1F).haze(state = LocalHaze.current), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = LocalPadding(), + state = state + ) { + DeviceContent(component.release, component.onDeviceReachable) + header { + Row( + modifier = Modifier.padding(top = 16.dp).fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + modifier = Modifier.weight(1F), + text = stringResource(SharedRes.strings.newest_episodes), + style = MaterialTheme.typography.headlineLarge, + fontWeight = FontWeight.Bold + ) + if (!StateSaver.sekretLibraryLoaded) { + IconButton( + onClick = { + component.showDialog(DialogConfig.Sekret) + }, + colors = IconButtonDefaults.filledIconButtonColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer + ) + ) { + Icon( + imageVector = MaterialSymbols.rememberDeployedCodeAlert(), + contentDescription = stringResource(SharedRes.strings.sekret_unavailable_title) + ) + } + } + } + } + items(home.episodes, key = { + it.href + }) { episode -> + EpisodeItem(episode) { + component.itemClicked(HomeConfig.Series(episode)) + } + } + header { + Spacer(modifier = Modifier.size(48.dp)) + } + header { Text( - modifier = Modifier.weight(1F), - text = stringResource(SharedRes.strings.newest_episodes), + text = stringResource(SharedRes.strings.newest_series), style = MaterialTheme.typography.headlineLarge, fontWeight = FontWeight.Bold ) - if (!StateSaver.sekretLibraryLoaded) { - IconButton( - onClick = { - component.showDialog(DialogConfig.Sekret) - }, - colors = IconButtonDefaults.filledIconButtonColors( - containerColor = MaterialTheme.colorScheme.errorContainer, - contentColor = MaterialTheme.colorScheme.onErrorContainer - ) - ) { - Icon( - imageVector = MaterialSymbols.rememberDeployedCodeAlert(), - contentDescription = stringResource(SharedRes.strings.sekret_unavailable_title) - ) - } - } } - } - items(home.episodes, key = { - it.href - }) { episode -> - EpisodeItem(episode) { - component.itemClicked(HomeConfig.Series(episode)) + items(home.series, key = { + it.href + }) { series -> + SeriesItem(series) { + component.itemClicked(HomeConfig.Series(series)) + } } } - header { - Spacer(modifier = Modifier.size(48.dp)) - } - header { - Text( - text = stringResource(SharedRes.strings.newest_series), - style = MaterialTheme.typography.headlineLarge, - fontWeight = FontWeight.Bold - ) - } - items(home.series, key = { - it.href - }) { series -> - SeriesItem(series) { - component.itemClicked(HomeConfig.Series(series)) + VerticalScrollbar(rememberScrollbarAdapter(state)) + + DisposableEffect(state) { + onDispose { + StateSaver.homeGridIndex = state.firstVisibleItemIndex + StateSaver.homeGridOffset = state.firstVisibleItemScrollOffset } } } - VerticalScrollbar(rememberScrollbarAdapter(state)) + FloatingSearchButton( + icon = Icons.Default.Search, + contentDescription = null, + clearIcon = Icons.Default.Clear, + closeIcon = Icons.AutoMirrored.Default.KeyboardArrowRight, + modifier = Modifier.align(Alignment.BottomEnd).localPadding(16.dp), + onTextChange = { - DisposableEffect(state) { - onDispose { - StateSaver.homeGridIndex = state.firstVisibleItemIndex - StateSaver.homeGridOffset = state.firstVisibleItemScrollOffset } - } + ) } } \ No newline at end of file