Better tab UI & remember tab setting

This commit is contained in:
Damontecres 2025-10-07 16:22:28 -04:00
parent c642636da7
commit 88038bfcfd
No known key found for this signature in database
7 changed files with 139 additions and 15 deletions

View file

@ -254,6 +254,18 @@ sealed interface AppPreference<T> {
}, },
) )
val RememberSelectedTab =
AppSwitchPreference(
title = R.string.remember_selected_tab,
defaultValue = false,
getter = { it.interfacePreferences.rememberSelectedTab },
setter = { prefs, value ->
prefs.updateInterfacePreferences { rememberSelectedTab = value }
},
summaryOn = R.string.enabled,
summaryOff = R.string.disabled,
)
val InstalledVersion = val InstalledVersion =
AppClickablePreference( AppClickablePreference(
title = R.string.installed_version, title = R.string.installed_version,
@ -301,6 +313,7 @@ val basicPreferences =
AppPreference.HomePageItems, AppPreference.HomePageItems,
AppPreference.RewatchNextUp, AppPreference.RewatchNextUp,
AppPreference.PlayThemeMusic, AppPreference.PlayThemeMusic,
AppPreference.RememberSelectedTab,
), ),
), ),
PreferenceGroup( PreferenceGroup(

View file

@ -3,8 +3,10 @@ package com.github.damontecres.dolphin.preferences
import androidx.datastore.core.CorruptionException import androidx.datastore.core.CorruptionException
import androidx.datastore.core.Serializer import androidx.datastore.core.Serializer
import com.google.protobuf.InvalidProtocolBufferException import com.google.protobuf.InvalidProtocolBufferException
import timber.log.Timber
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@ -77,3 +79,13 @@ inline fun AppPreferences.updateInterfacePreferences(block: InterfacePreferences
update { update {
interfacePreferences = interfacePreferences.toBuilder().apply(block).build() interfacePreferences = interfacePreferences.toBuilder().apply(block).build()
} }
fun AppPreferences.rememberTab(
itemId: UUID,
index: Int,
): AppPreferences {
Timber.v("Updating $itemId tab to $index")
return updateInterfacePreferences {
putRememberedTabs(itemId.toString(), index)
}
}

View file

@ -14,17 +14,23 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Tab import androidx.tv.material3.Tab
import androidx.tv.material3.TabDefaults
import androidx.tv.material3.TabRow import androidx.tv.material3.TabRow
import androidx.tv.material3.TabRowDefaults
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.dolphin.preferences.UserPreferences import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.preferences.rememberTab
import com.github.damontecres.dolphin.ui.components.ErrorMessage import com.github.damontecres.dolphin.ui.components.ErrorMessage
import com.github.damontecres.dolphin.ui.components.RecommendedMovie import com.github.damontecres.dolphin.ui.components.RecommendedMovie
import com.github.damontecres.dolphin.ui.ifElse import com.github.damontecres.dolphin.ui.ifElse
import com.github.damontecres.dolphin.ui.nav.Destination import com.github.damontecres.dolphin.ui.nav.Destination
import com.github.damontecres.dolphin.ui.nav.NavigationManager import com.github.damontecres.dolphin.ui.nav.NavigationManager
import com.github.damontecres.dolphin.ui.preferences.PreferencesViewModel
import com.github.damontecres.dolphin.ui.tryRequestFocus import com.github.damontecres.dolphin.ui.tryRequestFocus
@Composable @Composable
@ -33,29 +39,71 @@ fun CollectionFolderMovie(
navigationManager: NavigationManager, navigationManager: NavigationManager,
destination: Destination.MediaItem, destination: Destination.MediaItem,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
preferencesViewModel: PreferencesViewModel = hiltViewModel(),
) { ) {
val uiPrefs = preferences.appPreferences.interfacePreferences
val rememberedTabIndex =
if (uiPrefs.rememberSelectedTab) {
uiPrefs.getRememberedTabsOrDefault(destination.itemId.toString(), 0)
} else {
0
}
val tabs = listOf("Recommended", "Library") val tabs = listOf("Recommended", "Library")
var selectedTabIndex by rememberSaveable { mutableIntStateOf(0) } var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val firstTabFocusRequester = remember { FocusRequester() } val firstTabFocusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() } LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() }
if (uiPrefs.rememberSelectedTab) {
LaunchedEffect(selectedTabIndex) {
preferencesViewModel.preferenceDataStore.updateData {
preferences.appPreferences.rememberTab(destination.itemId, selectedTabIndex)
}
}
}
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
Column( Column(
modifier = modifier, modifier = modifier,
) { ) {
TabRow( TabRow(
selectedTabIndex = selectedTabIndex, selectedTabIndex = focusTabIndex,
modifier = modifier =
Modifier Modifier
.padding(start = 32.dp, top = 16.dp) .padding(start = 32.dp, top = 16.dp, bottom = 16.dp)
.focusRestorer(firstTabFocusRequester), .focusRestorer(firstTabFocusRequester)
.onFocusChanged {
if (!it.isFocused) {
focusTabIndex = selectedTabIndex
}
},
indicator =
@Composable { tabPositions, doesTabRowHaveFocus ->
tabPositions.getOrNull(focusTabIndex)?.let { currentTabPosition ->
// TabRowDefaults.PillIndicator(
// currentTabPosition = currentTabPosition,
// doesTabRowHaveFocus = doesTabRowHaveFocus,
// )
TabRowDefaults.UnderlinedIndicator(
currentTabPosition = currentTabPosition,
doesTabRowHaveFocus = doesTabRowHaveFocus,
activeColor = MaterialTheme.colorScheme.border,
)
}
},
tabs = { tabs = {
tabs.forEachIndexed { index, title -> tabs.forEachIndexed { index, title ->
Tab( Tab(
selected = selectedTabIndex == index, selected = focusTabIndex == index,
onClick = { selectedTabIndex = index }, onClick = { selectedTabIndex = index },
onFocus = {}, onFocus = { focusTabIndex = index },
colors =
TabDefaults.pillIndicatorTabColors(
focusedContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
),
) { ) {
Text( Text(
text = title, text = title,
@ -64,7 +112,7 @@ fun CollectionFolderMovie(
Modifier Modifier
.padding(8.dp) .padding(8.dp)
.ifElse( .ifElse(
index == 0, index == selectedTabIndex,
Modifier.focusRequester(firstTabFocusRequester), Modifier.focusRequester(firstTabFocusRequester),
), ),
) )

View file

@ -14,17 +14,23 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Tab import androidx.tv.material3.Tab
import androidx.tv.material3.TabDefaults
import androidx.tv.material3.TabRow import androidx.tv.material3.TabRow
import androidx.tv.material3.TabRowDefaults
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.dolphin.preferences.UserPreferences import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.preferences.rememberTab
import com.github.damontecres.dolphin.ui.components.ErrorMessage import com.github.damontecres.dolphin.ui.components.ErrorMessage
import com.github.damontecres.dolphin.ui.components.RecommendedTvShow import com.github.damontecres.dolphin.ui.components.RecommendedTvShow
import com.github.damontecres.dolphin.ui.ifElse import com.github.damontecres.dolphin.ui.ifElse
import com.github.damontecres.dolphin.ui.nav.Destination import com.github.damontecres.dolphin.ui.nav.Destination
import com.github.damontecres.dolphin.ui.nav.NavigationManager import com.github.damontecres.dolphin.ui.nav.NavigationManager
import com.github.damontecres.dolphin.ui.preferences.PreferencesViewModel
import com.github.damontecres.dolphin.ui.tryRequestFocus import com.github.damontecres.dolphin.ui.tryRequestFocus
@Composable @Composable
@ -33,29 +39,71 @@ fun CollectionFolderTv(
navigationManager: NavigationManager, navigationManager: NavigationManager,
destination: Destination.MediaItem, destination: Destination.MediaItem,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
preferencesViewModel: PreferencesViewModel = hiltViewModel(),
) { ) {
val uiPrefs = preferences.appPreferences.interfacePreferences
val rememberedTabIndex =
if (uiPrefs.rememberSelectedTab) {
uiPrefs.getRememberedTabsOrDefault(destination.itemId.toString(), 0)
} else {
0
}
val tabs = listOf("Recommended", "Library") val tabs = listOf("Recommended", "Library")
var selectedTabIndex by rememberSaveable { mutableIntStateOf(0) } var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val firstTabFocusRequester = remember { FocusRequester() } val firstTabFocusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() } LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() }
if (uiPrefs.rememberSelectedTab) {
LaunchedEffect(selectedTabIndex) {
preferencesViewModel.preferenceDataStore.updateData {
preferences.appPreferences.rememberTab(destination.itemId, selectedTabIndex)
}
}
}
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
Column( Column(
modifier = modifier, modifier = modifier,
) { ) {
TabRow( TabRow(
selectedTabIndex = selectedTabIndex, selectedTabIndex = focusTabIndex,
modifier = modifier =
Modifier Modifier
.padding(start = 32.dp, top = 16.dp) .padding(start = 32.dp, top = 16.dp, bottom = 16.dp)
.focusRestorer(firstTabFocusRequester), .focusRestorer(firstTabFocusRequester)
.onFocusChanged {
if (!it.isFocused) {
focusTabIndex = selectedTabIndex
}
},
indicator =
@Composable { tabPositions, doesTabRowHaveFocus ->
tabPositions.getOrNull(focusTabIndex)?.let { currentTabPosition ->
// TabRowDefaults.PillIndicator(
// currentTabPosition = currentTabPosition,
// doesTabRowHaveFocus = doesTabRowHaveFocus,
// )
TabRowDefaults.UnderlinedIndicator(
currentTabPosition = currentTabPosition,
doesTabRowHaveFocus = doesTabRowHaveFocus,
activeColor = MaterialTheme.colorScheme.border,
)
}
},
tabs = { tabs = {
tabs.forEachIndexed { index, title -> tabs.forEachIndexed { index, title ->
Tab( Tab(
selected = selectedTabIndex == index, selected = focusTabIndex == index,
onClick = { selectedTabIndex = index }, onClick = { selectedTabIndex = index },
onFocus = {}, onFocus = { focusTabIndex = index },
colors =
TabDefaults.pillIndicatorTabColors(
focusedContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
),
) { ) {
Text( Text(
text = title, text = title,
@ -64,7 +112,7 @@ fun CollectionFolderTv(
Modifier Modifier
.padding(8.dp) .padding(8.dp)
.ifElse( .ifElse(
index == 0, index == selectedTabIndex,
Modifier.focusRequester(firstTabFocusRequester), Modifier.focusRequester(firstTabFocusRequester),
), ),
) )

View file

@ -68,7 +68,7 @@ data class HomeRow(
fun HomePage( fun HomePage(
preferences: UserPreferences, preferences: UserPreferences,
navigationManager: NavigationManager, navigationManager: NavigationManager,
modifier: Modifier, modifier: Modifier = Modifier,
viewModel: HomeViewModel = hiltViewModel(), viewModel: HomeViewModel = hiltViewModel(),
) { ) {
OneTimeLaunchedEffect { OneTimeLaunchedEffect {

View file

@ -30,6 +30,8 @@ enum ThemeSongVolume {
message InterfacePreferences { message InterfacePreferences {
ThemeSongVolume play_theme_songs = 1; ThemeSongVolume play_theme_songs = 1;
bool remember_selected_tab = 2;
map<string, int32> remembered_tabs = 3;
} }
message AppPreferences { message AppPreferences {

View file

@ -59,5 +59,6 @@
<string name="pref_key_update_last_check_threshold">preference.update.threshold</string> <string name="pref_key_update_last_check_threshold">preference.update.threshold</string>
<string name="pref_key_update_last_check">preference.update.lastTimestamp</string> <string name="pref_key_update_last_check">preference.update.lastTimestamp</string>
<string name="confirm">Confirm</string> <string name="confirm">Confirm</string>
<string name="remember_selected_tab">Remember selected tabs</string>
</resources> </resources>