From 88038bfcfd45218242ad4b9ddfa3b7dab9df6a16 Mon Sep 17 00:00:00 2001 From: Damontecres Date: Tue, 7 Oct 2025 16:22:28 -0400 Subject: [PATCH] Better tab UI & remember tab setting --- .../dolphin/preferences/AppPreference.kt | 13 ++++ .../preferences/AppPreferencesSerializer.kt | 12 ++++ .../ui/detail/CollectionFolderMovie.kt | 62 ++++++++++++++++--- .../dolphin/ui/detail/CollectionFolderTv.kt | 62 ++++++++++++++++--- .../damontecres/dolphin/ui/main/HomePage.kt | 2 +- app/src/main/proto/DolphinDataStore.proto | 2 + app/src/main/res/values/strings.xml | 1 + 7 files changed, 139 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/dolphin/preferences/AppPreference.kt b/app/src/main/java/com/github/damontecres/dolphin/preferences/AppPreference.kt index a2bb8308..3fb86b10 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/preferences/AppPreference.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/preferences/AppPreference.kt @@ -254,6 +254,18 @@ sealed interface AppPreference { }, ) + 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 = AppClickablePreference( title = R.string.installed_version, @@ -301,6 +313,7 @@ val basicPreferences = AppPreference.HomePageItems, AppPreference.RewatchNextUp, AppPreference.PlayThemeMusic, + AppPreference.RememberSelectedTab, ), ), PreferenceGroup( diff --git a/app/src/main/java/com/github/damontecres/dolphin/preferences/AppPreferencesSerializer.kt b/app/src/main/java/com/github/damontecres/dolphin/preferences/AppPreferencesSerializer.kt index 17a76b35..9cb09c05 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/preferences/AppPreferencesSerializer.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/preferences/AppPreferencesSerializer.kt @@ -3,8 +3,10 @@ package com.github.damontecres.dolphin.preferences import androidx.datastore.core.CorruptionException import androidx.datastore.core.Serializer import com.google.protobuf.InvalidProtocolBufferException +import timber.log.Timber import java.io.InputStream import java.io.OutputStream +import java.util.UUID import javax.inject.Inject import kotlin.time.Duration.Companion.seconds @@ -77,3 +79,13 @@ inline fun AppPreferences.updateInterfacePreferences(block: InterfacePreferences update { 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) + } +} diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderMovie.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderMovie.kt index 40b3fd3e..b6db39f5 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderMovie.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderMovie.kt @@ -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.focusRestorer +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Tab +import androidx.tv.material3.TabDefaults import androidx.tv.material3.TabRow +import androidx.tv.material3.TabRowDefaults import androidx.tv.material3.Text 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.RecommendedMovie import com.github.damontecres.dolphin.ui.ifElse import com.github.damontecres.dolphin.ui.nav.Destination import com.github.damontecres.dolphin.ui.nav.NavigationManager +import com.github.damontecres.dolphin.ui.preferences.PreferencesViewModel import com.github.damontecres.dolphin.ui.tryRequestFocus @Composable @@ -33,29 +39,71 @@ fun CollectionFolderMovie( navigationManager: NavigationManager, destination: Destination.MediaItem, 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") - var selectedTabIndex by rememberSaveable { mutableIntStateOf(0) } + var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } + var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } val focusRequester = remember { FocusRequester() } + val firstTabFocusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() } + if (uiPrefs.rememberSelectedTab) { + LaunchedEffect(selectedTabIndex) { + preferencesViewModel.preferenceDataStore.updateData { + preferences.appPreferences.rememberTab(destination.itemId, selectedTabIndex) + } + } + } + LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } Column( modifier = modifier, ) { TabRow( - selectedTabIndex = selectedTabIndex, + selectedTabIndex = focusTabIndex, modifier = Modifier - .padding(start = 32.dp, top = 16.dp) - .focusRestorer(firstTabFocusRequester), + .padding(start = 32.dp, top = 16.dp, bottom = 16.dp) + .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.forEachIndexed { index, title -> Tab( - selected = selectedTabIndex == index, + selected = focusTabIndex == index, onClick = { selectedTabIndex = index }, - onFocus = {}, + onFocus = { focusTabIndex = index }, + colors = + TabDefaults.pillIndicatorTabColors( + focusedContentColor = MaterialTheme.colorScheme.onSurfaceVariant, + ), ) { Text( text = title, @@ -64,7 +112,7 @@ fun CollectionFolderMovie( Modifier .padding(8.dp) .ifElse( - index == 0, + index == selectedTabIndex, Modifier.focusRequester(firstTabFocusRequester), ), ) diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderTv.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderTv.kt index c2823ec2..5c2e3581 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderTv.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderTv.kt @@ -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.focusRestorer +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Tab +import androidx.tv.material3.TabDefaults import androidx.tv.material3.TabRow +import androidx.tv.material3.TabRowDefaults import androidx.tv.material3.Text 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.RecommendedTvShow import com.github.damontecres.dolphin.ui.ifElse import com.github.damontecres.dolphin.ui.nav.Destination import com.github.damontecres.dolphin.ui.nav.NavigationManager +import com.github.damontecres.dolphin.ui.preferences.PreferencesViewModel import com.github.damontecres.dolphin.ui.tryRequestFocus @Composable @@ -33,29 +39,71 @@ fun CollectionFolderTv( navigationManager: NavigationManager, destination: Destination.MediaItem, 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") - var selectedTabIndex by rememberSaveable { mutableIntStateOf(0) } + var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } + var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } val focusRequester = remember { FocusRequester() } + val firstTabFocusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() } + if (uiPrefs.rememberSelectedTab) { + LaunchedEffect(selectedTabIndex) { + preferencesViewModel.preferenceDataStore.updateData { + preferences.appPreferences.rememberTab(destination.itemId, selectedTabIndex) + } + } + } + LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } Column( modifier = modifier, ) { TabRow( - selectedTabIndex = selectedTabIndex, + selectedTabIndex = focusTabIndex, modifier = Modifier - .padding(start = 32.dp, top = 16.dp) - .focusRestorer(firstTabFocusRequester), + .padding(start = 32.dp, top = 16.dp, bottom = 16.dp) + .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.forEachIndexed { index, title -> Tab( - selected = selectedTabIndex == index, + selected = focusTabIndex == index, onClick = { selectedTabIndex = index }, - onFocus = {}, + onFocus = { focusTabIndex = index }, + colors = + TabDefaults.pillIndicatorTabColors( + focusedContentColor = MaterialTheme.colorScheme.onSurfaceVariant, + ), ) { Text( text = title, @@ -64,7 +112,7 @@ fun CollectionFolderTv( Modifier .padding(8.dp) .ifElse( - index == 0, + index == selectedTabIndex, Modifier.focusRequester(firstTabFocusRequester), ), ) diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomePage.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomePage.kt index 0f540605..431e1fea 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomePage.kt @@ -68,7 +68,7 @@ data class HomeRow( fun HomePage( preferences: UserPreferences, navigationManager: NavigationManager, - modifier: Modifier, + modifier: Modifier = Modifier, viewModel: HomeViewModel = hiltViewModel(), ) { OneTimeLaunchedEffect { diff --git a/app/src/main/proto/DolphinDataStore.proto b/app/src/main/proto/DolphinDataStore.proto index 38f5b938..d4484c61 100644 --- a/app/src/main/proto/DolphinDataStore.proto +++ b/app/src/main/proto/DolphinDataStore.proto @@ -30,6 +30,8 @@ enum ThemeSongVolume { message InterfacePreferences { ThemeSongVolume play_theme_songs = 1; + bool remember_selected_tab = 2; + map remembered_tabs = 3; } message AppPreferences { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6a06c9c9..0d4fe706 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -59,5 +59,6 @@ preference.update.threshold preference.update.lastTimestamp Confirm + Remember selected tabs