Store remembered tabs in app database instead of preferences (#1541)

## Description
Moves storing the remembered tabs from app preferences to the app
database. This means deleting the user will remove the remembered tabs
and the app preferences file is smaller and faster to load.

The remembered tabs are migrated automatically when the app is updated.

There are no significant user facing changes in this PR.

### Related issues
N/A

### Testing
Emulator

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-06-08 13:45:38 -04:00 • committed by GitHub
parent a4ee1e5175
commit b49926f80b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1379 additions and 599 deletions

View file

@ -7,7 +7,10 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
@ -15,6 +18,9 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.unit.dp
import androidx.tv.material3.MaterialTheme
import com.github.damontecres.wholphin.ui.tryRequestFocus
import kotlinx.coroutines.delay
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
@Composable
fun CircularProgress(modifier: Modifier = Modifier) {
@ -58,3 +64,41 @@ fun LoadingPage(
)
}
}
/**
* Fill the space and take focus, showing a loading indicator after the specified delay
*/
@Composable
fun DelayedLoadingPage(
modifier: Modifier = Modifier,
focusEnabled: Boolean = true,
delay: Duration = 300.milliseconds,
) {
val focusRequester = remember { FocusRequester() }
if (focusEnabled) {
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
}
var show by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
delay(delay)
show = true
}
Box(
contentAlignment = Alignment.Center,
modifier =
modifier
.fillMaxSize()
.focusRequester(focusRequester)
.focusable(focusEnabled),
) {
if (show) {
CircularProgressIndicator(
color = MaterialTheme.colorScheme.border,
modifier =
Modifier
.align(Alignment.Center)
.size(48.dp),
)
}
}
}

View file

@ -29,7 +29,6 @@ import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
@ -42,26 +41,26 @@ import androidx.tv.material3.Text
import com.github.damontecres.wholphin.ui.PreviewTvSpec
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.ui.util.StringStringProvider
import timber.log.Timber
@Composable
fun TabRow(
selectedTabIndex: Int,
tabs: List<String>,
focusRequesters: List<FocusRequester>,
tabs: List<TabDetails>,
onClick: (Int) -> Unit,
modifier: Modifier = Modifier,
) {
val state = rememberLazyListState()
LaunchedEffect(selectedTabIndex) {
if (selectedTabIndex >= 0) {
if (selectedTabIndex in tabs.indices) {
state.animateScrollToItem(selectedTabIndex, -(state.layoutInfo.viewportSize.width / 3.5).toInt())
}
}
var rowHasFocus by remember { mutableStateOf(false) }
val currentSelectedTabIndex by rememberUpdatedState(selectedTabIndex)
val currentFocusRequesters by rememberUpdatedState(focusRequesters)
// val currentFocusRequesters by rememberUpdatedState(focusRequesters)
val currentOnClick by rememberUpdatedState(onClick)
LazyRow(
@ -76,21 +75,21 @@ fun TabRow(
// If entering from left or right, use last or first tab
// Otherwise use the selected tab
val index = currentSelectedTabIndex
val requesters = currentFocusRequesters
// val requesters = currentFocusRequesters
Timber.v("onEnter requestedFocusDirection=$requestedFocusDirection, selectedTabIndex=$index")
val focusRequester =
val tab =
if (requestedFocusDirection == FocusDirection.Left) {
requesters.lastOrNull()
tabs.lastOrNull()
} else if (requestedFocusDirection == FocusDirection.Right) {
requesters.firstOrNull()
tabs.firstOrNull()
} else {
requesters.getOrNull(index)
tabs.getOrNull(index)
}
(focusRequester ?: FocusRequester.Default).tryRequestFocus()
(tab?.tabFocusRequester ?: FocusRequester.Default).tryRequestFocus()
}
},
) {
itemsIndexed(tabs) { index, tabTitle ->
itemsIndexed(tabs) { index, tab ->
val interactionSource = remember { MutableInteractionSource() }
val onTabClick =
remember(index) {
@ -99,12 +98,12 @@ fun TabRow(
}
}
Tab(
title = tabTitle,
title = tab.title.getString(),
selected = index == selectedTabIndex,
rowActive = rowHasFocus,
interactionSource = interactionSource,
onClick = onTabClick,
modifier = Modifier.focusRequester(focusRequesters.getOrElse(index) { FocusRequester() }),
modifier = Modifier.focusRequester(tab.tabFocusRequester),
)
}
}
@ -195,9 +194,11 @@ private fun TabRowPreview() {
WholphinTheme {
Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
TabRow(
selectedTabIndex = 1,
tabs = listOf("Tab 1", "Tab 2", "Tab 3"),
focusRequesters = listOf(),
selectedTabIndex = -1,
tabs =
remember {
listOf("Tab 1", "Tab 2", "Tab 3").map { TabDetails(StringStringProvider(it)) }
},
onClick = {},
)
Tab(

View file

@ -0,0 +1,140 @@
package com.github.damontecres.wholphin.ui.components
import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.RememberedTabService
import com.github.damontecres.wholphin.services.UserPreferencesService
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.util.ResStringProvider
import com.github.damontecres.wholphin.ui.util.StringProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import timber.log.Timber
@HiltViewModel(assistedFactory = TabViewModel.Factory::class)
class TabViewModel
@AssistedInject
constructor(
private val userPreferencesService: UserPreferencesService,
private val rememberedTabService: RememberedTabService,
private val backdropService: BackdropService,
@param:Assisted private val itemId: String,
) : ViewModel() {
@AssistedFactory
interface Factory {
fun create(itemId: String): TabViewModel
}
private val _state = MutableStateFlow<Int>(UNSET)
val state: StateFlow<Int> = _state
init {
viewModelScope.launchIO {
val startingTab =
if (shouldRememberTabs()) {
Timber.v("Getting remembered tab for %s", itemId)
try {
rememberedTabService.getRememberedTab(itemId)
} catch (ex: Exception) {
Timber.e(ex, "Error getting tab for %s", itemId)
null
} ?: 0
} else {
0
}
_state.value = startingTab
}
}
private suspend fun shouldRememberTabs(): Boolean =
userPreferencesService
.getCurrent()
.appPreferences.interfacePreferences.rememberSelectedTab
fun updateSelectedTabIndex(tabIndex: Int) {
_state.value = tabIndex
viewModelScope.launchIO {
backdropService.clearBackdrop()
if (shouldRememberTabs()) {
Timber.v("Saving remembered tab for %s: %s", itemId, tabIndex)
rememberedTabService.saveRememberedTab(itemId, tabIndex)
}
}
}
}
data class TabDetails(
val title: StringProvider,
val tabFocusRequester: FocusRequester = FocusRequester(),
val contentFocusRequester: FocusRequester = FocusRequester(),
) {
constructor(
@StringRes stringResId: Int,
) : this(ResStringProvider(stringResId))
}
const val UNSET = -1234
/**
* A page showing multiple tabs
*
* This handles remembering the selected tabs via [TabViewModel] if needed
*/
@Composable
fun TabbedPage(
itemId: String,
tabs: List<TabDetails>,
modifier: Modifier = Modifier,
showTabs: Boolean = true,
viewModel: TabViewModel =
hiltViewModel<TabViewModel, TabViewModel.Factory>(
key = itemId,
creationCallback = { it.create(itemId) },
),
tabContent: @Composable (Int, TabDetails) -> Unit,
) {
val selectedTabIndex by viewModel.state.collectAsState()
Column(
modifier = modifier,
) {
AnimatedVisibility(
showTabs,
enter = expandVertically(),
exit = shrinkVertically(),
) {
TabRow(
selectedTabIndex = selectedTabIndex,
modifier =
Modifier
.padding(vertical = 16.dp),
tabs = tabs,
onClick = viewModel::updateSelectedTabIndex,
)
}
selectedTabIndex.let { tabIndex ->
if (tabIndex >= 0) {
tabContent.invoke(tabIndex, tabs[tabIndex])
} else {
DelayedLoadingPage(focusEnabled = false)
}
}
}
}

View file

@ -1,25 +1,15 @@
package com.github.damontecres.wholphin.ui.detail
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
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.focus.focusRequester
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@ -32,20 +22,23 @@ import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.ui.components.CollectionFolderView
import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.TabRow
import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.components.TabDetails
import com.github.damontecres.wholphin.ui.components.TabbedPage
import com.github.damontecres.wholphin.ui.components.ViewOptions
import com.github.damontecres.wholphin.ui.data.VideoSortOptions
import com.github.damontecres.wholphin.ui.detail.livetv.DvrSchedule
import com.github.damontecres.wholphin.ui.detail.livetv.TvGuideGrid
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.logTab
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.RememberTabManager
import com.github.damontecres.wholphin.ui.util.StringStringProvider
import com.github.damontecres.wholphin.util.DataLoadingState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.MutableStateFlow
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.liveTvApi
import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
@ -56,24 +49,35 @@ class LiveTvCollectionViewModel
val api: ApiClient,
val serverRepository: ServerRepository,
val navigationManager: NavigationManager,
val rememberTabManager: RememberTabManager,
val backdropService: BackdropService,
) : ViewModel(),
RememberTabManager by rememberTabManager {
val recordingFolders = MutableStateFlow<List<TabId>>(emptyList())
) : ViewModel() {
val state = MutableStateFlow(LiveTvRecordingFolderState())
init {
viewModelScope.launchIO {
val folders =
api.liveTvApi
.getRecordingFolders(userId = serverRepository.currentUser?.id)
.content.items
.map { TabId(it.name ?: "Recordings", it.id) }
recordingFolders.value = folders
try {
val folders =
api.liveTvApi
.getRecordingFolders(userId = serverRepository.currentUser?.id)
.content.items
.map { TabId(it.name ?: "Recordings", it.id) }
state.value =
LiveTvRecordingFolderState(DataLoadingState.Success(folders))
} catch (ex: CancellationException) {
throw ex
} catch (ex: Exception) {
Timber.e(ex, "Error fetching recording folders")
state.value =
LiveTvRecordingFolderState(DataLoadingState.Error(ex))
}
}
}
}
data class LiveTvRecordingFolderState(
val folders: DataLoadingState<List<TabId>> = DataLoadingState.Pending,
)
data class TabId(
val title: String,
val id: UUID,
@ -86,103 +90,91 @@ fun CollectionFolderLiveTv(
modifier: Modifier = Modifier,
viewModel: LiveTvCollectionViewModel = hiltViewModel(),
) {
val rememberedTabIndex =
remember { viewModel.getRememberedTab(preferences, destination.itemId, 0) }
val folders by viewModel.recordingFolders.collectAsState()
val state by viewModel.state.collectAsState()
val tvGuideStr = stringResource(R.string.tv_guide)
val tvDvrStr = stringResource(R.string.tv_dvr_schedule)
val tabs =
remember(folders) {
listOf(
TabId(tvGuideStr, UUID.randomUUID()),
TabId(tvDvrStr, UUID.randomUUID()),
) + folders
when (val st = state.folders) {
is DataLoadingState.Error -> {
ErrorMessage(st, modifier)
}
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
val focusRequester = remember { FocusRequester() }
val tabFocusRequesters = remember(tabs.size) { List(tabs.size) { FocusRequester() } }
val firstTabFocusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() }
LaunchedEffect(selectedTabIndex) {
logTab("livetv", selectedTabIndex)
viewModel.saveRememberedTab(preferences, destination.itemId, selectedTabIndex)
viewModel.backdropService.clearBackdrop()
}
val onClickItem = { position: Int, item: BaseItem ->
viewModel.navigationManager.navigateTo(item.destination())
}
var showHeader by rememberSaveable { mutableStateOf(true) }
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
Column(
modifier = modifier,
) {
AnimatedVisibility(
showHeader,
enter = expandVertically(),
exit = shrinkVertically(),
) {
TabRow(
selectedTabIndex = selectedTabIndex,
modifier =
Modifier
.padding(vertical = 16.dp)
.focusRequester(firstTabFocusRequester),
tabs = tabs.map { it.title },
onClick = { selectedTabIndex = it },
focusRequesters = tabFocusRequesters,
)
DataLoadingState.Loading,
DataLoadingState.Pending,
-> {
LoadingPage(modifier)
}
when (selectedTabIndex) {
0 -> {
TvGuideGrid(
preferences = preferences,
onRowPosition = {
showHeader = it <= 0
},
Modifier
.fillMaxSize()
.focusRequester(focusRequester),
)
}
1 -> {
DvrSchedule(
requestFocusAfterLoading = true,
focusRequesterOnEmpty = tabFocusRequesters[1],
modifier =
Modifier
.fillMaxSize()
.focusRequester(focusRequester),
)
}
is DataLoadingState.Success<List<TabId>> -> {
val tabs =
remember {
listOf(
TabDetails(R.string.tv_guide),
TabDetails(R.string.tv_dvr_schedule),
) +
st.data.map {
TabDetails(StringStringProvider(it.title))
}
}
val onClickItem =
remember {
{ position: Int, item: BaseItem ->
viewModel.navigationManager.navigateTo(item.destination())
}
}
else -> {
val folderIndex = selectedTabIndex - 2
if (folderIndex in folders.indices) {
CollectionFolderView(
preferences = preferences,
onClickItem = onClickItem,
itemId = folders[folderIndex].id,
initialFilter = CollectionFolderFilter(),
showTitle = false,
recursive = false,
sortOptions = VideoSortOptions,
modifier = Modifier,
positionCallback = { columns, position ->
showHeader = position < columns
},
playEnabled = false,
defaultViewOptions = ViewOptions(),
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
)
} else {
ErrorMessage("Invalid tab index $selectedTabIndex", null)
var showHeader by rememberSaveable { mutableStateOf(true) }
TabbedPage(
itemId = destination.itemId.toString(),
tabs = tabs,
modifier = modifier,
showTabs = showHeader,
) { tabIndex, tabDetails ->
when (tabIndex) {
0 -> {
TvGuideGrid(
preferences = preferences,
onRowPosition = {
showHeader = it <= 0
},
Modifier
.fillMaxSize()
.focusRequester(tabDetails.contentFocusRequester),
)
}
1 -> {
DvrSchedule(
requestFocusAfterLoading = true,
focusRequesterOnEmpty = tabDetails.tabFocusRequester,
modifier =
Modifier
.fillMaxSize()
.focusRequester(tabDetails.contentFocusRequester),
)
}
else -> {
val folderIndex = tabIndex - 2
if (folderIndex in st.data.indices) {
CollectionFolderView(
preferences = preferences,
onClickItem = onClickItem,
itemId = st.data[folderIndex].id,
initialFilter = CollectionFolderFilter(),
showTitle = false,
recursive = false,
sortOptions = VideoSortOptions,
modifier = Modifier.focusRequester(tabDetails.contentFocusRequester),
positionCallback = { columns, position ->
showHeader = position < columns
},
playEnabled = false,
defaultViewOptions = ViewOptions(),
focusRequesterOnEmpty = tabDetails.tabFocusRequester,
)
} else {
ErrorMessage("Invalid tab index $tabIndex", null)
}
}
}
}
}

View file

@ -1,24 +1,14 @@
package com.github.damontecres.wholphin.ui.detail
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
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.focus.focusRequester
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
@ -28,11 +18,11 @@ import com.github.damontecres.wholphin.ui.components.CollectionFolderView
import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.GenreCardGrid
import com.github.damontecres.wholphin.ui.components.RecommendedMovie
import com.github.damontecres.wholphin.ui.components.TabRow
import com.github.damontecres.wholphin.ui.components.TabDetails
import com.github.damontecres.wholphin.ui.components.TabbedPage
import com.github.damontecres.wholphin.ui.components.ViewOptionsPoster
import com.github.damontecres.wholphin.ui.data.MovieSortOptions
import com.github.damontecres.wholphin.ui.data.VideoSortOptions
import com.github.damontecres.wholphin.ui.logTab
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
import org.jellyfin.sdk.model.api.BaseItemKind
@ -45,51 +35,25 @@ fun CollectionFolderMovie(
modifier: Modifier = Modifier,
preferencesViewModel: PreferencesViewModel = hiltViewModel(),
) {
val rememberedTabIndex =
remember { preferencesViewModel.getRememberedTab(preferences, destination.itemId, 0) }
val tabs =
listOf(
stringResource(R.string.recommended),
stringResource(R.string.library),
stringResource(R.string.collections),
stringResource(R.string.genres),
)
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
val focusRequester = remember { FocusRequester() }
val tabFocusRequesters = remember { List(tabs.size) { FocusRequester() } }
val firstTabFocusRequester = remember { FocusRequester() }
// LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() }
LaunchedEffect(selectedTabIndex) {
logTab("movie", selectedTabIndex)
preferencesViewModel.saveRememberedTab(preferences, destination.itemId, selectedTabIndex)
preferencesViewModel.backdropService.clearBackdrop()
}
remember {
listOf(
TabDetails(R.string.recommended),
TabDetails(R.string.library),
TabDetails(R.string.collections),
TabDetails(R.string.genres),
)
}
var showHeader by rememberSaveable { mutableStateOf(true) }
Column(
TabbedPage(
itemId = destination.itemId.toString(),
tabs = tabs,
modifier = modifier,
) {
AnimatedVisibility(
showHeader,
enter = expandVertically(),
exit = shrinkVertically(),
) {
TabRow(
selectedTabIndex = selectedTabIndex,
modifier =
Modifier
.padding(vertical = 16.dp)
.focusRequester(firstTabFocusRequester),
tabs = tabs,
onClick = { selectedTabIndex = it },
focusRequesters = tabFocusRequesters,
)
}
when (selectedTabIndex) {
showTabs = showHeader,
) { tabIndex, tabDetails ->
when (tabIndex) {
// Recommended
0 -> {
RecommendedMovie(
@ -101,7 +65,7 @@ fun CollectionFolderMovie(
modifier =
Modifier
.fillMaxSize()
.focusRequester(focusRequester),
.focusRequester(tabDetails.contentFocusRequester),
)
}
@ -128,12 +92,12 @@ fun CollectionFolderMovie(
modifier =
Modifier
.fillMaxSize()
.focusRequester(focusRequester),
.focusRequester(tabDetails.contentFocusRequester),
positionCallback = { columns, position ->
showHeader = position < columns
},
playEnabled = true,
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
focusRequesterOnEmpty = tabDetails.tabFocusRequester,
)
}
@ -160,12 +124,12 @@ fun CollectionFolderMovie(
modifier =
Modifier
.fillMaxSize()
.focusRequester(focusRequester),
.focusRequester(tabDetails.contentFocusRequester),
positionCallback = { columns, position ->
showHeader = position < columns
},
playEnabled = false,
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
focusRequesterOnEmpty = tabDetails.tabFocusRequester,
)
}
@ -178,12 +142,12 @@ fun CollectionFolderMovie(
modifier =
Modifier
.fillMaxSize()
.focusRequester(focusRequester),
.focusRequester(tabDetails.contentFocusRequester),
)
}
else -> {
ErrorMessage("Invalid tab index $selectedTabIndex", null)
ErrorMessage("Invalid tab index $tabIndex", null)
}
}
}

View file

@ -1,24 +1,14 @@
package com.github.damontecres.wholphin.ui.detail
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
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.focus.focusRequester
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@ -36,17 +26,16 @@ import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.GenreCardGrid
import com.github.damontecres.wholphin.ui.components.GridClickActions
import com.github.damontecres.wholphin.ui.components.RecommendedMusic
import com.github.damontecres.wholphin.ui.components.TabRow
import com.github.damontecres.wholphin.ui.components.TabDetails
import com.github.damontecres.wholphin.ui.components.TabbedPage
import com.github.damontecres.wholphin.ui.components.ViewOptionsSquare
import com.github.damontecres.wholphin.ui.data.AlbumSortOptions
import com.github.damontecres.wholphin.ui.data.ArtistSortOptions
import com.github.damontecres.wholphin.ui.data.SongSortOptions
import com.github.damontecres.wholphin.ui.launchDefault
import com.github.damontecres.wholphin.ui.logTab
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.util.ApiRequestPager
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
import com.github.damontecres.wholphin.util.RememberTabManager
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -66,10 +55,8 @@ class CollectionFolderMusicViewModel
private val musicService: MusicService,
private val navigationManager: NavigationManager,
val backdropService: BackdropService,
private val rememberTabManager: RememberTabManager,
@Assisted private val itemId: UUID,
) : ViewModel(),
RememberTabManager by rememberTabManager {
) : ViewModel() {
@AssistedFactory
interface Factory {
fun create(itemId: UUID): CollectionFolderMusicViewModel
@ -132,30 +119,16 @@ fun CollectionFolderMusic(
creationCallback = { it.create(destination.itemId) },
),
) {
val rememberedTabIndex =
remember { viewModel.getRememberedTab(preferences, destination.itemId, 0) }
val tabs =
listOf(
stringResource(R.string.recommended),
stringResource(R.string.albums),
stringResource(R.string.artists),
stringResource(R.string.genres),
stringResource(R.string.songs),
)
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
val focusRequester = remember { FocusRequester() }
val tabFocusRequesters = remember { List(tabs.size) { FocusRequester() } }
val firstTabFocusRequester = remember { FocusRequester() }
// LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() }
LaunchedEffect(selectedTabIndex) {
logTab("music", selectedTabIndex)
viewModel.saveRememberedTab(preferences, destination.itemId, selectedTabIndex)
viewModel.backdropService.clearBackdrop()
}
remember {
listOf(
TabDetails(R.string.recommended),
TabDetails(R.string.albums),
TabDetails(R.string.artists),
TabDetails(R.string.genres),
TabDetails(R.string.songs),
)
}
var showHeader by rememberSaveable { mutableStateOf(true) }
val actions =
@ -167,27 +140,13 @@ fun CollectionFolderMusic(
onClickPlayRemoteButton = viewModel::onClickPlayRemoteButton,
)
}
Column(
TabbedPage(
itemId = destination.itemId.toString(),
tabs = tabs,
modifier = modifier,
) {
AnimatedVisibility(
showHeader,
enter = expandVertically(),
exit = shrinkVertically(),
) {
TabRow(
selectedTabIndex = selectedTabIndex,
modifier =
Modifier
.padding(vertical = 16.dp)
.focusRequester(firstTabFocusRequester),
tabs = tabs,
onClick = { selectedTabIndex = it },
focusRequesters = tabFocusRequesters,
)
}
when (selectedTabIndex) {
showTabs = showHeader,
) { tabIndex, tabDetails ->
when (tabIndex) {
// Recommended
0 -> {
RecommendedMusic(
@ -199,7 +158,7 @@ fun CollectionFolderMusic(
modifier =
Modifier
.fillMaxSize()
.focusRequester(focusRequester),
.focusRequester(tabDetails.contentFocusRequester),
)
}
@ -224,12 +183,12 @@ fun CollectionFolderMusic(
modifier =
Modifier
.fillMaxSize()
.focusRequester(focusRequester),
.focusRequester(tabDetails.contentFocusRequester),
positionCallback = { columns, position ->
showHeader = position < columns
},
playEnabled = true,
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
focusRequesterOnEmpty = tabDetails.tabFocusRequester,
)
}
@ -254,12 +213,12 @@ fun CollectionFolderMusic(
modifier =
Modifier
.fillMaxSize()
.focusRequester(focusRequester),
.focusRequester(tabDetails.contentFocusRequester),
positionCallback = { columns, position ->
showHeader = position < columns
},
playEnabled = false,
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
focusRequesterOnEmpty = tabDetails.tabFocusRequester,
)
}
@ -272,7 +231,7 @@ fun CollectionFolderMusic(
modifier =
Modifier
.fillMaxSize()
.focusRequester(focusRequester),
.focusRequester(tabDetails.contentFocusRequester),
)
}
@ -297,17 +256,17 @@ fun CollectionFolderMusic(
modifier =
Modifier
.fillMaxSize()
.focusRequester(focusRequester),
.focusRequester(tabDetails.contentFocusRequester),
positionCallback = { columns, position ->
showHeader = position < columns
},
playEnabled = true,
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
focusRequesterOnEmpty = tabDetails.tabFocusRequester,
)
}
else -> {
ErrorMessage("Invalid tab index $selectedTabIndex", null)
ErrorMessage("Invalid tab index $tabIndex", null)
}
}
}

View file

@ -1,102 +1,77 @@
package com.github.damontecres.wholphin.ui.detail
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
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.focus.focusRequester
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.filter.DefaultTvFilterOptions
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
import com.github.damontecres.wholphin.data.model.GetItemsFilter
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.ui.components.CollectionFolderView
import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.GenreCardGrid
import com.github.damontecres.wholphin.ui.components.RecommendedTvShow
import com.github.damontecres.wholphin.ui.components.StudioCardGrid
import com.github.damontecres.wholphin.ui.components.TabRow
import com.github.damontecres.wholphin.ui.components.TabDetails
import com.github.damontecres.wholphin.ui.components.TabbedPage
import com.github.damontecres.wholphin.ui.components.ViewOptionsPoster
import com.github.damontecres.wholphin.ui.data.SeriesSortOptions
import com.github.damontecres.wholphin.ui.logTab
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
import com.github.damontecres.wholphin.ui.tryRequestFocus
import dagger.hilt.android.lifecycle.HiltViewModel
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.CollectionType
import javax.inject.Inject
@HiltViewModel
class CollectionFolderTvViewModel
@Inject
constructor(
val navigationManager: NavigationManager,
) : ViewModel()
@Composable
fun CollectionFolderTv(
preferences: UserPreferences,
destination: Destination.MediaItem,
modifier: Modifier = Modifier,
preferencesViewModel: PreferencesViewModel = hiltViewModel(),
viewModel: CollectionFolderTvViewModel = hiltViewModel(),
) {
val rememberedTabIndex =
remember { preferencesViewModel.getRememberedTab(preferences, destination.itemId, 0) }
val tabs =
listOf(
stringResource(R.string.recommended),
stringResource(R.string.library),
stringResource(R.string.genres),
stringResource(R.string.studios),
)
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
val focusRequester = remember { FocusRequester() }
val tabFocusRequesters = remember { List(tabs.size) { FocusRequester() } }
val firstTabFocusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() }
LaunchedEffect(selectedTabIndex) {
logTab("tv", selectedTabIndex)
preferencesViewModel.saveRememberedTab(preferences, destination.itemId, selectedTabIndex)
preferencesViewModel.backdropService.clearBackdrop()
}
val onClickItem = { item: BaseItem ->
preferencesViewModel.navigationManager.navigateTo(item.destination())
}
remember {
listOf(
TabDetails(R.string.recommended),
TabDetails(R.string.library),
TabDetails(R.string.genres),
TabDetails(R.string.studios),
)
}
val onClickItem =
remember {
{ position: Int, item: BaseItem ->
viewModel.navigationManager.navigateTo(item.destination())
}
}
var showHeader by rememberSaveable { mutableStateOf(true) }
Column(
TabbedPage(
itemId = destination.itemId.toString(),
tabs = tabs,
modifier = modifier,
) {
AnimatedVisibility(
showHeader,
enter = expandVertically(),
exit = shrinkVertically(),
) {
TabRow(
selectedTabIndex = selectedTabIndex,
modifier =
Modifier
.padding(vertical = 16.dp)
.focusRequester(firstTabFocusRequester),
tabs = tabs,
onClick = { selectedTabIndex = it },
focusRequesters = tabFocusRequesters,
)
}
when (selectedTabIndex) {
showTabs = showHeader,
) { tabIndex, tabDetails ->
when (tabIndex) {
// Recommended
0 -> {
RecommendedTvShow(
@ -108,7 +83,7 @@ fun CollectionFolderTv(
modifier =
Modifier
.fillMaxSize()
.focusRequester(focusRequester),
.focusRequester(tabDetails.contentFocusRequester),
)
}
@ -132,15 +107,13 @@ fun CollectionFolderTv(
modifier =
Modifier
.fillMaxSize()
.focusRequester(focusRequester),
.focusRequester(tabDetails.contentFocusRequester),
positionCallback = { columns, position ->
showHeader = position < columns
},
onClickItem = { _, item ->
preferencesViewModel.navigationManager.navigateTo(item.destination())
},
onClickItem = onClickItem,
playEnabled = false,
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
focusRequesterOnEmpty = tabDetails.tabFocusRequester,
)
}
@ -153,7 +126,7 @@ fun CollectionFolderTv(
modifier =
Modifier
.fillMaxSize()
.focusRequester(focusRequester),
.focusRequester(tabDetails.contentFocusRequester),
)
}
@ -165,12 +138,12 @@ fun CollectionFolderTv(
modifier =
Modifier
.fillMaxSize()
.focusRequester(focusRequester),
.focusRequester(tabDetails.contentFocusRequester),
)
}
else -> {
ErrorMessage("Invalid tab index $selectedTabIndex", null)
ErrorMessage("Invalid tab index $tabIndex", null)
}
}
}

View file

@ -1,36 +1,30 @@
package com.github.damontecres.wholphin.ui.detail
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
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.focus.focusRequester
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.filter.DefaultForFavoritesFilterOptions
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
import com.github.damontecres.wholphin.data.model.GetItemsFilter
import com.github.damontecres.wholphin.data.model.GetItemsFilterOverride
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.ui.components.CollectionFolderView
import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.GridClickActions
import com.github.damontecres.wholphin.ui.components.TabRow
import com.github.damontecres.wholphin.ui.components.TabDetails
import com.github.damontecres.wholphin.ui.components.TabbedPage
import com.github.damontecres.wholphin.ui.components.ViewOptionsPoster
import com.github.damontecres.wholphin.ui.components.ViewOptionsSquare
import com.github.damontecres.wholphin.ui.components.ViewOptionsWide
@ -39,86 +33,55 @@ import com.github.damontecres.wholphin.ui.data.MovieSortOptions
import com.github.damontecres.wholphin.ui.data.SeriesSortOptions
import com.github.damontecres.wholphin.ui.data.SortAndDirection
import com.github.damontecres.wholphin.ui.data.VideoSortOptions
import com.github.damontecres.wholphin.ui.logTab
import com.github.damontecres.wholphin.ui.nav.NavDrawerItem
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
import com.github.damontecres.wholphin.ui.tryRequestFocus
import dagger.hilt.android.lifecycle.HiltViewModel
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ItemSortBy
import org.jellyfin.sdk.model.api.SortOrder
import javax.inject.Inject
@HiltViewModel
class FavoritesViewModel
@Inject
constructor(
val navigationManager: NavigationManager,
) : ViewModel()
@Composable
fun FavoritesPage(
preferences: UserPreferences,
modifier: Modifier = Modifier,
preferencesViewModel: PreferencesViewModel = hiltViewModel(),
viewModel: FavoritesViewModel = hiltViewModel(),
) {
val uiPrefs = preferences.appPreferences.interfacePreferences
val rememberedTabIndex =
val tabs =
remember {
preferencesViewModel.getRememberedTab(
preferences,
NavDrawerItem.Favorites.id,
0,
listOf(
TabDetails(R.string.movies),
TabDetails(R.string.tv_shows),
TabDetails(R.string.episodes),
TabDetails(R.string.videos),
TabDetails(R.string.playlists),
TabDetails(R.string.people),
)
}
val actions =
remember {
GridClickActions(
onClickItem = { _, item -> viewModel.navigationManager.navigateTo(item.destination()) },
)
}
val tabs =
listOf(
stringResource(R.string.movies),
stringResource(R.string.tv_shows),
stringResource(R.string.episodes),
stringResource(R.string.videos),
stringResource(R.string.playlists),
stringResource(R.string.people),
)
val tabFocusRequesters = remember { List(tabs.size) { FocusRequester() } }
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
val focusRequester = remember { FocusRequester() }
LaunchedEffect(selectedTabIndex) {
logTab("favorites", selectedTabIndex)
preferencesViewModel.saveRememberedTab(
preferences,
NavDrawerItem.Favorites.id,
selectedTabIndex,
)
preferencesViewModel.backdropService.clearBackdrop()
}
var showHeader by rememberSaveable { mutableStateOf(true) }
val onClickItem = { item: BaseItem ->
preferencesViewModel.navigationManager.navigateTo(item.destination())
}
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
Column(
// LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
TabbedPage(
itemId = NavDrawerItem.Favorites.id,
tabs = tabs,
showTabs = showHeader,
modifier = modifier,
) {
AnimatedVisibility(
showHeader,
enter = expandVertically(),
exit = shrinkVertically(),
) {
TabRow(
selectedTabIndex = selectedTabIndex,
modifier =
Modifier
.padding(start = 32.dp, top = 16.dp, bottom = 16.dp),
tabs = tabs,
onClick = { selectedTabIndex = it },
focusRequesters = tabFocusRequesters,
)
}
val actions =
remember {
GridClickActions(
onClickItem = { _, item -> onClickItem.invoke(item) },
)
}
) { tabIndex, tabDetails ->
// TODO playEnabled = true for movies & episodes
when (selectedTabIndex) {
when (tabIndex) {
// Movies
0 -> {
CollectionFolderView(
@ -141,13 +104,13 @@ fun FavoritesPage(
Modifier
.padding(start = 16.dp)
.fillMaxSize()
.focusRequester(focusRequester),
.focusRequester(tabDetails.contentFocusRequester),
positionCallback = { columns, position ->
showHeader = position < columns
},
playEnabled = false,
filterOptions = DefaultForFavoritesFilterOptions,
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
focusRequesterOnEmpty = tabDetails.tabFocusRequester,
)
}
@ -173,13 +136,13 @@ fun FavoritesPage(
Modifier
.padding(start = 16.dp)
.fillMaxSize()
.focusRequester(focusRequester),
.focusRequester(tabDetails.contentFocusRequester),
positionCallback = { columns, position ->
showHeader = position < columns
},
playEnabled = false,
filterOptions = DefaultForFavoritesFilterOptions,
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
focusRequesterOnEmpty = tabDetails.tabFocusRequester,
)
}
@ -206,13 +169,13 @@ fun FavoritesPage(
Modifier
.padding(start = 16.dp)
.fillMaxSize()
.focusRequester(focusRequester),
.focusRequester(tabDetails.contentFocusRequester),
positionCallback = { columns, position ->
showHeader = position < columns
},
playEnabled = false,
filterOptions = DefaultForFavoritesFilterOptions,
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
focusRequesterOnEmpty = tabDetails.tabFocusRequester,
)
}
@ -238,13 +201,13 @@ fun FavoritesPage(
Modifier
.padding(start = 16.dp)
.fillMaxSize()
.focusRequester(focusRequester),
.focusRequester(tabDetails.contentFocusRequester),
positionCallback = { columns, position ->
showHeader = position < columns
},
playEnabled = false,
filterOptions = DefaultForFavoritesFilterOptions,
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
focusRequesterOnEmpty = tabDetails.tabFocusRequester,
)
}
@ -270,13 +233,13 @@ fun FavoritesPage(
Modifier
.padding(start = 16.dp)
.fillMaxSize()
.focusRequester(focusRequester),
.focusRequester(tabDetails.contentFocusRequester),
positionCallback = { columns, position ->
showHeader = position < columns
},
playEnabled = false,
filterOptions = DefaultForFavoritesFilterOptions,
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
focusRequesterOnEmpty = tabDetails.tabFocusRequester,
)
}
@ -307,18 +270,18 @@ fun FavoritesPage(
Modifier
.padding(start = 16.dp)
.fillMaxSize()
.focusRequester(focusRequester),
.focusRequester(tabDetails.contentFocusRequester),
positionCallback = { columns, position ->
showHeader = position < columns
},
playEnabled = false,
filterOptions = listOf(),
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
focusRequesterOnEmpty = tabDetails.tabFocusRequester,
)
}
else -> {
ErrorMessage("Invalid tab index $selectedTabIndex", null)
ErrorMessage("Invalid tab index $tabIndex", null)
}
}
}

View file

@ -57,6 +57,7 @@ import com.github.damontecres.wholphin.ui.cards.PersonRow
import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.HeaderUtils
import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.components.TabDetails
import com.github.damontecres.wholphin.ui.components.TabRow
import com.github.damontecres.wholphin.ui.components.TitleOrLogo
import com.github.damontecres.wholphin.ui.ifElse
@ -64,6 +65,7 @@ import com.github.damontecres.wholphin.ui.logTab
import com.github.damontecres.wholphin.ui.playback.isPlayKeyUp
import com.github.damontecres.wholphin.ui.rememberInt
import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.ui.util.StringStringProvider
import com.github.damontecres.wholphin.ui.util.rememberDelayedNestedScroll
import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.MediaSourceInfo
@ -120,13 +122,21 @@ fun SeriesOverviewContent(
var requestFocusAfterSeason by remember { mutableStateOf(false) }
val seasonStr = stringResource(R.string.tv_season)
val tabFocusRequesters = remember(seasons) { List(seasons.size) { FocusRequester() } }
val contentFocusRequesters = remember(seasons) { List(seasons.size) { FocusRequester() } }
val tabs =
seasons.map { season ->
season?.name
?: season?.data?.indexNumber?.let { "$seasonStr $it" }
?: ""
seasons.mapIndexed { index, season ->
TabDetails(
title =
StringStringProvider(
season?.name
?: season?.data?.indexNumber?.let { "$seasonStr $it" }
?: "",
),
tabFocusRequester = tabFocusRequesters[index],
contentFocusRequester = contentFocusRequesters[index],
)
}
val focusRequesters = remember(seasons) { List(seasons.size) { FocusRequester() } }
val currentOnChangeSeason by rememberUpdatedState(onChangeSeason)
@ -172,7 +182,6 @@ fun SeriesOverviewContent(
requestFocusAfterSeason = true
}
},
focusRequesters = focusRequesters,
modifier =
Modifier
.focusRequester(tabRowFocusRequester)

View file

@ -1,111 +1,78 @@
package com.github.damontecres.wholphin.ui.discover
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
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.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.compose.ui.focus.focusRequester
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.TabRow
import com.github.damontecres.wholphin.ui.logTab
import com.github.damontecres.wholphin.ui.components.TabDetails
import com.github.damontecres.wholphin.ui.components.TabbedPage
import com.github.damontecres.wholphin.ui.nav.NavDrawerItem
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
@Composable
fun DiscoverPage(
preferences: UserPreferences,
modifier: Modifier = Modifier,
preferencesViewModel: PreferencesViewModel = hiltViewModel(),
) {
val rememberedTabIndex =
remember { preferencesViewModel.getRememberedTab(preferences, NavDrawerItem.Discover.id, 0) }
val tabs =
listOf(
stringResource(R.string.discover),
stringResource(R.string.request),
stringResource(R.string.search),
)
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
val tabFocusRequesters = remember(tabs) { List(tabs.size) { FocusRequester() } }
LaunchedEffect(selectedTabIndex) {
logTab("discover", selectedTabIndex)
preferencesViewModel.saveRememberedTab(preferences, NavDrawerItem.Discover.id, selectedTabIndex)
}
OneTimeLaunchedEffect { preferencesViewModel.backdropService.clearBackdrop() }
var showHeader by rememberSaveable { mutableStateOf(true) }
Column(
modifier = modifier,
) {
AnimatedVisibility(
showHeader,
enter = expandVertically(),
exit = shrinkVertically(),
) {
TabRow(
selectedTabIndex = selectedTabIndex,
modifier =
Modifier
.padding(start = 32.dp, top = 16.dp, bottom = 16.dp),
tabs = tabs,
onClick = { selectedTabIndex = it },
focusRequesters = tabFocusRequesters,
remember {
listOf(
TabDetails(R.string.discover),
TabDetails(R.string.request),
TabDetails(R.string.search),
)
}
when (selectedTabIndex) {
var showHeader by rememberSaveable { mutableStateOf(true) }
TabbedPage(
itemId = NavDrawerItem.Discover.id,
tabs = tabs,
modifier = modifier,
showTabs = showHeader,
) { tabIndex, tabDetails ->
when (tabIndex) {
// Discover
0 -> {
SeerrDiscoverPage(
preferences = preferences,
modifier =
Modifier
.fillMaxSize(),
.fillMaxSize()
.focusRequester(tabDetails.contentFocusRequester),
)
}
// Requests
1 -> {
SeerrRequestsPage(
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
focusRequesterOnEmpty = tabDetails.tabFocusRequester,
modifier =
Modifier
.fillMaxSize(),
.fillMaxSize()
.focusRequester(tabDetails.contentFocusRequester),
)
}
// Search
2 -> {
LaunchedEffect(Unit) {
preferencesViewModel.backdropService.clearBackdrop()
}
DiscoverSearchPage(
preferences = preferences,
modifier = Modifier.fillMaxSize(),
modifier =
Modifier
.fillMaxSize()
.focusRequester(tabDetails.contentFocusRequester),
)
}
else -> {
ErrorMessage("Invalid tab index $selectedTabIndex", null)
ErrorMessage("Invalid tab index $tabIndex", null)
}
}
}

View file

@ -86,6 +86,7 @@ import com.github.damontecres.wholphin.ui.cards.ItemRowTitle
import com.github.damontecres.wholphin.ui.cards.SeasonCard
import com.github.damontecres.wholphin.ui.components.ExpandableFaButton
import com.github.damontecres.wholphin.ui.components.SearchEditTextBox
import com.github.damontecres.wholphin.ui.components.TabDetails
import com.github.damontecres.wholphin.ui.components.TabRow
import com.github.damontecres.wholphin.ui.components.VoiceInputManager
import com.github.damontecres.wholphin.ui.components.VoiceSearchButton
@ -518,6 +519,15 @@ fun SearchPage(
}
}
}
val tabs =
remember {
listOf(
TabDetails(R.string.library),
TabDetails(R.string.discover),
)
}
Column(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(12.dp),
@ -610,12 +620,7 @@ fun SearchPage(
) {
TabRow(
selectedTabIndex = selectedTab,
tabs =
listOf(
stringResource(R.string.library),
stringResource(R.string.discover),
),
focusRequesters = tabFocusRequesters,
tabs = tabs,
onClick = {
selectedTab = it
val row =

View file

@ -359,6 +359,7 @@ fun DestinationContent(
}
Destination.Discover -> {
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
DiscoverPage(
preferences = preferences,
modifier = modifier,

View file

@ -28,7 +28,6 @@ import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.util.DataLoadingState
import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState
import com.github.damontecres.wholphin.util.RememberTabManager
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow
@ -52,14 +51,12 @@ class PreferencesViewModel
val navigationManager: NavigationManager,
val backdropService: BackdropService,
val screensaverService: ScreensaverService,
private val rememberTabManager: RememberTabManager,
private val serverRepository: ServerRepository,
private val seerrServerRepository: SeerrServerRepository,
private val deviceInfo: DeviceInfo,
private val clientInfo: ClientInfo,
private val updateChecker: UpdateChecker,
) : ViewModel(),
RememberTabManager by rememberTabManager {
) : ViewModel() {
val currentUser
get() =
serverRepository.currentUserFlow.stateIn(