mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-18 07:51:46 +00:00
feat: add scroll to top functionality across root screens
This commit is contained in:
parent
3c61e0d39e
commit
59bfb3f26b
6 changed files with 119 additions and 29 deletions
|
|
@ -181,6 +181,8 @@ import com.nuvio.app.features.watchprogress.WatchProgressRepository
|
|||
import com.nuvio.app.features.watchprogress.nextUpDismissKey
|
||||
import com.nuvio.app.features.watching.application.WatchingActions
|
||||
import com.nuvio.app.features.watching.application.WatchingState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
|
|
@ -544,8 +546,11 @@ private fun MainAppContent(
|
|||
val coroutineScope = rememberCoroutineScope()
|
||||
var selectedTab by rememberSaveable { mutableStateOf(AppScreenTab.Home) }
|
||||
var searchFocusRequestCount by remember { mutableStateOf(0) }
|
||||
val homeScrollToTopRequests = remember { MutableSharedFlow<Unit>(extraBufferCapacity = 1) }
|
||||
val searchScrollToTopRequests = remember { MutableSharedFlow<Unit>(extraBufferCapacity = 1) }
|
||||
val libraryScrollToTopRequests = remember { MutableSharedFlow<Unit>(extraBufferCapacity = 1) }
|
||||
val settingsRootActionRequests = remember { MutableSharedFlow<Unit>(extraBufferCapacity = 1) }
|
||||
val currentBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val nativeRequestedTab by remember { NativeTabBridge.requestedTab }.collectAsStateWithLifecycle()
|
||||
val liquidGlassNativeTabBarEnabled by remember {
|
||||
ThemeSettingsRepository.liquidGlassNativeTabBarEnabled
|
||||
}.collectAsStateWithLifecycle()
|
||||
|
|
@ -602,9 +607,28 @@ private fun MainAppContent(
|
|||
.sorted()
|
||||
}
|
||||
|
||||
LaunchedEffect(nativeRequestedTab) {
|
||||
if (liquidGlassNativeTabBarSupported && liquidGlassNativeTabBarEnabled) {
|
||||
selectedTab = nativeRequestedTab.toAppScreenTab()
|
||||
fun handleRootTabClick(tab: AppScreenTab) {
|
||||
if (selectedTab != tab) {
|
||||
selectedTab = tab
|
||||
return
|
||||
}
|
||||
|
||||
when (tab) {
|
||||
AppScreenTab.Home -> homeScrollToTopRequests.tryEmit(Unit)
|
||||
AppScreenTab.Search -> {
|
||||
searchFocusRequestCount++
|
||||
searchScrollToTopRequests.tryEmit(Unit)
|
||||
}
|
||||
AppScreenTab.Library -> libraryScrollToTopRequests.tryEmit(Unit)
|
||||
AppScreenTab.Settings -> settingsRootActionRequests.tryEmit(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(liquidGlassNativeTabBarSupported, liquidGlassNativeTabBarEnabled) {
|
||||
NativeTabBridge.requestedTabs.collectLatest { requestedTab ->
|
||||
if (liquidGlassNativeTabBarSupported && liquidGlassNativeTabBarEnabled) {
|
||||
handleRootTabClick(requestedTab.toAppScreenTab())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1059,35 +1083,29 @@ private fun MainAppContent(
|
|||
NuvioNavigationBar {
|
||||
NavItem(
|
||||
selected = selectedTab == AppScreenTab.Home,
|
||||
onClick = { selectedTab = AppScreenTab.Home },
|
||||
onClick = { handleRootTabClick(AppScreenTab.Home) },
|
||||
icon = Icons.Filled.Home,
|
||||
contentDescription = stringResource(Res.string.compose_nav_home),
|
||||
)
|
||||
NavItem(
|
||||
selected = selectedTab == AppScreenTab.Search,
|
||||
onClick = {
|
||||
if (selectedTab == AppScreenTab.Search) {
|
||||
searchFocusRequestCount++
|
||||
} else {
|
||||
selectedTab = AppScreenTab.Search
|
||||
}
|
||||
},
|
||||
onClick = { handleRootTabClick(AppScreenTab.Search) },
|
||||
icon = Res.drawable.sidebar_search,
|
||||
contentDescription = stringResource(Res.string.compose_nav_search),
|
||||
)
|
||||
NavItem(
|
||||
selected = selectedTab == AppScreenTab.Library,
|
||||
onClick = { selectedTab = AppScreenTab.Library },
|
||||
onClick = { handleRootTabClick(AppScreenTab.Library) },
|
||||
icon = Res.drawable.sidebar_library,
|
||||
contentDescription = stringResource(Res.string.compose_nav_library),
|
||||
)
|
||||
NavItem(
|
||||
selected = selectedTab == AppScreenTab.Settings,
|
||||
onClick = { selectedTab = AppScreenTab.Settings },
|
||||
onClick = { handleRootTabClick(AppScreenTab.Settings) },
|
||||
) {
|
||||
ProfileSwitcherTab(
|
||||
selected = selectedTab == AppScreenTab.Settings,
|
||||
onClick = { selectedTab = AppScreenTab.Settings },
|
||||
onClick = { handleRootTabClick(AppScreenTab.Settings) },
|
||||
onProfileSelected = onProfileSelected,
|
||||
onAddProfileRequested = onSwitchProfile,
|
||||
)
|
||||
|
|
@ -1106,6 +1124,11 @@ private fun MainAppContent(
|
|||
.padding(innerPadding),
|
||||
selectedTab = selectedTab,
|
||||
searchFocusRequestCount = searchFocusRequestCount,
|
||||
rootActionsEnabled = tabsRouteActive,
|
||||
homeScrollToTopRequests = homeScrollToTopRequests,
|
||||
searchScrollToTopRequests = searchScrollToTopRequests,
|
||||
libraryScrollToTopRequests = libraryScrollToTopRequests,
|
||||
settingsRootActionRequests = settingsRootActionRequests,
|
||||
animateHomeCollectionGifs = tabsRouteActive,
|
||||
onCatalogClick = onCatalogClick,
|
||||
onPosterClick = { meta ->
|
||||
|
|
@ -1160,13 +1183,7 @@ private fun MainAppContent(
|
|||
if (isTabletLayout && !useNativeBottomTabs) {
|
||||
TabletFloatingTopBar(
|
||||
selectedTab = selectedTab,
|
||||
onTabSelected = { tab ->
|
||||
if (tab == AppScreenTab.Search && selectedTab == AppScreenTab.Search) {
|
||||
searchFocusRequestCount++
|
||||
} else {
|
||||
selectedTab = tab
|
||||
}
|
||||
},
|
||||
onTabSelected = ::handleRootTabClick,
|
||||
onProfileSelected = onProfileSelected,
|
||||
onAddProfileRequested = onSwitchProfile,
|
||||
)
|
||||
|
|
@ -2196,6 +2213,11 @@ private fun AppTabHost(
|
|||
selectedTab: AppScreenTab,
|
||||
modifier: Modifier = Modifier,
|
||||
searchFocusRequestCount: Int = 0,
|
||||
rootActionsEnabled: Boolean = true,
|
||||
homeScrollToTopRequests: Flow<Unit>,
|
||||
searchScrollToTopRequests: Flow<Unit>,
|
||||
libraryScrollToTopRequests: Flow<Unit>,
|
||||
settingsRootActionRequests: Flow<Unit>,
|
||||
animateHomeCollectionGifs: Boolean = true,
|
||||
onCatalogClick: ((HomeCatalogSection) -> Unit)? = null,
|
||||
onPosterClick: ((MetaPreview) -> Unit)? = null,
|
||||
|
|
@ -2228,6 +2250,7 @@ private fun AppTabHost(
|
|||
HomeScreen(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
animateCollectionGifs = animateHomeCollectionGifs,
|
||||
scrollToTopRequests = homeScrollToTopRequests,
|
||||
onCatalogClick = onCatalogClick,
|
||||
onPosterClick = onPosterClick,
|
||||
onPosterLongClick = onPosterLongClick,
|
||||
|
|
@ -2244,12 +2267,14 @@ private fun AppTabHost(
|
|||
onPosterClick = onPosterClick,
|
||||
onPosterLongClick = onPosterLongClick,
|
||||
searchFocusRequestCount = searchFocusRequestCount,
|
||||
scrollToTopRequests = searchScrollToTopRequests,
|
||||
)
|
||||
}
|
||||
|
||||
AppScreenTab.Library -> {
|
||||
LibraryScreen(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
scrollToTopRequests = libraryScrollToTopRequests,
|
||||
onPosterClick = onLibraryPosterClick,
|
||||
onSectionViewAllClick = onLibrarySectionViewAllClick,
|
||||
)
|
||||
|
|
@ -2258,6 +2283,8 @@ private fun AppTabHost(
|
|||
AppScreenTab.Settings -> {
|
||||
SettingsScreen(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
rootActionRequests = settingsRootActionRequests,
|
||||
rootActionsEnabled = rootActionsEnabled,
|
||||
onSwitchProfile = onSwitchProfile,
|
||||
onHomescreenClick = onHomescreenSettingsClick,
|
||||
onMetaScreenClick = onMetaScreenSettingsClick,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package com.nuvio.app.core.ui
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
|
||||
internal enum class NativeNavigationTab {
|
||||
Home,
|
||||
|
|
@ -18,11 +18,11 @@ internal enum class NativeNavigationTab {
|
|||
}
|
||||
|
||||
internal object NativeTabBridge {
|
||||
private val _requestedTab = MutableStateFlow(NativeNavigationTab.Home)
|
||||
val requestedTab: StateFlow<NativeNavigationTab> = _requestedTab.asStateFlow()
|
||||
private val _requestedTabs = MutableSharedFlow<NativeNavigationTab>(extraBufferCapacity = 1)
|
||||
val requestedTabs: SharedFlow<NativeNavigationTab> = _requestedTabs.asSharedFlow()
|
||||
|
||||
fun requestTab(tabName: String) {
|
||||
_requestedTab.value = NativeNavigationTab.fromName(tabName)
|
||||
_requestedTabs.tryEmit(NativeNavigationTab.fromName(tabName))
|
||||
}
|
||||
|
||||
fun publishSelectedTab(tab: NativeNavigationTab) {
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@ import com.nuvio.app.features.home.components.HomeCollectionRowSection
|
|||
import com.nuvio.app.features.watchprogress.ContinueWatchingSectionStyle
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import com.nuvio.app.features.home.components.ContinueWatchingLayout
|
||||
|
|
@ -72,6 +74,7 @@ import org.jetbrains.compose.resources.stringResource
|
|||
fun HomeScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
animateCollectionGifs: Boolean = true,
|
||||
scrollToTopRequests: Flow<Unit> = emptyFlow(),
|
||||
onCatalogClick: ((HomeCatalogSection) -> Unit)? = null,
|
||||
onPosterClick: ((MetaPreview) -> Unit)? = null,
|
||||
onPosterLongClick: ((MetaPreview) -> Unit)? = null,
|
||||
|
|
@ -107,6 +110,12 @@ fun HomeScreen(
|
|||
}.collectAsStateWithLifecycle()
|
||||
var observedOfflineState by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(scrollToTopRequests) {
|
||||
scrollToTopRequests.collect {
|
||||
homeListState.animateScrollToItem(0)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(networkStatusUiState.condition) {
|
||||
when (networkStatusUiState.condition) {
|
||||
NetworkCondition.NoInternet,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -32,6 +33,8 @@ import com.nuvio.app.features.home.components.HomeEmptyStateCard
|
|||
import com.nuvio.app.features.home.components.HomePosterCard
|
||||
import com.nuvio.app.features.home.components.HomeSkeletonRow
|
||||
import com.nuvio.app.features.profiles.ProfileRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import nuvio.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.getString
|
||||
|
|
@ -46,6 +49,7 @@ private data class LibraryRemovalTarget(
|
|||
@Composable
|
||||
fun LibraryScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
scrollToTopRequests: Flow<Unit> = emptyFlow(),
|
||||
onPosterClick: ((LibraryItem) -> Unit)? = null,
|
||||
onSectionViewAllClick: ((LibrarySection) -> Unit)? = null,
|
||||
) {
|
||||
|
|
@ -57,6 +61,7 @@ fun LibraryScreen(
|
|||
var pendingRemovalTarget by remember { mutableStateOf<LibraryRemovalTarget?>(null) }
|
||||
var observedOfflineState by remember { mutableStateOf(false) }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val listState = rememberLazyListState()
|
||||
val isTraktSource = uiState.sourceMode == LibrarySourceMode.TRAKT
|
||||
val retryLibraryLoad: () -> Unit = {
|
||||
NetworkStatusRepository.requestRefresh(force = true)
|
||||
|
|
@ -89,9 +94,16 @@ fun LibraryScreen(
|
|||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(scrollToTopRequests) {
|
||||
scrollToTopRequests.collect {
|
||||
listState.animateScrollToItem(0)
|
||||
}
|
||||
}
|
||||
|
||||
NuvioScreen(
|
||||
modifier = modifier,
|
||||
horizontalPadding = 0.dp,
|
||||
listState = listState,
|
||||
) {
|
||||
stickyHeader {
|
||||
androidx.compose.foundation.layout.Column(
|
||||
|
|
|
|||
|
|
@ -57,7 +57,9 @@ import com.nuvio.app.features.home.components.homeSectionHorizontalPaddingForWid
|
|||
import com.nuvio.app.features.home.components.HomeSkeletonRow
|
||||
import com.nuvio.app.features.watched.WatchedRepository
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
import nuvio.composeapp.generated.resources.Res
|
||||
|
|
@ -83,6 +85,7 @@ fun SearchScreen(
|
|||
onPosterClick: ((MetaPreview) -> Unit)? = null,
|
||||
onPosterLongClick: ((MetaPreview) -> Unit)? = null,
|
||||
searchFocusRequestCount: Int = 0,
|
||||
scrollToTopRequests: Flow<Unit> = emptyFlow(),
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
|
|
@ -115,6 +118,12 @@ fun SearchScreen(
|
|||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(scrollToTopRequests) {
|
||||
scrollToTopRequests.collect {
|
||||
listState.animateScrollToItem(0)
|
||||
}
|
||||
}
|
||||
|
||||
val addonRefreshKey = remember(addonsUiState.addons) {
|
||||
addonsUiState.addons.mapNotNull { addon ->
|
||||
val manifest = addon.manifest ?: return@mapNotNull null
|
||||
|
|
|
|||
|
|
@ -80,6 +80,9 @@ import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesUiState
|
|||
import nuvio.composeapp.generated.resources.Res
|
||||
import nuvio.composeapp.generated.resources.compose_settings_page_root
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
|
|
@ -90,6 +93,8 @@ private const val SettingsSearchRevealHapticDelayMillis = 90L
|
|||
@Composable
|
||||
fun SettingsScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
rootActionRequests: Flow<Unit> = emptyFlow(),
|
||||
rootActionsEnabled: Boolean = true,
|
||||
onSwitchProfile: (() -> Unit)? = null,
|
||||
onHomescreenClick: () -> Unit = {},
|
||||
onMetaScreenClick: () -> Unit = {},
|
||||
|
|
@ -200,17 +205,31 @@ fun SettingsScreen(
|
|||
}
|
||||
|
||||
var currentPage by rememberSaveable { mutableStateOf(SettingsPage.Root.name) }
|
||||
val scrollToTopRequests = remember { MutableSharedFlow<Unit>(extraBufferCapacity = 1) }
|
||||
val page = remember(currentPage) { SettingsPage.valueOf(currentPage) }
|
||||
val previousPage = page.previousPage()
|
||||
|
||||
LaunchedEffect(rootActionRequests, rootActionsEnabled, page) {
|
||||
rootActionRequests.collect {
|
||||
if (!rootActionsEnabled) return@collect
|
||||
val pageToOpen = page.previousPage()
|
||||
if (pageToOpen != null) {
|
||||
currentPage = pageToOpen.name
|
||||
} else {
|
||||
scrollToTopRequests.tryEmit(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PlatformBackHandler(
|
||||
enabled = previousPage != null,
|
||||
enabled = rootActionsEnabled && previousPage != null,
|
||||
onBack = { previousPage?.let { currentPage = it.name } },
|
||||
)
|
||||
|
||||
if (maxWidth >= 768.dp) {
|
||||
TabletSettingsScreen(
|
||||
page = page,
|
||||
scrollToTopRequests = scrollToTopRequests,
|
||||
onPageChange = { currentPage = it.name },
|
||||
showLoadingOverlay = playerSettingsUiState.showLoadingOverlay,
|
||||
holdToSpeedEnabled = playerSettingsUiState.holdToSpeedEnabled,
|
||||
|
|
@ -259,6 +278,7 @@ fun SettingsScreen(
|
|||
} else {
|
||||
MobileSettingsScreen(
|
||||
page = page,
|
||||
scrollToTopRequests = scrollToTopRequests,
|
||||
onPageChange = { currentPage = it.name },
|
||||
showLoadingOverlay = playerSettingsUiState.showLoadingOverlay,
|
||||
holdToSpeedEnabled = playerSettingsUiState.holdToSpeedEnabled,
|
||||
|
|
@ -317,6 +337,7 @@ fun SettingsScreen(
|
|||
@Composable
|
||||
private fun MobileSettingsScreen(
|
||||
page: SettingsPage,
|
||||
scrollToTopRequests: Flow<Unit>,
|
||||
onPageChange: (SettingsPage) -> Unit,
|
||||
showLoadingOverlay: Boolean,
|
||||
holdToSpeedEnabled: Boolean,
|
||||
|
|
@ -427,6 +448,12 @@ private fun MobileSettingsScreen(
|
|||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(scrollToTopRequests) {
|
||||
scrollToTopRequests.collect {
|
||||
listState.animateScrollToItem(0)
|
||||
}
|
||||
}
|
||||
|
||||
NuvioScreen(
|
||||
modifier = Modifier.nestedScroll(rootSearchRevealConnection),
|
||||
listState = listState,
|
||||
|
|
@ -624,6 +651,7 @@ private fun rememberSettingsRootSearchRevealConnection(
|
|||
@Composable
|
||||
private fun TabletSettingsScreen(
|
||||
page: SettingsPage,
|
||||
scrollToTopRequests: Flow<Unit>,
|
||||
onPageChange: (SettingsPage) -> Unit,
|
||||
showLoadingOverlay: Boolean,
|
||||
holdToSpeedEnabled: Boolean,
|
||||
|
|
@ -773,6 +801,11 @@ private fun TabletSettingsScreen(
|
|||
rootSearchRevealAnimating = false
|
||||
}
|
||||
}
|
||||
LaunchedEffect(scrollToTopRequests) {
|
||||
scrollToTopRequests.collect {
|
||||
listState.animateScrollToItem(0)
|
||||
}
|
||||
}
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
|
|
|
|||
Loading…
Reference in a new issue