From eb4e9b93beb0c92a98e9f5d2440964078b34bf93 Mon Sep 17 00:00:00 2001 From: Ray <154766448+damontecres@users.noreply.github.com> Date: Mon, 22 Dec 2025 14:52:09 -0500 Subject: [PATCH] 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 --- .../damontecres/wholphin/MainActivity.kt | 251 ++++++++++-------- .../wholphin/data/ServerRepository.kt | 92 ++++--- .../wholphin/data/model/JellyfinServer.kt | 1 + .../wholphin/preferences/AppPreference.kt | 3 +- .../wholphin/services/DeviceProfileService.kt | 2 +- .../wholphin/services/NavigationManager.kt | 7 +- .../services/SetupNavigationManager.kt | 54 ++++ .../wholphin/ui/nav/ApplicationContent.kt | 18 +- .../wholphin/ui/nav/Destination.kt | 9 - .../wholphin/ui/nav/DestinationContent.kt | 10 - .../damontecres/wholphin/ui/nav/NavDrawer.kt | 9 +- .../ui/setup/SwitchServerViewModel.kt | 10 +- .../wholphin/ui/setup/SwitchUserContent.kt | 5 +- .../wholphin/ui/setup/SwitchUserViewModel.kt | 29 +- 14 files changed, 302 insertions(+), 198 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/wholphin/services/SetupNavigationManager.kt diff --git a/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt b/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt index 52a63020..22e41a49 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt @@ -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 @@ -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, + 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() + } + } + } diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/ServerRepository.kt b/app/src/main/java/com/github/damontecres/wholphin/data/ServerRepository.kt index 48bea5b3..539c8ac4 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/ServerRepository.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/ServerRepository.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/JellyfinServer.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/JellyfinServer.kt index d8e40779..2c516272 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/JellyfinServer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/JellyfinServer.kt @@ -41,6 +41,7 @@ data class JellyfinServer( ], indices = [Index("id", "serverId", unique = true)], ) +@Serializable data class JellyfinUser( @PrimaryKey(autoGenerate = true) val rowId: Int = 0, diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt index 4d6154d6..6bca9d38 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/DeviceProfileService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/DeviceProfileService.kt index ed04c3f2..9ce70b21 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/DeviceProfileService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/DeviceProfileService.kt @@ -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) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/NavigationManager.kt b/app/src/main/java/com/github/damontecres/wholphin/services/NavigationManager.kt index a387ac8c..136cb746 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/NavigationManager.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/NavigationManager.kt @@ -29,12 +29,7 @@ class NavigationManager */ fun navigateToFromDrawer(destination: Destination) { goToHome() - if (destination == Destination.ServerList || destination is Destination.UserList) { - backStack.add(destination) - (0.. backStack.removeAt(0) } - } else { - backStack.add(destination) - } + backStack.add(destination) log() } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/SetupNavigationManager.kt b/app/src/main/java/com/github/damontecres/wholphin/services/SetupNavigationManager.kt new file mode 100644 index 00000000..bd95ea1d --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/services/SetupNavigationManager.kt @@ -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 = 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 +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/ApplicationContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/ApplicationContent.kt index 2d3200ba..7e7d0d2b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/ApplicationContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/ApplicationContent.kt @@ -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 = + 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( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Destination.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Destination.kt index 9310d0fa..b9452daa 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Destination.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Destination.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt index 2f6feb7a..038f231a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt index 9a7d3559..ef858eb7 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt @@ -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? = 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), diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerViewModel.kt index 75af5155..f9e7d0a3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerViewModel.kt @@ -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>(listOf()) val serverStatus = MutableLiveData>(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) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserContent.kt index 19c00f18..b71e0af0 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserContent.kt @@ -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(), ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserViewModel.kt index 7da64ca7..68b9e583 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserViewModel.kt @@ -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")