feat: remember last used profile

This commit is contained in:
tapframe 2026-06-06 23:17:53 +05:30
parent 939bca4848
commit 2446465494
9 changed files with 159 additions and 5 deletions

View file

@ -404,6 +404,7 @@
<string name="compose_settings_category_general">General</string>
<string name="compose_settings_page_account">Account</string>
<string name="compose_settings_page_addons">Addons</string>
<string name="compose_settings_page_advanced">Advanced</string>
<string name="compose_settings_page_appearance">Layout</string>
<string name="compose_settings_page_content_discovery">Content &amp; Discovery</string>
<string name="compose_settings_page_continue_watching">Continue Watching</string>
@ -425,6 +426,7 @@
<string name="compose_settings_root_about_section">ABOUT</string>
<string name="compose_settings_root_account_description">Account and sync status</string>
<string name="compose_settings_root_account_section">ACCOUNT</string>
<string name="compose_settings_root_advanced_description">Startup and profile behavior.</string>
<string name="compose_settings_root_appearance_description">Home structure and poster styles</string>
<string name="compose_settings_root_check_updates_description">Download latest release</string>
<string name="compose_settings_root_check_updates_title">Check for updates</string>
@ -441,6 +443,9 @@
<string name="settings_search_empty">No settings found.</string>
<string name="settings_search_placeholder">Search settings...</string>
<string name="settings_search_results_section">RESULTS</string>
<string name="settings_advanced_section_startup">STARTUP</string>
<string name="settings_advanced_remember_last_profile">Remember Last Profile</string>
<string name="settings_advanced_remember_last_profile_description">Start with the last selected profile when it does not have a PIN lock.</string>
<string name="settings_licenses_attributions_section_app">APP LICENSE</string>
<string name="settings_licenses_attributions_section_data">DATA &amp; SERVICES</string>
<string name="settings_licenses_attributions_section_playback">PLAYBACK LICENSE</string>

View file

@ -413,6 +413,20 @@ fun App() {
var isNewProfile by remember { mutableStateOf(false) }
var autoSkipProfileSelection by rememberSaveable { mutableStateOf(false) }
fun rememberedStartupProfile(profiles: List<NuvioProfile>): 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<NuvioProfile>, 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

View file

@ -43,6 +43,8 @@ data class ProfileState(
val profiles: List<NuvioProfile> = emptyList(),
val activeProfile: NuvioProfile? = null,
val isLoaded: Boolean = false,
val hasEverSelectedProfile: Boolean = false,
val rememberLastProfileEnabled: Boolean = false,
)
@Serializable

View file

@ -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<NuvioProfile> = 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,
),
),
)

View file

@ -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,
)
}
}
}
}

View file

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

View file

@ -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),

View file

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

View file

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