diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/BottomBar.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/BottomBar.kt index 6796dae..e31d7e3 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/BottomBar.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/BottomBar.kt @@ -39,3 +39,7 @@ fun createNavigationList(): List> { ) return list } + +fun List>.findTitle(route: Route): String? { + return firstOrNull { it.route == route }?.title +} diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/MainScreen.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/MainScreen.kt index 33b2a11..4c39bd3 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/MainScreen.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/MainScreen.kt @@ -1,147 +1,96 @@ package ru.shadowsparky.vbox.shared.presentation -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.text.input.TextFieldValue import ru.shadowsparky.ui.BaseScreen import ru.shadowsparky.ui.LocalBlurState -import ru.shadowsparky.ui.USE_EXPERIMENTAL_RAIL import ru.shadowsparky.ui.applyBlurEffect -import ru.shadowsparky.ui.components.isTv import ru.shadowsparky.ui.currentWindowType import ru.shadowsparky.ui.isCompact -import ru.shadowsparky.ui.isExpanded import ru.shadowsparky.ui.rememberBlurState import ru.shadowsparky.ui.theme.ComponentDefaults import ru.shadowsparky.vbox.shared.presentation.list.VideoContent import ru.shadowsparky.vbox.shared.presentation.nav.Child import ru.shadowsparky.vbox.shared.presentation.nav.Route -import ru.shadowsparky.vbox.shared.presentation.saved.SavedMovieContent -import ru.shadowsparky.vbox.shared.presentation.search.SearchAppBar -import ru.shadowsparky.vbox.shared.presentation.search.SearchVideoContent +import ru.shadowsparky.vbox.shared.presentation.saved.SavedMovieScreen +import ru.shadowsparky.vbox.shared.presentation.search.SearchScreen import ru.shadowsparky.vbox.shared.presentation.settings.SettingsContent @Composable -fun MainScreen( - route: Route, - child: Child, - rootComponent: RootComponent -) { - if (child is Child.SearchChild) { - SearchScreenInternal(child, route, rootComponent) - } else { - MainScreenInternal(route, child, rootComponent) +fun MainScreen(route: Route, child: Child, rootComponent: RootComponent) { + when (child) { + is Child.SearchChild -> SearchScreen(child.component, route) { rootComponent.nav(it, true) } + is Child.SavedMovieChild -> SavedMovieScreen(child.component, route) { rootComponent.navReplace(it) } + else -> MainScreenInternal(route, child) { rootComponent.navReplace(it) } } } @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun SearchScreenInternal( - child: Child.SearchChild, +fun BaseMainScreen( + appBar: @Composable () -> Unit, route: Route, - rootComponent: RootComponent, + scrollBehavior: TopAppBarScrollBehavior, + onNavigate: (Route) -> Unit, + content: @Composable (PaddingValues) -> Unit ) { - val component = child.component - var textFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) { - mutableStateOf(TextFieldValue()) - } - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() - val requester by remember { mutableStateOf(FocusRequester()) } - val windowType = currentWindowType() - val searchAppBar: (@Composable () -> Unit) = { - SearchAppBar( - textFieldValue = textFieldValue, - onDone = { component.done(textFieldValue.text) }, - onValueChange = { textFieldValue = it }, - focusRequester = requester - ) - } val blurState = rememberBlurState() CompositionLocalProvider(LocalBlurState provides blurState) { BaseScreen( - appBar = { - if (windowType.isCompact || !USE_EXPERIMENTAL_RAIL) searchAppBar() - }, + appBar = { appBar() }, currentRoute = route, - onSelectNavigation = { rootComponent.nav(it, true) }, + onSelectNavigation = { onNavigate(it) }, navigationEntries = createNavigationList(), colorScheme = getColorScheme(), scrollBehavior = scrollBehavior - ) { - val content: @Composable () -> Unit = { - SearchVideoContent( - child.component, - it, - requester - ) { textFieldValue = it } - } - if (windowType.isExpanded && USE_EXPERIMENTAL_RAIL) { - Column { - searchAppBar() - content() - } - } else { - content() - } + ) { padding -> + content(padding) } } } @OptIn(ExperimentalMaterial3Api::class) @Composable -fun MainScreenInternal(route: Route, child: Child, rootComponent: RootComponent) { - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() +private fun MainScreenInternal(route: Route, child: Child, onNavigate: (Route) -> Unit) { val navList = createNavigationList() - val windowType = currentWindowType() - val blurState = rememberBlurState() - CompositionLocalProvider(LocalBlurState provides blurState) { - BaseScreen( - appBar = { - if (windowType.isCompact || !USE_EXPERIMENTAL_RAIL) { - navList.firstOrNull { it.route == route }?.title?.let { - if (isTv()) { - TopAppBar( - title = { Text(it) }, - scrollBehavior = scrollBehavior, - modifier = Modifier.applyBlurEffect(blurState), - colors = ComponentDefaults.topAppBarColors() - ) - } else { - LargeTopAppBar( - title = { Text(it) }, - scrollBehavior = scrollBehavior, - modifier = Modifier.applyBlurEffect(blurState), - colors = ComponentDefaults.topAppBarColors() - ) - } - } + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + BaseMainScreen( + appBar = { + navList.findTitle(route)?.let { title -> + if (currentWindowType().isCompact) { + LargeTopAppBar( + title = { Text(title) }, + scrollBehavior = scrollBehavior, + modifier = Modifier.applyBlurEffect(LocalBlurState.current), + colors = ComponentDefaults.topAppBarColors() + ) + } else { + TopAppBar( + title = { Text(title) }, + scrollBehavior = scrollBehavior, + modifier = Modifier.applyBlurEffect(LocalBlurState.current), + colors = ComponentDefaults.topAppBarColors() + ) } - }, - currentRoute = route, - onSelectNavigation = { rootComponent.navReplace(it) }, - navigationEntries = navList, - colorScheme = getColorScheme(), - scrollBehavior = scrollBehavior - ) { padding -> + } + }, + route = route, + scrollBehavior = scrollBehavior, + onNavigate = onNavigate, + content = { padding -> when (child) { is Child.VideosChild -> VideoContent(child.component, padding) is Child.SettingsChild -> SettingsContent(child.component, padding) - is Child.SavedMovieChild -> SavedMovieContent(child.component, padding) else -> {} } } - } + ) } diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/list/VideoScreen.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/list/VideoScreen.kt index ed8a9bc..a29cdf1 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/list/VideoScreen.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/list/VideoScreen.kt @@ -10,7 +10,6 @@ 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.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.aspectRatio @@ -42,7 +41,6 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -119,7 +117,6 @@ fun VideoScreenContent( emptyResource: StringResource, onRefresh: () -> Unit, onShowOverview: (Long) -> Unit, - extraFab: (@Composable BoxScope.() -> Unit)? = null, onLoad: () -> Unit ) { LaunchedEffect(Unit) { @@ -130,7 +127,7 @@ fun VideoScreenContent( onRefresh() } } else if (items != null) { - VideosInfo(items = items, paddingValues, emptyResource, extraFab) { + VideosInfo(items = items, paddingValues, emptyResource) { onShowOverview(it.id) } } else { @@ -143,16 +140,11 @@ fun VideosInfo( items: List, paddingValues: PaddingValues, emptyResource: StringResource, - extraFab: (@Composable BoxScope.() -> Unit)? = null, onClick: (item: VideoItem) -> Unit ) { val blur = LocalBlurState.current val gridState = rememberLazyGridState() - val density = LocalDensity.current val layoutDirection = LocalLayoutDirection.current - - var fabHeight by remember { mutableStateOf(0.dp) } - Box(modifier = Modifier.fillMaxSize()) { if (items.isNotEmpty()) { LazyVerticalGrid( @@ -164,7 +156,7 @@ fun VideosInfo( start = paddingValues.calculateStartPadding(layoutDirection), top = paddingValues.calculateTopPadding(), end = paddingValues.calculateEndPadding(layoutDirection), - bottom = paddingValues.calculateBottomPadding() + (fabHeight / 2) + bottom = paddingValues.calculateBottomPadding() ), state = gridState ) { @@ -179,19 +171,6 @@ fun VideosInfo( } else { VideosNotFound(emptyResource) } - - val rootBoxScope = this - - extraFab?.let { - Box( - modifier = Modifier.align(Alignment.BottomEnd) - .onGloballyPositioned { coordinates -> - fabHeight = with(density) { - coordinates.size.height.toDp() - } - } - ) { it(rootBoxScope) } - } } } diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/saved/SavedMovieScreen.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/saved/SavedMovieScreen.kt index 47e728a..7ddb520 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/saved/SavedMovieScreen.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/saved/SavedMovieScreen.kt @@ -9,18 +9,19 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.items import androidx.compose.material3.Checkbox import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -28,19 +29,24 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import ru.shadowsparky.ui.components.LazyColumn +import ru.shadowsparky.ui.theme.ComponentDefaults import ru.shadowsparky.vbox.shared.domain.tag.TagInfo +import ru.shadowsparky.vbox.shared.presentation.BaseMainScreen +import ru.shadowsparky.vbox.shared.presentation.createNavigationList +import ru.shadowsparky.vbox.shared.presentation.findTitle import ru.shadowsparky.vbox.shared.presentation.list.VideoScreenContent +import ru.shadowsparky.vbox.shared.presentation.nav.Route +@OptIn(ExperimentalMaterial3Api::class) @Composable -fun SavedMovieContent(comp: SavedMovieScreenComponent, paddingValues: PaddingValues) { - val items by comp.all.collectAsStateWithLifecycle(null) +fun SavedMovieScreen(comp: SavedMovieScreenComponent, route: Route, onNavigate: (Route) -> Unit) { val tags by comp.userTags.collectAsStateWithLifecycle(null) + val navList = createNavigationList() var filterBottomSheetVisible by rememberSaveable { mutableStateOf(false) } if (filterBottomSheetVisible) { FilterBottomSheet( @@ -50,6 +56,33 @@ fun SavedMovieContent(comp: SavedMovieScreenComponent, paddingValues: PaddingVal onFilterBy = { comp.filterByTag(it) } ) } + BaseMainScreen( + appBar = { + TopAppBar( + title = { navList.findTitle(route)?.let { Text(it) } }, + actions = { + IconButton( + onClick = { filterBottomSheetVisible = true } + ) { + Icon( + painterResource(Res.drawable.filter_list), + contentDescription = null + ) + } + }, + colors = ComponentDefaults.topAppBarColors() + ) + }, + route = route, + scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(), + onNavigate = onNavigate, + content = { SavedMovieContent(comp, it) } + ) +} + +@Composable +private fun SavedMovieContent(comp: SavedMovieScreenComponent, paddingValues: PaddingValues) { + val items by comp.all.collectAsStateWithLifecycle(null) VideoScreenContent( paddingValues, null, @@ -57,23 +90,7 @@ fun SavedMovieContent(comp: SavedMovieScreenComponent, paddingValues: PaddingVal onRefresh = {}, onShowOverview = { comp.showOverview(it) }, emptyResource = Res.string.saved_movies_not_found, - onLoad = {}, - extraFab = { - if (tags?.isNotEmpty() == true) { - FloatingActionButton( - onClick = { filterBottomSheetVisible = true }, - modifier = Modifier.padding( - end = paddingValues.calculateEndPadding(LocalLayoutDirection.current) + 4.dp, - bottom = paddingValues.calculateBottomPadding() + 4.dp - ) - ) { - Icon( - painterResource(Res.drawable.filter_list), - contentDescription = null - ) - } - } - } + onLoad = {} ) } diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/search/SearchScreen.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/search/SearchScreen.kt index 3ec41b9..83ffd99 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/search/SearchScreen.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/search/SearchScreen.kt @@ -35,13 +35,17 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldColors import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberSearchBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.runtime.withFrameNanos import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -62,7 +66,6 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import kotlinx.coroutines.delay import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import ru.shadowsparky.ui.LocalBlurState @@ -71,7 +74,42 @@ import ru.shadowsparky.ui.applyBlurSource import ru.shadowsparky.ui.components.BackButton import ru.shadowsparky.ui.components.ExpressiveLazyColumn import ru.shadowsparky.ui.theme.ComponentDefaults +import ru.shadowsparky.vbox.shared.presentation.BaseMainScreen import ru.shadowsparky.vbox.shared.presentation.imePaddingInternal +import ru.shadowsparky.vbox.shared.presentation.nav.Route + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SearchScreen( + component: SearchScreenComponent, + route: Route, + onNavigate: (Route) -> Unit +) { + val requester by remember { mutableStateOf(FocusRequester()) } + var textFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue()) + } + BaseMainScreen( + appBar = { + SearchAppBar( + textFieldValue = textFieldValue, + onDone = { component.done(textFieldValue.text) }, + onValueChange = { textFieldValue = it }, + focusRequester = requester + ) + }, + content = { + SearchVideoContent( + component, + it, + requester + ) { textFieldValue = it } + }, + route = route, + scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(), + onNavigate = onNavigate + ) +} @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable @@ -103,7 +141,7 @@ fun SearchVideoContent( Text( text = stringResource(Res.string.search_empty_now), style = MaterialTheme.typography.titleMedium, - modifier = Modifier.clickable { }, + modifier = Modifier.focusRequester(focusRequester).clickable {}, textAlign = TextAlign.Center ) } diff --git a/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/BaseScreen.kt b/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/BaseScreen.kt index 04cd278..bbe0f84 100644 --- a/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/BaseScreen.kt +++ b/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/BaseScreen.kt @@ -3,12 +3,12 @@ package ru.shadowsparky.ui import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing -import androidx.compose.foundation.layout.width import androidx.compose.material3.ColorScheme import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @@ -29,12 +29,13 @@ import androidx.compose.runtime.Immutable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import ru.shadowsparky.ui.nav.Route import ru.shadowsparky.ui.theme.ComponentDefaults import ru.shadowsparky.ui.theme.getColorScheme -const val USE_EXPERIMENTAL_RAIL = false +const val USE_EXPERIMENTAL_RAIL = true @Immutable data class NavigationEntry( @@ -125,9 +126,7 @@ private fun BottomBar( NavigationBarItem( currentRoute == entry.route, onClick = { onSelectNavigation?.invoke(entry.route) }, - icon = { - Icon(entry.painter, contentDescription = entry.title) - }, + icon = { Icon(entry.painter, contentDescription = entry.title) }, label = { Text(entry.title) } ) } @@ -144,23 +143,30 @@ private fun ExpandedContent( onSelectNavigation: ((T) -> Unit)?, content: @Composable ((PaddingValues) -> Unit) ) { - Row(Modifier.fillMaxSize().padding(values)) { - Spacer(Modifier.width(4.dp)) - NavigationRail { + val topPadding = values.calculateTopPadding() + val bottomPadding = values.calculateBottomPadding() + val startPadding = values.calculateStartPadding(LayoutDirection.Ltr) + val endPadding = values.calculateEndPadding(LayoutDirection.Ltr) + Row(modifier = Modifier.fillMaxSize().padding(top = topPadding)) { + NavigationRail(modifier = Modifier.padding(horizontal = 4.dp)) { navigationEntries.forEach { entry -> NavigationRailItem( selected = currentRoute == entry.route, onClick = { onSelectNavigation?.invoke(entry.route) }, - icon = { - Icon(entry.painter, contentDescription = entry.title) - }, + icon = { Icon(entry.painter, contentDescription = entry.title) }, label = { Text(entry.title) } ) } } - Spacer(Modifier.width(4.dp)) - Box(Modifier.weight(1f)) { - content(PaddingValues(0.dp)) + Box(modifier = Modifier.weight(1f)) { + content( + PaddingValues( + start = startPadding, + top = 0.dp, + end = endPadding, + bottom = bottomPadding + ) + ) } } }