From 24464654947ba6339c59ea400d534430fe60eacf Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Sat, 6 Jun 2026 23:17:53 +0530 Subject: [PATCH] feat: remember last used profile --- .../composeResources/values/strings.xml | 5 ++ .../commonMain/kotlin/com/nuvio/app/App.kt | 53 +++++++++++++++++-- .../app/features/profiles/ProfileModels.kt | 2 + .../features/profiles/ProfileRepository.kt | 20 ++++++- .../features/settings/AdvancedSettingsPage.kt | 31 +++++++++++ .../app/features/settings/SettingsModels.kt | 6 +++ .../app/features/settings/SettingsRootPage.kt | 12 +++++ .../app/features/settings/SettingsScreen.kt | 18 +++++++ .../app/features/settings/SettingsSearch.kt | 17 ++++++ 9 files changed, 159 insertions(+), 5 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AdvancedSettingsPage.kt diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index 0c34f27f..68a45e55 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -404,6 +404,7 @@ General Account Addons + Advanced Layout Content & Discovery Continue Watching @@ -425,6 +426,7 @@ ABOUT Account and sync status ACCOUNT + Startup and profile behavior. Home structure and poster styles Download latest release Check for updates @@ -441,6 +443,9 @@ No settings found. Search settings... RESULTS + STARTUP + Remember Last Profile + Start with the last selected profile when it does not have a PIN lock. APP LICENSE DATA & SERVICES PLAYBACK LICENSE diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt index 1f53c387..d6db9960 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt @@ -413,6 +413,20 @@ fun App() { var isNewProfile by remember { mutableStateOf(false) } var autoSkipProfileSelection by rememberSaveable { mutableStateOf(false) } + fun rememberedStartupProfile(profiles: List): NuvioProfile? { + val currentProfileState = ProfileRepository.state.value + if ( + !currentProfileState.rememberLastProfileEnabled || + !currentProfileState.hasEverSelectedProfile + ) { + return null + } + + return profiles + .find { it.profileIndex == ProfileRepository.activeProfileId } + ?.takeUnless { it.pinEnabled } + } + fun enterProfileGate(profiles: List, syncOnEnter: Boolean) { if (profiles.isEmpty()) { autoSkipProfileSelection = true @@ -420,9 +434,23 @@ fun App() { return } + rememberedStartupProfile(profiles)?.let { profile -> + ProfileRepository.selectProfile(profile.profileIndex) + if (syncOnEnter) { + SyncManager.pullAllForProfile(profile.profileIndex) + } + gateScreen = AppGateScreen.Main.name + autoSkipProfileSelection = false + return + } + autoSkipProfileSelection = true if (profiles.size == 1) { val onlyProfile = profiles.first() + if (onlyProfile.pinEnabled) { + gateScreen = AppGateScreen.ProfileSelection.name + return + } ProfileRepository.selectProfile(onlyProfile.profileIndex) if (syncOnEnter) { SyncManager.pullAllForProfile(onlyProfile.profileIndex) @@ -473,13 +501,32 @@ fun App() { ProfileRepository.pullProfiles() } - LaunchedEffect(gateScreen, autoSkipProfileSelection, profileState.profiles) { + LaunchedEffect( + gateScreen, + autoSkipProfileSelection, + profileState.profiles, + profileState.hasEverSelectedProfile, + profileState.rememberLastProfileEnabled, + profileState.activeProfile?.profileIndex, + profileState.activeProfile?.pinEnabled, + ) { if ( autoSkipProfileSelection && - gateScreen == AppGateScreen.ProfileSelection.name && - profileState.profiles.size == 1 + gateScreen == AppGateScreen.ProfileSelection.name ) { + rememberedStartupProfile(profileState.profiles)?.let { profile -> + ProfileRepository.selectProfile(profile.profileIndex) + SyncManager.pullAllForProfile(profile.profileIndex) + gateScreen = AppGateScreen.Main.name + autoSkipProfileSelection = false + return@LaunchedEffect + } + + if (profileState.profiles.size != 1) return@LaunchedEffect + val onlyProfile = profileState.profiles.first() + if (onlyProfile.pinEnabled) return@LaunchedEffect + ProfileRepository.selectProfile(onlyProfile.profileIndex) SyncManager.pullAllForProfile(onlyProfile.profileIndex) gateScreen = AppGateScreen.Main.name diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileModels.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileModels.kt index f36aee81..ccca24b3 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileModels.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileModels.kt @@ -43,6 +43,8 @@ data class ProfileState( val profiles: List = emptyList(), val activeProfile: NuvioProfile? = null, val isLoaded: Boolean = false, + val hasEverSelectedProfile: Boolean = false, + val rememberLastProfileEnabled: Boolean = false, ) @Serializable diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileRepository.kt index 18057320..ab6b5b2b 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileRepository.kt @@ -53,6 +53,8 @@ import org.jetbrains.compose.resources.getString private data class StoredProfilePayload( val userId: String, val activeProfileIndex: Int = 1, + val hasEverSelectedProfile: Boolean = false, + val rememberLastProfileEnabled: Boolean = false, val profiles: List = emptyList(), ) @@ -70,6 +72,13 @@ object ProfileRepository { val activeProfileId: Int get() = activeProfileIndex + fun setRememberLastProfileEnabled(enabled: Boolean) { + if (_state.value.rememberLastProfileEnabled == enabled) return + + _state.value = _state.value.copy(rememberLastProfileEnabled = enabled) + persist() + } + fun loadCachedProfiles(): Boolean { val stored = decodeStoredPayload() ?: return false loadedCacheForUserId = stored.userId @@ -134,8 +143,10 @@ object ProfileRepository { fun selectProfile(profileIndex: Int) { activeProfileIndex = profileIndex + val selectedProfile = _state.value.profiles.find { it.profileIndex == profileIndex } _state.value = _state.value.copy( - activeProfile = _state.value.profiles.find { it.profileIndex == profileIndex }, + activeProfile = selectedProfile, + hasEverSelectedProfile = selectedProfile != null || _state.value.hasEverSelectedProfile, ) persist() WatchedRepository.onProfileChanged(profileIndex) @@ -402,6 +413,8 @@ object ProfileRepository { profiles = profiles, activeProfile = profiles.find { it.profileIndex == activeProfileIndex } ?: profiles.firstOrNull(), isLoaded = profiles.isNotEmpty(), + hasEverSelectedProfile = stored.hasEverSelectedProfile, + rememberLastProfileEnabled = stored.rememberLastProfileEnabled, ) _state.value.activeProfile?.let { activeProfileIndex = it.profileIndex } syncPinCache(profiles) @@ -490,12 +503,15 @@ object ProfileRepository { private fun persist() { val authState = AuthRepository.state.value as? AuthState.Authenticated ?: return + val state = _state.value ProfileStorage.savePayload( json.encodeToString( StoredProfilePayload( userId = authState.userId, activeProfileIndex = activeProfileIndex, - profiles = _state.value.profiles, + hasEverSelectedProfile = state.hasEverSelectedProfile, + rememberLastProfileEnabled = state.rememberLastProfileEnabled, + profiles = state.profiles, ), ), ) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AdvancedSettingsPage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AdvancedSettingsPage.kt new file mode 100644 index 00000000..57af823a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AdvancedSettingsPage.kt @@ -0,0 +1,31 @@ +package com.nuvio.app.features.settings + +import androidx.compose.foundation.lazy.LazyListScope +import com.nuvio.app.features.profiles.ProfileRepository +import nuvio.composeapp.generated.resources.Res +import nuvio.composeapp.generated.resources.settings_advanced_remember_last_profile +import nuvio.composeapp.generated.resources.settings_advanced_remember_last_profile_description +import nuvio.composeapp.generated.resources.settings_advanced_section_startup +import org.jetbrains.compose.resources.stringResource + +internal fun LazyListScope.advancedSettingsContent( + isTablet: Boolean, + rememberLastProfileEnabled: Boolean, +) { + item { + SettingsSection( + title = stringResource(Res.string.settings_advanced_section_startup), + isTablet = isTablet, + ) { + SettingsGroup(isTablet = isTablet) { + SettingsSwitchRow( + title = stringResource(Res.string.settings_advanced_remember_last_profile), + description = stringResource(Res.string.settings_advanced_remember_last_profile_description), + checked = rememberLastProfileEnabled, + isTablet = isTablet, + onCheckedChange = ProfileRepository::setRememberLastProfileEnabled, + ) + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsModels.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsModels.kt index 006d8768..7baa1761 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsModels.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsModels.kt @@ -11,6 +11,7 @@ import nuvio.composeapp.generated.resources.compose_settings_category_about import nuvio.composeapp.generated.resources.compose_settings_category_general import nuvio.composeapp.generated.resources.compose_settings_page_account import nuvio.composeapp.generated.resources.compose_settings_page_addons +import nuvio.composeapp.generated.resources.compose_settings_page_advanced import nuvio.composeapp.generated.resources.compose_settings_page_appearance import nuvio.composeapp.generated.resources.compose_settings_page_content_discovery import nuvio.composeapp.generated.resources.compose_settings_page_debrid @@ -81,6 +82,11 @@ internal enum class SettingsPage( category = SettingsCategory.General, parentPage = Root, ), + Advanced( + titleRes = Res.string.compose_settings_page_advanced, + category = SettingsCategory.General, + parentPage = Root, + ), Notifications( titleRes = Res.string.compose_settings_page_notifications, category = SettingsCategory.General, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsRootPage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsRootPage.kt index 9ce3a1ff..a7f8b440 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsRootPage.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsRootPage.kt @@ -15,6 +15,7 @@ import androidx.compose.material.icons.rounded.Palette import androidx.compose.material.icons.rounded.People import androidx.compose.material.icons.rounded.PlayArrow import androidx.compose.material.icons.rounded.Style +import androidx.compose.material.icons.rounded.Tune import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.ui.Modifier @@ -25,6 +26,7 @@ import nuvio.composeapp.generated.resources.Res import nuvio.composeapp.generated.resources.compose_about_made_with import nuvio.composeapp.generated.resources.compose_about_version_format import nuvio.composeapp.generated.resources.compose_settings_page_account +import nuvio.composeapp.generated.resources.compose_settings_page_advanced import nuvio.composeapp.generated.resources.compose_settings_page_appearance import nuvio.composeapp.generated.resources.compose_settings_page_integrations import nuvio.composeapp.generated.resources.compose_settings_page_licenses_attributions @@ -48,6 +50,7 @@ import nuvio.composeapp.generated.resources.compose_settings_root_switch_profile import nuvio.composeapp.generated.resources.compose_settings_root_trakt_description import nuvio.composeapp.generated.resources.compose_settings_root_about_section import nuvio.composeapp.generated.resources.compose_settings_root_account_section +import nuvio.composeapp.generated.resources.compose_settings_root_advanced_description import nuvio.composeapp.generated.resources.compose_settings_page_content_discovery import nuvio.composeapp.generated.resources.compose_settings_page_trakt import nuvio.composeapp.generated.resources.settings_playback_subtitle @@ -60,6 +63,7 @@ internal fun LazyListScope.settingsRootContent( onPlaybackClick: () -> Unit, onStreamsClick: () -> Unit, onAppearanceClick: () -> Unit, + onAdvancedClick: () -> Unit, onNotificationsClick: () -> Unit, onContentDiscoveryClick: () -> Unit, onIntegrationsClick: () -> Unit, @@ -125,6 +129,14 @@ internal fun LazyListScope.settingsRootContent( onClick = onAppearanceClick, ) SettingsGroupDivider(isTablet = isTablet) + SettingsNavigationRow( + title = stringResource(Res.string.compose_settings_page_advanced), + description = stringResource(Res.string.compose_settings_root_advanced_description), + icon = Icons.Rounded.Tune, + isTablet = isTablet, + onClick = onAdvancedClick, + ) + SettingsGroupDivider(isTablet = isTablet) SettingsNavigationRow( title = stringResource(Res.string.compose_settings_page_content_discovery), description = stringResource(Res.string.compose_settings_root_content_discovery_description), diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt index 88cf0b01..4c237106 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt @@ -69,6 +69,7 @@ import com.nuvio.app.features.mdblist.MdbListSettingsRepository import com.nuvio.app.features.notifications.EpisodeReleaseNotificationsRepository import com.nuvio.app.features.notifications.EpisodeReleaseNotificationsUiState import com.nuvio.app.features.player.PlayerSettingsRepository +import com.nuvio.app.features.profiles.ProfileRepository import com.nuvio.app.features.trakt.TraktAuthUiState import com.nuvio.app.features.trakt.TraktAuthRepository import com.nuvio.app.features.trakt.TraktCommentsSettings @@ -194,6 +195,9 @@ fun SettingsScreen( EpisodeReleaseNotificationsRepository.ensureLoaded() EpisodeReleaseNotificationsRepository.uiState }.collectAsStateWithLifecycle() + val profileSettingsState by remember { + ProfileRepository.state + }.collectAsStateWithLifecycle() LaunchedEffect(homescreenCatalogRefreshKey) { if (homescreenCatalogRefreshKey.isEmpty()) return@LaunchedEffect @@ -258,6 +262,7 @@ fun SettingsScreen( tunnelingEnabled = playerSettingsUiState.tunnelingEnabled, useLibass = playerSettingsUiState.useLibass, libassRenderType = playerSettingsUiState.libassRenderType, + rememberLastProfileEnabled = profileSettingsState.rememberLastProfileEnabled, selectedTheme = selectedTheme, onThemeSelected = ThemeSettingsRepository::setTheme, amoledEnabled = amoledEnabled, @@ -307,6 +312,7 @@ fun SettingsScreen( tunnelingEnabled = playerSettingsUiState.tunnelingEnabled, useLibass = playerSettingsUiState.useLibass, libassRenderType = playerSettingsUiState.libassRenderType, + rememberLastProfileEnabled = profileSettingsState.rememberLastProfileEnabled, selectedTheme = selectedTheme, onThemeSelected = ThemeSettingsRepository::setTheme, amoledEnabled = amoledEnabled, @@ -366,6 +372,7 @@ private fun MobileSettingsScreen( tunnelingEnabled: Boolean, useLibass: Boolean, libassRenderType: String, + rememberLastProfileEnabled: Boolean, selectedTheme: AppTheme, onThemeSelected: (AppTheme) -> Unit, amoledEnabled: Boolean, @@ -496,6 +503,7 @@ private fun MobileSettingsScreen( onPlaybackClick = { onPageChange(SettingsPage.Playback) }, onStreamsClick = { onPageChange(SettingsPage.Streams) }, onAppearanceClick = { onPageChange(SettingsPage.Appearance) }, + onAdvancedClick = { onPageChange(SettingsPage.Advanced) }, onNotificationsClick = { onPageChange(SettingsPage.Notifications) }, onContentDiscoveryClick = { onPageChange(SettingsPage.ContentDiscovery) }, onIntegrationsClick = { onPageChange(SettingsPage.Integrations) }, @@ -552,6 +560,10 @@ private fun MobileSettingsScreen( onContinueWatchingClick = onContinueWatchingClick, onPosterCustomizationClick = { onPageChange(SettingsPage.PosterCustomization) }, ) + SettingsPage.Advanced -> advancedSettingsContent( + isTablet = false, + rememberLastProfileEnabled = rememberLastProfileEnabled, + ) SettingsPage.Notifications -> notificationsSettingsContent( isTablet = false, uiState = episodeReleaseNotificationsUiState, @@ -684,6 +696,7 @@ private fun TabletSettingsScreen( tunnelingEnabled: Boolean, useLibass: Boolean, libassRenderType: String, + rememberLastProfileEnabled: Boolean, selectedTheme: AppTheme, onThemeSelected: (AppTheme) -> Unit, amoledEnabled: Boolean, @@ -869,6 +882,7 @@ private fun TabletSettingsScreen( onPlaybackClick = { openInlinePage(SettingsPage.Playback) }, onStreamsClick = { openInlinePage(SettingsPage.Streams) }, onAppearanceClick = { openInlinePage(SettingsPage.Appearance) }, + onAdvancedClick = { openInlinePage(SettingsPage.Advanced) }, onNotificationsClick = { openInlinePage(SettingsPage.Notifications) }, onContentDiscoveryClick = { openInlinePage(SettingsPage.ContentDiscovery) }, onIntegrationsClick = { openInlinePage(SettingsPage.Integrations) }, @@ -928,6 +942,10 @@ private fun TabletSettingsScreen( onContinueWatchingClick = { openInlinePage(SettingsPage.ContinueWatching) }, onPosterCustomizationClick = { openInlinePage(SettingsPage.PosterCustomization) }, ) + SettingsPage.Advanced -> advancedSettingsContent( + isTablet = true, + rememberLastProfileEnabled = rememberLastProfileEnabled, + ) SettingsPage.Notifications -> notificationsSettingsContent( isTablet = true, uiState = episodeReleaseNotificationsUiState, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt index 0de4be7c..b604f5bd 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt @@ -89,6 +89,7 @@ internal fun settingsSearchEntries( val accountPage = stringResource(Res.string.compose_settings_page_account) val traktPage = stringResource(Res.string.compose_settings_page_trakt) val layoutPage = stringResource(Res.string.compose_settings_page_appearance) + val advancedPage = stringResource(Res.string.compose_settings_page_advanced) val contentDiscoveryPage = stringResource(Res.string.compose_settings_page_content_discovery) val downloadsPage = stringResource(Res.string.compose_settings_root_downloads_title) val playbackPage = stringResource(Res.string.compose_settings_page_playback) @@ -207,6 +208,13 @@ internal fun settingsSearchEntries( description = stringResource(Res.string.compose_settings_root_appearance_description), icon = Icons.Rounded.Palette, ) + addPage( + page = SettingsPage.Advanced, + key = "advanced", + title = advancedPage, + description = stringResource(Res.string.compose_settings_root_advanced_description), + icon = Icons.Rounded.Tune, + ) addPage( page = SettingsPage.ContentDiscovery, key = "content-discovery", @@ -368,6 +376,15 @@ internal fun settingsSearchEntries( section = stringResource(Res.string.settings_appearance_section_display), icon = Icons.Rounded.Language, ) + addRow( + page = SettingsPage.Advanced, + key = "remember-last-profile", + title = stringResource(Res.string.settings_advanced_remember_last_profile), + description = stringResource(Res.string.settings_advanced_remember_last_profile_description), + pageLabel = advancedPage, + section = stringResource(Res.string.settings_advanced_section_startup), + icon = Icons.Rounded.Tune, + ) addPage( page = SettingsPage.ContinueWatching, key = "continue-watching",