Fix unnecessary home page reloads (#2008)

## Description
Fixes the home page reloading from scratch when the app is resumed

This happened because the `userDto` is updated on app resume and since
it has a last accessed timestamp, it would always different and trigger
a reload.

### Related issues
Bug introduced by #1981

### Testing
Emulator

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-09-28 13:18:41 -04:00 • committed by GitHub
parent 365f8f8ada
commit 65954a99eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 61 additions and 36 deletions

View file

@ -6,6 +6,7 @@ import androidx.core.content.edit
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import com.github.damontecres.wholphin.data.model.JellyfinServer import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.data.model.JellyfinUser import com.github.damontecres.wholphin.data.model.JellyfinUser
import com.github.damontecres.wholphin.data.model.ServerUserConfig
import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.services.hilt.IoDispatcher import com.github.damontecres.wholphin.services.hilt.IoDispatcher
import com.github.damontecres.wholphin.ui.toServerString import com.github.damontecres.wholphin.ui.toServerString
@ -28,7 +29,6 @@ import org.jellyfin.sdk.api.client.extensions.quickConnectApi
import org.jellyfin.sdk.api.client.extensions.systemApi import org.jellyfin.sdk.api.client.extensions.systemApi
import org.jellyfin.sdk.api.client.extensions.userApi import org.jellyfin.sdk.api.client.extensions.userApi
import org.jellyfin.sdk.model.api.AuthenticationResult import org.jellyfin.sdk.model.api.AuthenticationResult
import org.jellyfin.sdk.model.api.UserDto
import org.jellyfin.sdk.model.serializer.toUUIDOrNull import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import timber.log.Timber import timber.log.Timber
import java.time.ZonedDateTime import java.time.ZonedDateTime
@ -53,9 +53,9 @@ class ServerRepository
private var _current = MutableStateFlow<CurrentUser?>(null) private var _current = MutableStateFlow<CurrentUser?>(null)
val current: StateFlow<CurrentUser?> = _current val current: StateFlow<CurrentUser?> = _current
private var _currentUserDto = MutableStateFlow<UserDto?>(null) private var _currentUserDto = MutableStateFlow<ServerUserConfig?>(null)
val currentUserDto: UserDto? get() = _currentUserDto.value val currentUserDto: ServerUserConfig? get() = _currentUserDto.value
val currentUserDtoFlow: StateFlow<UserDto?> get() = _currentUserDto val currentUserDtoFlow: StateFlow<ServerUserConfig?> get() = _currentUserDto
val currentServer: JellyfinServer? get() = _current.value?.server val currentServer: JellyfinServer? get() = _current.value?.server
val currentServerFlow: Flow<JellyfinServer?> get() = _current.map { it?.server } val currentServerFlow: Flow<JellyfinServer?> get() = _current.map { it?.server }
@ -126,7 +126,7 @@ class ServerRepository
val currentUser = CurrentUser(updatedServer, updatedUser) val currentUser = CurrentUser(updatedServer, updatedUser)
withContext(WholphinDispatchers.Main) { withContext(WholphinDispatchers.Main) {
_current.value = currentUser _current.value = currentUser
_currentUserDto.value = userDto _currentUserDto.value = ServerUserConfig(userDto)
} }
getServerSharedPreferences(context).edit(true) { getServerSharedPreferences(context).edit(true) {
putString(SERVER_URL_KEY, updatedServer.url) putString(SERVER_URL_KEY, updatedServer.url)
@ -319,7 +319,7 @@ class ServerRepository
suspend fun updateUserDto() { suspend fun updateUserDto() {
val userDto by apiClient.userApi.getCurrentUser() val userDto by apiClient.userApi.getCurrentUser()
_currentUserDto.update { _currentUserDto.update {
if (it?.id == userDto.id && currentUser?.id == userDto.id) userDto else it if (it?.id == userDto.id && currentUser?.id == userDto.id) ServerUserConfig(userDto) else it
} }
} }

View file

@ -0,0 +1,28 @@
package com.github.damontecres.wholphin.data.model
import org.jellyfin.sdk.model.api.UserConfiguration
import org.jellyfin.sdk.model.api.UserDto
import org.jellyfin.sdk.model.api.UserPolicy
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import java.util.UUID
/**
* Server side configuration for a user, similar to [UserDto] but without the timestamps allowing for more reliable equals
*/
data class ServerUserConfig(
val id: UUID,
val name: String? = null,
val serverId: UUID? = null,
val configuration: UserConfiguration? = null,
val policy: UserPolicy? = null,
) {
constructor(userDto: UserDto) : this(
id = userDto.id,
name = userDto.name,
serverId = userDto.serverId?.toUUIDOrNull(),
configuration = userDto.configuration,
policy = userDto.policy,
)
val tvAccess: Boolean get() = policy?.enableLiveTvAccess == true
}

View file

@ -9,6 +9,7 @@ import com.github.damontecres.wholphin.data.model.HomePageSettings
import com.github.damontecres.wholphin.data.model.HomeRowConfig import com.github.damontecres.wholphin.data.model.HomeRowConfig
import com.github.damontecres.wholphin.data.model.HomeRowViewOptions import com.github.damontecres.wholphin.data.model.HomeRowViewOptions
import com.github.damontecres.wholphin.data.model.SUPPORTED_HOME_PAGE_SETTINGS_VERSION import com.github.damontecres.wholphin.data.model.SUPPORTED_HOME_PAGE_SETTINGS_VERSION
import com.github.damontecres.wholphin.data.model.ServerUserConfig
import com.github.damontecres.wholphin.data.model.createGenreDestination import com.github.damontecres.wholphin.data.model.createGenreDestination
import com.github.damontecres.wholphin.data.model.createStudioDestination import com.github.damontecres.wholphin.data.model.createStudioDestination
import com.github.damontecres.wholphin.preferences.DefaultUserConfiguration import com.github.damontecres.wholphin.preferences.DefaultUserConfiguration
@ -591,7 +592,7 @@ class HomeSettingsService
row: HomeRowConfig, row: HomeRowConfig,
scope: CoroutineScope, scope: CoroutineScope,
prefs: HomePagePreferences, prefs: HomePagePreferences,
userDto: UserDto, userDto: ServerUserConfig,
libraries: List<Library>, libraries: List<Library>,
limit: Int = prefs.maxItemsPerRow, limit: Int = prefs.maxItemsPerRow,
isRefresh: Boolean, isRefresh: Boolean,

View file

@ -5,6 +5,7 @@ import com.github.damontecres.wholphin.data.ServerPreferencesDao
import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.JellyfinUser import com.github.damontecres.wholphin.data.model.JellyfinUser
import com.github.damontecres.wholphin.data.model.NavPinType import com.github.damontecres.wholphin.data.model.NavPinType
import com.github.damontecres.wholphin.data.model.ServerUserConfig
import com.github.damontecres.wholphin.services.hilt.DefaultCoroutineScope import com.github.damontecres.wholphin.services.hilt.DefaultCoroutineScope
import com.github.damontecres.wholphin.ui.collectLatestIn import com.github.damontecres.wholphin.ui.collectLatestIn
import com.github.damontecres.wholphin.ui.launchDefault import com.github.damontecres.wholphin.ui.launchDefault
@ -188,7 +189,7 @@ class NavDrawerService
*/ */
suspend fun updateNavDrawer( suspend fun updateNavDrawer(
user: JellyfinUser, user: JellyfinUser,
userDto: UserDto, userDto: ServerUserConfig,
discoverActive: Boolean, discoverActive: Boolean,
) { ) {
val builtins = val builtins =

View file

@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.HomeRowConfig import com.github.damontecres.wholphin.data.model.HomeRowConfig
import com.github.damontecres.wholphin.data.model.ServerUserConfig
import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.services.BackdropService import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.DatePlayedService import com.github.damontecres.wholphin.services.DatePlayedService
@ -19,7 +20,6 @@ import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.services.ServerReportService import com.github.damontecres.wholphin.services.ServerReportService
import com.github.damontecres.wholphin.services.UserPreferencesService import com.github.damontecres.wholphin.services.UserPreferencesService
import com.github.damontecres.wholphin.services.deleteItem import com.github.damontecres.wholphin.services.deleteItem
import com.github.damontecres.wholphin.services.tvAccess
import com.github.damontecres.wholphin.ui.collectLatestIn import com.github.damontecres.wholphin.ui.collectLatestIn
import com.github.damontecres.wholphin.ui.combinePair import com.github.damontecres.wholphin.ui.combinePair
import com.github.damontecres.wholphin.ui.data.RowColumn import com.github.damontecres.wholphin.ui.data.RowColumn
@ -48,7 +48,6 @@ import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.exception.InvalidStatusException import org.jellyfin.sdk.api.client.exception.InvalidStatusException
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.UserDto
import timber.log.Timber import timber.log.Timber
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
@ -85,20 +84,27 @@ class HomeViewModel
userDto?.id, userDto?.id,
settings != HomePageResolvedSettings.EMPTY, settings != HomePageResolvedSettings.EMPTY,
) )
_state.update { HomeState.EMPTY } // Timber.v("userDto=%s", userDto)
dataLoadingJob?.cancel() dataLoadingJob?.cancel()
if (userDto == null) { if (userDto == null) {
Timber.d("UserDto is null") Timber.d("UserDto is null")
_state.update { HomeState.EMPTY }
return@collectLatestIn return@collectLatestIn
} }
if (settings == HomePageResolvedSettings.EMPTY) { if (settings == HomePageResolvedSettings.EMPTY) {
Timber.d("Home settings are empty") Timber.d("Home settings are empty")
_state.update { HomeState.EMPTY }
return@collectLatestIn return@collectLatestIn
} }
if (userDto.id != settings.userId) { if (userDto.id != settings.userId) {
Timber.d("User IDs don't match: %s vs %s", userDto?.id, settings.userId) Timber.d("User IDs don't match: %s vs %s", userDto?.id, settings.userId)
_state.update { HomeState.EMPTY }
return@collectLatestIn return@collectLatestIn
} }
if (state.value.settings.userId != settings.userId) {
Timber.d("User changed")
_state.update { HomeState.EMPTY }
}
dataLoadingJob = dataLoadingJob =
viewModelScope.launchIO { viewModelScope.launchIO {
loadHomeRows(userDto, settings) loadHomeRows(userDto, settings)
@ -133,7 +139,7 @@ class HomeViewModel
} }
suspend fun loadHomeRows( suspend fun loadHomeRows(
userDto: UserDto, userDto: ServerUserConfig,
settings: HomePageResolvedSettings, settings: HomePageResolvedSettings,
) { ) {
Timber.i("Starting loadHomeRows") Timber.i("Starting loadHomeRows")

View file

@ -3,6 +3,7 @@ package com.github.damontecres.wholphin.services
import android.content.Context import android.content.Context
import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.ServerUserConfig
import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.InterfacePreferences import com.github.damontecres.wholphin.preferences.InterfacePreferences
import com.github.damontecres.wholphin.ui.successResponse import com.github.damontecres.wholphin.ui.successResponse
@ -26,7 +27,6 @@ import org.jellyfin.sdk.api.operations.LibraryApi
import org.jellyfin.sdk.model.UUID import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.UserDto
import org.jellyfin.sdk.model.api.UserPolicy import org.jellyfin.sdk.model.api.UserPolicy
import org.junit.After import org.junit.After
import org.junit.Assert import org.junit.Assert
@ -87,11 +87,8 @@ class MediaManagementServiceTest {
) )
private val userWithLiveTv = private val userWithLiveTv =
UserDto( ServerUserConfig(
id = UUID.randomUUID(), id = UUID.randomUUID(),
hasPassword = true,
hasConfiguredPassword = true,
hasConfiguredEasyPassword = false,
policy = policy =
mockk<UserPolicy> { mockk<UserPolicy> {
every { enableLiveTvManagement } returns true every { enableLiveTvManagement } returns true
@ -99,11 +96,8 @@ class MediaManagementServiceTest {
) )
private val userWithoutLiveTv = private val userWithoutLiveTv =
UserDto( ServerUserConfig(
id = UUID.randomUUID(), id = UUID.randomUUID(),
hasPassword = true,
hasConfiguredPassword = true,
hasConfiguredEasyPassword = false,
policy = policy =
mockk<UserPolicy> { mockk<UserPolicy> {
every { enableLiveTvManagement } returns false every { enableLiveTvManagement } returns false

View file

@ -10,6 +10,7 @@ import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.JellyfinServer import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.data.model.JellyfinServerUsers import com.github.damontecres.wholphin.data.model.JellyfinServerUsers
import com.github.damontecres.wholphin.data.model.JellyfinUser import com.github.damontecres.wholphin.data.model.JellyfinUser
import com.github.damontecres.wholphin.data.model.ServerUserConfig
import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.AppPreferencesSerializer import com.github.damontecres.wholphin.preferences.AppPreferencesSerializer
import com.github.damontecres.wholphin.test.nonBlankString import com.github.damontecres.wholphin.test.nonBlankString
@ -143,6 +144,7 @@ class ServerRepositoryTest {
hasConfiguredPassword = true, hasConfiguredPassword = true,
hasConfiguredEasyPassword = false, hasConfiguredEasyPassword = false,
) )
private val serverUserConfig = ServerUserConfig(userDto)
private fun setUpCurrentUser( private fun setUpCurrentUser(
serverRepository: ServerRepository, serverRepository: ServerRepository,
@ -190,7 +192,7 @@ class ServerRepositoryTest {
Assert.assertEquals(server, serverRepository.currentServer) Assert.assertEquals(server, serverRepository.currentServer)
Assert.assertEquals(user, serverRepository.currentUser) Assert.assertEquals(user, serverRepository.currentUser)
Assert.assertEquals(userDto, serverRepository.currentUserDto) Assert.assertEquals(serverUserConfig, serverRepository.currentUserDto)
val appPreferences = dataStore.data.first() val appPreferences = dataStore.data.first()
Assert.assertEquals(serverId.toServerString(), appPreferences.currentServerId) Assert.assertEquals(serverId.toServerString(), appPreferences.currentServerId)
@ -221,7 +223,7 @@ class ServerRepositoryTest {
} }
Assert.assertEquals(server, serverRepository.currentServer) Assert.assertEquals(server, serverRepository.currentServer)
Assert.assertEquals(user, serverRepository.currentUser) Assert.assertEquals(user, serverRepository.currentUser)
Assert.assertEquals(userDto, serverRepository.currentUserDto) Assert.assertEquals(serverUserConfig, serverRepository.currentUserDto)
} }
@Test @Test
@ -383,7 +385,7 @@ class ServerRepositoryTest {
Assert.assertEquals(server, serverRepository.currentServer) Assert.assertEquals(server, serverRepository.currentServer)
Assert.assertEquals(user, serverRepository.currentUser) Assert.assertEquals(user, serverRepository.currentUser)
Assert.assertEquals(userDto, serverRepository.currentUserDto) Assert.assertEquals(serverUserConfig, serverRepository.currentUserDto)
val appPreferences = dataStore.data.first() val appPreferences = dataStore.data.first()
Assert.assertEquals(serverId.toServerString(), appPreferences.currentServerId) Assert.assertEquals(serverId.toServerString(), appPreferences.currentServerId)
@ -426,7 +428,7 @@ class ServerRepositoryTest {
Assert.assertEquals(server, serverRepository.currentServer) Assert.assertEquals(server, serverRepository.currentServer)
Assert.assertEquals(user, serverRepository.currentUser) Assert.assertEquals(user, serverRepository.currentUser)
Assert.assertEquals(userDto, serverRepository.currentUserDto) Assert.assertEquals(serverUserConfig, serverRepository.currentUserDto)
val appPreferences = dataStore.data.first() val appPreferences = dataStore.data.first()
Assert.assertEquals(serverId.toServerString(), appPreferences.currentServerId) Assert.assertEquals(serverId.toServerString(), appPreferences.currentServerId)

View file

@ -5,6 +5,7 @@ import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.data.model.ItemPlayback
import com.github.damontecres.wholphin.data.model.JellyfinUserPreferences import com.github.damontecres.wholphin.data.model.JellyfinUserPreferences
import com.github.damontecres.wholphin.data.model.PlaybackLanguageChoice import com.github.damontecres.wholphin.data.model.PlaybackLanguageChoice
import com.github.damontecres.wholphin.data.model.ServerUserConfig
import com.github.damontecres.wholphin.data.model.TrackIndex import com.github.damontecres.wholphin.data.model.TrackIndex
import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.DefaultUserConfiguration import com.github.damontecres.wholphin.preferences.DefaultUserConfiguration
@ -17,7 +18,6 @@ import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.MediaStream import org.jellyfin.sdk.model.api.MediaStream
import org.jellyfin.sdk.model.api.MediaStreamType import org.jellyfin.sdk.model.api.MediaStreamType
import org.jellyfin.sdk.model.api.SubtitlePlaybackMode import org.jellyfin.sdk.model.api.SubtitlePlaybackMode
import org.jellyfin.sdk.model.api.UserDto
import org.junit.Assert import org.junit.Assert
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -902,11 +902,8 @@ private fun serverRepo(
): ServerRepository { ): ServerRepository {
val mocked = mockk<ServerRepository>() val mocked = mockk<ServerRepository>()
every { mocked.currentUserDto } returns every { mocked.currentUserDto } returns
UserDto( ServerUserConfig(
id = UUID.randomUUID(), id = UUID.randomUUID(),
hasPassword = true,
hasConfiguredPassword = true,
hasConfiguredEasyPassword = true,
configuration = configuration =
DefaultUserConfiguration.copy( DefaultUserConfiguration.copy(
audioLanguagePreference = audioLang, audioLanguagePreference = audioLang,

View file

@ -11,6 +11,7 @@ import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.data.model.JellyfinUser import com.github.damontecres.wholphin.data.model.JellyfinUser
import com.github.damontecres.wholphin.data.model.Playlist import com.github.damontecres.wholphin.data.model.Playlist
import com.github.damontecres.wholphin.data.model.PlaylistItem import com.github.damontecres.wholphin.data.model.PlaylistItem
import com.github.damontecres.wholphin.data.model.ServerUserConfig
import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.PlaybackPreferences import com.github.damontecres.wholphin.preferences.PlaybackPreferences
import com.github.damontecres.wholphin.preferences.ShowNextUpWhen import com.github.damontecres.wholphin.preferences.ShowNextUpWhen
@ -68,7 +69,6 @@ import org.jellyfin.sdk.model.api.MediaSegmentDtoQueryResult
import org.jellyfin.sdk.model.api.MediaSegmentType import org.jellyfin.sdk.model.api.MediaSegmentType
import org.jellyfin.sdk.model.api.MediaStreamType import org.jellyfin.sdk.model.api.MediaStreamType
import org.jellyfin.sdk.model.api.PlaybackInfoResponse import org.jellyfin.sdk.model.api.PlaybackInfoResponse
import org.jellyfin.sdk.model.api.UserDto
import org.jellyfin.sdk.model.extensions.inWholeTicks import org.jellyfin.sdk.model.extensions.inWholeTicks
import org.jellyfin.sdk.model.extensions.ticks import org.jellyfin.sdk.model.extensions.ticks
import org.junit.After import org.junit.After
@ -152,13 +152,9 @@ class PlaybackViewModelTests {
pin = "1234", pin = "1234",
) )
private val userDto = private val userDto =
UserDto( ServerUserConfig(
id = userId, id = userId,
name = "test-user", name = "test-user",
serverName = "test server",
hasPassword = true,
hasConfiguredPassword = true,
hasConfiguredEasyPassword = false,
) )
private val mediaSource = private val mediaSource =