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",