Add setting to toggle automatic sign in (#538)

## Description
The major user facing change in this PR is adding back the setting to
toggle automatic sign in on or off. This controls whether to restore the
app to where you left off or go the user selection page.

If enabled: re-opening the app will restore the previous user's session

If disabled: re-opening the app will always start with the user
selection page for the most recent server, or if there isn't a recent
server, the server selection page

This also separate this setting from the PIN protection settings in
previous PRs.

### Dev changes

This PR refactors the "setup" UI (server & user selection) to be
separate from the "app" UI (the actually app content). There is now a
`NavDisplay` for the setup flow which ends with displaying the
`ApplicationContent`'s `NavDisplay`. Most of the logic is moved out of
Compose code and into a `ViewModel`.

This means to switch users/server, the `SetupNavigationManager` must be
used instead of `NavigationManager`.


### Related issues
Closes #396
Fixes #376
This commit is contained in:
Ray 2025-12-22 14:52:09 -05:00 committed by GitHub
parent 20fe5e2626
commit eb4e9b93be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 302 additions and 198 deletions

View file

@ -15,6 +15,7 @@ import com.github.damontecres.wholphin.util.EqualityMutableLiveData
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import org.jellyfin.sdk.Jellyfin
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.systemApi
@ -68,44 +69,46 @@ class ServerRepository
suspend fun changeUser(
server: JellyfinServer,
user: JellyfinUser,
) = withContext(Dispatchers.IO) {
if (server.id != user.serverId) {
throw IllegalStateException("User is not part of the server")
}
Timber.v("Changing user to ${user.name} on ${server.url}")
apiClient.update(baseUrl = server.url, accessToken = user.accessToken)
val userDto by apiClient.userApi.getCurrentUser()
val updatedServer =
try {
val sysInfo by apiClient.systemApi.getPublicSystemInfo()
server.copy(name = sysInfo.serverName, version = sysInfo.version)
} catch (ex: Exception) {
Timber.w(ex, "Exception fetching public system info")
server
): CurrentUser? =
withContext(Dispatchers.IO) {
if (server.id != user.serverId) {
throw IllegalStateException("User is not part of the server")
}
var updatedUser =
user.copy(
id = userDto.id,
name = userDto.name,
)
serverDao.addOrUpdateServer(updatedServer)
updatedUser = serverDao.addOrUpdateUser(updatedUser)
userPreferencesDataStore.updateData {
it
.toBuilder()
.apply {
currentServerId = updatedServer.id.toServerString()
currentUserId = updatedUser.id.toServerString()
}.build()
Timber.v("Changing user to ${user.name} on ${server.url}")
apiClient.update(baseUrl = server.url, accessToken = user.accessToken)
val userDto by apiClient.userApi.getCurrentUser()
val updatedServer =
try {
val sysInfo by apiClient.systemApi.getPublicSystemInfo()
server.copy(name = sysInfo.serverName, version = sysInfo.version)
} catch (ex: Exception) {
Timber.w(ex, "Exception fetching public system info")
server
}
var updatedUser =
user.copy(
id = userDto.id,
name = userDto.name,
)
serverDao.addOrUpdateServer(updatedServer)
updatedUser = serverDao.addOrUpdateUser(updatedUser)
userPreferencesDataStore.updateData {
it
.toBuilder()
.apply {
currentServerId = updatedServer.id.toServerString()
currentUserId = updatedUser.id.toServerString()
}.build()
}
withContext(Dispatchers.Main) {
_current.value = CurrentUser(updatedServer, updatedUser, userDto)
}
sharedPreferences.edit(true) {
putString(SERVER_URL_KEY, updatedServer.url)
putString(ACCESS_TOKEN_KEY, updatedUser.accessToken)
}
return@withContext _current.value
}
withContext(Dispatchers.Main) {
_current.value = CurrentUser(updatedServer, updatedUser, userDto)
}
sharedPreferences.edit(true) {
putString(SERVER_URL_KEY, updatedServer.url)
putString(ACCESS_TOKEN_KEY, updatedUser.accessToken)
}
}
/**
* Restores a session for the given server & user such as when the app reopens
@ -115,10 +118,10 @@ class ServerRepository
suspend fun restoreSession(
serverId: UUID?,
userId: UUID?,
): Boolean {
): CurrentUser? {
if (serverId == null || userId == null) {
_current.setValueOnMain(null)
return false
return null
}
val serverAndUsers =
withContext(Dispatchers.IO) {
@ -129,13 +132,17 @@ class ServerRepository
if (user != null) {
// TODO pin-related
// if (user != null && !user.hasPin) {
changeUser(serverAndUsers.server, user)
return true
return changeUser(serverAndUsers.server, user)
}
}
return false
return null
}
suspend fun fetchLastUsedServer(serverId: UUID?): JellyfinServer? =
withContext(Dispatchers.IO) {
serverId?.let { serverDao.getServer(serverId)?.server }
}
fun closeSession() {
_current.value = null
}
@ -170,7 +177,7 @@ class ServerRepository
)
}
if (user != null) {
changeUser(server, user)
return@withContext changeUser(server, user)
} else {
throw IllegalArgumentException("Authentication result's user was null")
}
@ -260,6 +267,7 @@ class ServerRepository
}
}
@Serializable
data class CurrentUser(
val server: JellyfinServer,
val user: JellyfinUser,

View file

@ -41,6 +41,7 @@ data class JellyfinServer(
],
indices = [Index("id", "serverId", unique = true)],
)
@Serializable
data class JellyfinUser(
@PrimaryKey(autoGenerate = true)
val rowId: Int = 0,