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

@ -2,6 +2,7 @@ package com.github.damontecres.wholphin
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
@ -12,18 +13,18 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.unit.dp
import androidx.datastore.core.DataStore
import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.NavDisplay
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Surface
@ -33,24 +34,30 @@ import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.DefaultUserConfiguration
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.AppUpgradeHandler
import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.DeviceProfileService
import com.github.damontecres.wholphin.services.ImageUrlService
import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.services.PlaybackLifecycleObserver
import com.github.damontecres.wholphin.services.RefreshRateService
import com.github.damontecres.wholphin.services.ServerEventListener
import com.github.damontecres.wholphin.services.SetupDestination
import com.github.damontecres.wholphin.services.SetupNavigationManager
import com.github.damontecres.wholphin.services.UpdateChecker
import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient
import com.github.damontecres.wholphin.ui.CoilConfig
import com.github.damontecres.wholphin.ui.LocalImageUrlService
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.nav.ApplicationContent
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.setup.SwitchServerContent
import com.github.damontecres.wholphin.ui.setup.SwitchUserContent
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.github.damontecres.wholphin.ui.util.ProvideLocalClock
import com.github.damontecres.wholphin.util.DebugLogTree
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
@ -59,8 +66,7 @@ import javax.inject.Inject
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var serverRepository: ServerRepository
private val viewModel: MainActivityViewModel by viewModels()
@Inject
lateinit var userPreferencesDataStore: DataStore<AppPreferences>
@ -72,27 +78,26 @@ class MainActivity : AppCompatActivity() {
@Inject
lateinit var navigationManager: NavigationManager
@Inject
lateinit var setupNavigationManager: SetupNavigationManager
@Inject
lateinit var updateChecker: UpdateChecker
@Inject
lateinit var appUpgradeHandler: AppUpgradeHandler
@Inject
lateinit var serverEventListener: ServerEventListener
@Inject
lateinit var playbackLifecycleObserver: PlaybackLifecycleObserver
@Inject
lateinit var deviceProfileService: DeviceProfileService
@Inject
lateinit var imageUrlService: ImageUrlService
@Inject
lateinit var refreshRateService: RefreshRateService
private var signInAuto = true
@OptIn(ExperimentalTvMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -109,9 +114,13 @@ class MainActivity : AppCompatActivity() {
window.attributes = attrs.apply { preferredRefreshRate = mode.refreshRate }
}
}
viewModel.appStart()
setContent {
val appPreferences by userPreferencesDataStore.data.collectAsState(null)
appPreferences?.let { appPreferences ->
LaunchedEffect(appPreferences.signInAutomatically) {
signInAuto = appPreferences.signInAutomatically
}
CoilConfig(
diskCacheSizeBytes =
appPreferences.advancedPreferences.imageDiskCacheSizeBytes.let {
@ -140,100 +149,71 @@ class MainActivity : AppCompatActivity() {
.background(MaterialTheme.colorScheme.background),
shape = RectangleShape,
) {
var isRestoringSession by remember { mutableStateOf(true) }
LaunchedEffect(Unit) {
// TODO PIN-related
// if (appPreferences.signInAutomatically) {
try {
serverRepository.restoreSession(
appPreferences.currentServerId?.toUUIDOrNull(),
appPreferences.currentUserId?.toUUIDOrNull(),
)
} catch (ex: Exception) {
Timber.e(ex, "Exception restoring session")
}
// }
isRestoringSession = false
}
val current by serverRepository.current.observeAsState()
// val backStack = rememberNavBackStack(SetupDestination.Loading)
// setupNavigationManager.backStack = backStack
val backStack = setupNavigationManager.backStack
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
entryDecorators =
listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator(),
),
entryProvider = { key ->
key as SetupDestination
NavEntry(key) {
when (key) {
SetupDestination.Loading -> {
Box(
modifier = Modifier.size(200.dp),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(
color = MaterialTheme.colorScheme.border,
modifier = Modifier.align(Alignment.Center),
)
}
}
val preferences =
UserPreferences(
appPreferences,
current?.userDto?.configuration ?: DefaultUserConfiguration,
)
SetupDestination.ServerList -> {
SwitchServerContent(Modifier.fillMaxSize())
}
if (isRestoringSession) {
Box(
modifier = Modifier.size(200.dp),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(
color = MaterialTheme.colorScheme.border,
modifier = Modifier.align(Alignment.Center),
)
}
} else {
// TODO PIN-related
// DisposableEffect(Unit) {
// onDispose {
// if (!appPreferences.signInAutomatically || current?.user?.hasPin == true) {
// serverRepository.closeSession()
// }
// }
// }
key(current?.server?.id, current?.user?.id) {
// TODO PIN-related
// LaunchedEffect(current?.user?.pin) {
// if (current?.user?.pin?.isNotNullOrBlank() == true) {
// // If user has a pin, then obscure the window in previews
// window?.setFlags(
// WindowManager.LayoutParams.FLAG_SECURE,
// WindowManager.LayoutParams.FLAG_SECURE,
// )
// } else {
// window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
// }
// }
val initialDestination =
when {
current != null -> Destination.Home()
is SetupDestination.UserList -> {
SwitchUserContent(
currentServer = key.server,
Modifier.fillMaxSize(),
)
}
// TODO PIN-related
// !appPreferences.signInAutomatically -> Destination.ServerList
else -> Destination.ServerList
}
val backStack = rememberNavBackStack(initialDestination)
navigationManager.backStack = backStack
if (UpdateChecker.ACTIVE && appPreferences.autoCheckForUpdates) {
LaunchedEffect(Unit) {
try {
updateChecker.maybeShowUpdateToast(appPreferences.updateUrl)
} catch (ex: Exception) {
Timber.w(ex, "Failed to check for update")
is SetupDestination.AppContent -> {
val current = key.current
val preferences =
UserPreferences(
appPreferences,
current.userDto.configuration
?: DefaultUserConfiguration,
)
ProvideLocalClock {
LaunchedEffect(Unit) {
updateChecker.maybeShowUpdateToast(
appPreferences.updateUrl,
)
}
ApplicationContent(
user = current.user,
server = current.server,
navigationManager = navigationManager,
preferences = preferences,
modifier = Modifier.fillMaxSize(),
)
}
}
}
}
LaunchedEffect(current, preferences) {
withContext(Dispatchers.IO) {
deviceProfileService.getOrCreateDeviceProfile(
preferences.appPreferences.playbackPreferences,
current?.server?.serverVersion,
)
}
}
ProvideLocalClock {
ApplicationContent(
user = current?.user,
server = current?.server,
navigationManager = navigationManager,
preferences = preferences,
modifier = Modifier.fillMaxSize(),
)
}
}
}
},
)
}
}
}
@ -250,12 +230,69 @@ class MainActivity : AppCompatActivity() {
override fun onRestart() {
super.onRestart()
// TODO PIN-related
Timber.i("onRestart")
viewModel.appStart()
// val signInAutomatically =
// runBlocking { userPreferencesDataStore.data.firstOrNull()?.signInAutomatically } ?: true
// Timber.i("onRestart: signInAutomatically=$signInAutomatically")
// if (!signInAutomatically || serverRepository.currentUser.value?.hasPin == true) {
// // TODO PIN-related
// // if (!signInAutomatically || serverRepository.currentUser.value?.hasPin == true) {
// if (!signInAutomatically) {
// serverRepository.closeSession()
// }
}
}
@HiltViewModel
class MainActivityViewModel
@Inject
constructor(
private val preferences: DataStore<AppPreferences>,
private val serverRepository: ServerRepository,
private val navigationManager: SetupNavigationManager,
private val deviceProfileService: DeviceProfileService,
private val backdropService: BackdropService,
) : ViewModel() {
fun appStart() {
viewModelScope.launch {
val prefs = preferences.data.firstOrNull() ?: AppPreferences.getDefaultInstance()
if (prefs.signInAutomatically) {
val current =
withContext(Dispatchers.IO) {
serverRepository.restoreSession(
prefs.currentServerId?.toUUIDOrNull(),
prefs.currentUserId?.toUUIDOrNull(),
)
}
if (current != null) {
// Restored
navigationManager.navigateTo(SetupDestination.AppContent(current))
} else {
// Did not restore
navigationManager.navigateTo(SetupDestination.ServerList)
}
} else {
navigationManager.navigateTo(SetupDestination.Loading)
backdropService.clearBackdrop()
val currentServerId = prefs.currentServerId?.toUUIDOrNull()
if (currentServerId != null) {
val currentServer =
withContext(Dispatchers.IO) {
serverRepository.serverDao.getServer(currentServerId)?.server
}
if (currentServer != null) {
navigationManager.navigateTo(SetupDestination.UserList(currentServer))
} else {
navigationManager.navigateTo(SetupDestination.ServerList)
}
} else {
navigationManager.navigateTo(SetupDestination.ServerList)
}
}
}
viewModelScope.launchIO {
// Create the mediaCodecCapabilitiesTest if needed
deviceProfileService.mediaCodecCapabilitiesTest.supportsAVC()
}
}
}

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,

View file

@ -851,8 +851,7 @@ val basicPreferences =
title = R.string.ui_interface,
preferences =
listOf(
// TODO PIN-related
// AppPreference.SignInAuto,
AppPreference.SignInAuto,
AppPreference.HomePageItems,
AppPreference.CombineContinueNext,
AppPreference.RewatchNextUp,

View file

@ -20,7 +20,7 @@ class DeviceProfileService
constructor(
@param:ApplicationContext private val context: Context,
) {
private val mediaCodecCapabilitiesTest by lazy {
val mediaCodecCapabilitiesTest by lazy {
// Created lazily below on the IO thread since it cn take time
MediaCodecCapabilitiesTest(context)
}

View file

@ -29,12 +29,7 @@ class NavigationManager
*/
fun navigateToFromDrawer(destination: Destination) {
goToHome()
if (destination == Destination.ServerList || destination is Destination.UserList) {
backStack.add(destination)
(0..<backStack.size - 1).forEach { _ -> backStack.removeAt(0) }
} else {
backStack.add(destination)
}
backStack.add(destination)
log()
}

View file

@ -0,0 +1,54 @@
package com.github.damontecres.wholphin.services
import androidx.compose.runtime.mutableStateListOf
import androidx.navigation3.runtime.NavKey
import com.github.damontecres.wholphin.data.CurrentUser
import com.github.damontecres.wholphin.data.model.JellyfinServer
import kotlinx.serialization.Serializable
import org.acra.ACRA
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
/**
* Manages navigating for setup
*/
@Singleton
class SetupNavigationManager
@Inject
constructor() {
var backStack: MutableList<NavKey> = mutableStateListOf(SetupDestination.Loading)
/**
* Go to the specified [SetupDestination]
*/
fun navigateTo(destination: SetupDestination) {
backStack[0] = destination
log()
}
private fun log() {
val dest = backStack.lastOrNull().toString()
Timber.i("Current setup destination: %s", dest)
ACRA.errorReporter.putCustomData("setupDestination", dest)
}
}
@Serializable
sealed interface SetupDestination : NavKey {
@Serializable
data object Loading : SetupDestination
@Serializable
data object ServerList : SetupDestination
@Serializable
data class UserList(
val server: JellyfinServer,
) : SetupDestination
@Serializable
data class AppContent(
val current: CurrentUser,
) : SetupDestination
}

View file

@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.saveable.rememberSerializable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
@ -24,8 +25,12 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.runtime.serialization.NavBackStackSerializer
import androidx.navigation3.runtime.serialization.NavKeySerializer
import androidx.navigation3.ui.NavDisplay
import androidx.tv.material3.MaterialTheme
import coil3.compose.AsyncImage
@ -62,13 +67,22 @@ class ApplicationContentViewModel
*/
@Composable
fun ApplicationContent(
server: JellyfinServer?,
user: JellyfinUser?,
server: JellyfinServer,
user: JellyfinUser,
navigationManager: NavigationManager,
preferences: UserPreferences,
modifier: Modifier = Modifier,
viewModel: ApplicationContentViewModel = hiltViewModel(),
) {
val backStack: MutableList<NavKey> =
rememberSerializable(
server,
user,
serializer = NavBackStackSerializer(elementSerializer = NavKeySerializer()),
) {
NavBackStack(Destination.Home())
}
navigationManager.backStack = backStack
val backdrop by viewModel.backdropService.backdropFlow.collectAsStateWithLifecycle()
val backdropStyle = preferences.appPreferences.interfacePreferences.backdropStyle
Box(

View file

@ -7,7 +7,6 @@ import androidx.navigation3.runtime.NavKey
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.GetItemsFilter
import com.github.damontecres.wholphin.data.model.ItemPlayback
import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.ui.data.SortAndDirection
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisode
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
@ -28,14 +27,6 @@ import java.util.UUID
sealed class Destination(
val fullScreen: Boolean = false,
) : NavKey {
@Serializable
data object ServerList : Destination(true)
@Serializable
data class UserList(
val server: JellyfinServer,
) : Destination(true)
@Serializable
data class Home(
val id: Long = 0L,

View file

@ -29,8 +29,6 @@ import com.github.damontecres.wholphin.ui.main.SearchPage
import com.github.damontecres.wholphin.ui.playback.PlaybackPage
import com.github.damontecres.wholphin.ui.preferences.PreferencesPage
import com.github.damontecres.wholphin.ui.setup.InstallUpdatePage
import com.github.damontecres.wholphin.ui.setup.SwitchServerContent
import com.github.damontecres.wholphin.ui.setup.SwitchUserContent
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.CollectionType
import timber.log.Timber
@ -66,14 +64,6 @@ fun DestinationContent(
)
}
Destination.ServerList -> {
SwitchServerContent(modifier)
}
is Destination.UserList -> {
SwitchUserContent(destination.server, modifier)
}
is Destination.Settings -> {
PreferencesPage(
preferences.appPreferences,

View file

@ -79,6 +79,8 @@ import com.github.damontecres.wholphin.preferences.AppThemeColors
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.services.SetupDestination
import com.github.damontecres.wholphin.services.SetupNavigationManager
import com.github.damontecres.wholphin.ui.FontAwesome
import com.github.damontecres.wholphin.ui.components.TimeDisplay
import com.github.damontecres.wholphin.ui.ifElse
@ -106,6 +108,7 @@ class NavDrawerViewModel
constructor(
private val navDrawerItemRepository: NavDrawerItemRepository,
val navigationManager: NavigationManager,
val setupNavigationManager: SetupNavigationManager,
val backdropService: BackdropService,
) : ViewModel() {
private var all: List<NavDrawerItem>? = null
@ -361,10 +364,8 @@ fun NavDrawer(
drawerOpen = drawerState.isOpen,
interactionSource = interactionSource,
onClick = {
viewModel.navigationManager.navigateToFromDrawer(
Destination.UserList(
server,
),
viewModel.setupNavigationManager.navigateTo(
SetupDestination.UserList(server),
)
},
modifier = Modifier.padding(start = drawerPadding),

View file

@ -9,9 +9,9 @@ import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.JellyfinServerDao
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.services.SetupDestination
import com.github.damontecres.wholphin.services.SetupNavigationManager
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.setValueOnMain
import com.github.damontecres.wholphin.ui.showToast
import com.github.damontecres.wholphin.util.LoadingState
@ -41,7 +41,7 @@ class SwitchServerViewModel
val jellyfin: Jellyfin,
val serverRepository: ServerRepository,
val serverDao: JellyfinServerDao,
val navigationManager: NavigationManager,
val navigationManager: SetupNavigationManager,
) : ViewModel() {
val servers = MutableLiveData<List<JellyfinServer>>(listOf())
val serverStatus = MutableLiveData<Map<UUID, ServerConnectionStatus>>(mapOf())
@ -156,7 +156,7 @@ class SwitchServerViewModel
)
serverRepository.addAndChangeServer(updatedServer)
withContext(Dispatchers.Main) {
navigationManager.navigateTo(Destination.UserList(server))
navigationManager.navigateTo(SetupDestination.UserList(updatedServer))
}
} else if (result is ServerConnectionStatus.Error) {
showToast(context, "Error connecting: $${result.message}")
@ -201,7 +201,7 @@ class SwitchServerViewModel
}
withContext(Dispatchers.Main) {
addServerState.value = LoadingState.Success
navigationManager.navigateTo(Destination.UserList(server))
navigationManager.navigateTo(SetupDestination.UserList(server))
}
} else {
withContext(Dispatchers.Main) {

View file

@ -37,6 +37,7 @@ import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.data.model.JellyfinUser
import com.github.damontecres.wholphin.services.SetupDestination
import com.github.damontecres.wholphin.ui.components.BasicDialog
import com.github.damontecres.wholphin.ui.components.CircularProgress
import com.github.damontecres.wholphin.ui.components.EditTextBox
@ -130,7 +131,9 @@ fun SwitchUserContent(
viewModel.removeUser(user)
},
onSwitchServer = {
viewModel.navigationManager.navigateTo(Destination.ServerList)
viewModel.setupNavigationManager.navigateTo(
SetupDestination.ServerList,
)
},
modifier = Modifier.fillMaxWidth(),
)

View file

@ -9,6 +9,8 @@ import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.data.model.JellyfinUser
import com.github.damontecres.wholphin.services.ImageUrlService
import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.services.SetupDestination
import com.github.damontecres.wholphin.services.SetupNavigationManager
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.setValueOnMain
import com.github.damontecres.wholphin.util.ExceptionHandler
@ -43,6 +45,7 @@ class SwitchUserViewModel
val serverRepository: ServerRepository,
val serverDao: JellyfinServerDao,
val navigationManager: NavigationManager,
val setupNavigationManager: SetupNavigationManager,
val imageUrlService: ImageUrlService,
@Assisted val server: JellyfinServer,
) : ViewModel() {
@ -120,9 +123,11 @@ class SwitchUserViewModel
fun switchUser(user: JellyfinUser) {
viewModelScope.launchIO {
try {
serverRepository.changeUser(server, user)
withContext(Dispatchers.Main) {
navigationManager.goToHome()
val current = serverRepository.changeUser(server, user)
if (current != null) {
withContext(Dispatchers.Main) {
setupNavigationManager.navigateTo(SetupDestination.AppContent(current))
}
}
} catch (ex: Exception) {
Timber.e(ex, "Error switching user")
@ -144,9 +149,11 @@ class SwitchUserViewModel
username = username,
password = password,
)
serverRepository.changeUser(server.url, authenticationResult)
withContext(Dispatchers.Main) {
navigationManager.goToHome()
val current = serverRepository.changeUser(server.url, authenticationResult)
if (current != null) {
withContext(Dispatchers.Main) {
setupNavigationManager.navigateTo(SetupDestination.AppContent(current))
}
}
} catch (ex: Exception) {
Timber.e(ex, "Error logging in user")
@ -192,9 +199,13 @@ class SwitchUserViewModel
val authenticationResult by api.userApi.authenticateWithQuickConnect(
QuickConnectDto(secret = state.secret),
)
serverRepository.changeUser(server.url, authenticationResult)
withContext(Dispatchers.Main) {
navigationManager.goToHome()
val current = serverRepository.changeUser(server.url, authenticationResult)
if (current != null) {
withContext(Dispatchers.Main) {
setupNavigationManager.navigateTo(
SetupDestination.AppContent(current),
)
}
}
} catch (ex: Exception) {
Timber.e(ex, "Error during quick connect")