Refactoring switching server or user (#205)

Now when initiating a user or server switch, you can't back out of it
and must select a user/server. This will be useful when PIN code are
implemented down the road.

The app will now disconnect the web socket when backgrounded.

Also lots of internal refactoring
This commit is contained in:
damontecres 2025-11-12 17:44:16 -05:00 committed by GitHub
parent 2e3e4e3f5f
commit 58395e9adf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 442 additions and 277 deletions

View file

@ -108,7 +108,7 @@ class CollectionFolderViewModel
val sortAndDirection =
if (initialSortAndDirection == null) {
serverRepository.currentUser?.let { user ->
serverRepository.currentUser.value?.let { user ->
libraryDisplayInfoDao.getItem(user, itemId)?.sortAndDirection
} ?: SortAndDirection.DEFAULT
} else {
@ -123,7 +123,7 @@ class CollectionFolderViewModel
recursive: Boolean,
filter: GetItemsFilter,
) {
serverRepository.currentUser?.let { user ->
serverRepository.currentUser.value?.let { user ->
viewModelScope.launch(Dispatchers.IO) {
val libraryDisplayInfo =
LibraryDisplayInfo(

View file

@ -133,7 +133,7 @@ class RecommendedMovieViewModel
val suggestionsRequest =
GetSuggestionsRequest(
userId = serverRepository.currentUser?.id,
userId = serverRepository.currentUser.value?.id,
type = listOf(BaseItemKind.MOVIE),
)
val suggestedItems = ApiRequestPager(api, suggestionsRequest, GetSuggestionsRequestHandler, viewModelScope)

View file

@ -161,7 +161,7 @@ class RecommendedTvShowViewModel
val suggestionsRequest =
GetSuggestionsRequest(
userId = serverRepository.currentUser?.id,
userId = serverRepository.currentUser.value?.id,
type = listOf(BaseItemKind.SERIES),
)
val suggestedItems = ApiRequestPager(api, suggestionsRequest, GetSuggestionsRequestHandler, viewModelScope)

View file

@ -66,7 +66,7 @@ class LiveTvCollectionViewModel
viewModelScope.launchIO {
val folders =
api.liveTvApi
.getRecordingFolders(userId = serverRepository.currentUser?.id)
.getRecordingFolders(userId = serverRepository.currentUser.value?.id)
.content.items
.map { TabId(it.name ?: "Recordings", it.id) }
this@LiveTvCollectionViewModel.recordingFolders.setValueOnMain(folders)

View file

@ -61,7 +61,7 @@ class DebugViewModel
init {
viewModelScope.launchIO {
serverRepository.currentUser?.rowId?.let {
serverRepository.currentUser.value?.rowId?.let {
val results = itemPlaybackDao.getItems(it)
withContext(Dispatchers.Main) {
itemPlaybacks.value = results

View file

@ -303,7 +303,7 @@ fun MovieDetails(
ItemDetailsDialog(
info = info,
showFilePath =
viewModel.serverRepository.currentUserDto
viewModel.serverRepository.currentUserDto.value
?.policy
?.isAdministrator == true,
onDismissRequest = { overviewDialog = null },

View file

@ -147,7 +147,7 @@ class MovieViewModel
api.libraryApi
.getSimilarItems(
GetSimilarItemsRequest(
userId = serverRepository.currentUser?.id,
userId = serverRepository.currentUser.value?.id,
itemId = itemId,
fields = SlimItemFields,
limit = 25,

View file

@ -346,7 +346,7 @@ fun SeriesOverview(
ItemDetailsDialog(
info = info,
showFilePath =
viewModel.serverRepository.currentUserDto
viewModel.serverRepository.currentUserDto.value
?.policy
?.isAdministrator == true,
onDismissRequest = { overviewDialog = null },

View file

@ -121,7 +121,7 @@ class SeriesViewModel
api.libraryApi
.getSimilarItems(
GetSimilarItemsRequest(
userId = serverRepository.currentUser?.id,
userId = serverRepository.currentUser.value?.id,
itemId = itemId,
fields = SlimItemFields,
limit = 25,

View file

@ -77,7 +77,7 @@ class HomeViewModel
) {
Timber.d("init HomeViewModel")
serverRepository.currentUserDto?.let { userDto ->
serverRepository.currentUserDto.value?.let { userDto ->
val includedIds =
navDrawerItemRepository
.getFilteredNavDrawerItems(navDrawerItemRepository.getNavDrawerItems())

View file

@ -10,6 +10,7 @@ import androidx.navigation3.ui.NavDisplay
import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.data.model.JellyfinUser
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.components.ErrorMessage
import org.jellyfin.sdk.model.api.DeviceProfile
/**
@ -45,7 +46,7 @@ fun ApplicationContent(
deviceProfile = deviceProfile,
modifier = modifier.fillMaxSize(),
)
} else {
} else if (user != null && server != null) {
NavDrawer(
destination = key,
preferences = preferences,
@ -54,6 +55,8 @@ fun ApplicationContent(
server = server,
modifier = modifier,
)
} else {
ErrorMessage("Trying to go to $key without a user logged in", null)
}
}
},

View file

@ -6,6 +6,7 @@ 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.detail.series.SeasonEpisode
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption
@ -29,7 +30,9 @@ sealed class Destination(
data object ServerList : Destination(true)
@Serializable
data object UserList : Destination(true)
data class UserList(
val server: JellyfinServer,
) : Destination(true)
@Serializable
data class Home(

View file

@ -54,7 +54,7 @@ fun DestinationContent(
)
Destination.ServerList -> SwitchServerContent(modifier)
Destination.UserList -> SwitchUserContent(modifier)
is Destination.UserList -> SwitchUserContent(destination.server, modifier)
is Destination.Settings ->
PreferencesPage(

View file

@ -84,7 +84,6 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.model.api.CollectionType
import org.jellyfin.sdk.model.api.DeviceProfile
import timber.log.Timber
import java.time.LocalTime
import java.util.UUID
import javax.inject.Inject
@ -141,7 +140,7 @@ class NavDrawerViewModel
null
}
}
Timber.v("Found $index => $key")
// Timber.v("Found $index => $key")
if (index != null) {
selectedIndex.setValueOnMain(index)
break
@ -198,8 +197,8 @@ data class ServerNavDrawerItem(
fun NavDrawer(
destination: Destination,
preferences: UserPreferences,
user: JellyfinUser?,
server: JellyfinServer?,
user: JellyfinUser,
server: JellyfinServer,
deviceProfile: DeviceProfile,
modifier: Modifier = Modifier,
viewModel: NavDrawerViewModel =
@ -319,7 +318,11 @@ fun NavDrawer(
selected = false,
interactionSource = interactionSource,
onClick = {
viewModel.navigationManager.navigateTo(Destination.UserList)
viewModel.navigationManager.navigateToFromDrawer(
Destination.UserList(
server,
),
)
},
modifier = Modifier.animateItem(),
)

View file

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

View file

@ -268,13 +268,14 @@ class PlaybackViewModel
if (itemPlayback != null) {
itemPlayback
} else {
val user = serverRepository.currentUser!!
itemPlaybackDao.getItem(user, base.id)?.let {
Timber.v("Fetched itemPlayback from DB: %s", it)
if (it.sourceId != null) {
it
} else {
null
serverRepository.currentUser.value?.let { user ->
itemPlaybackDao.getItem(user, base.id)?.let {
Timber.v("Fetched itemPlayback from DB: %s", it)
if (it.sourceId != null) {
it
} else {
null
}
}
}
}

View file

@ -49,7 +49,7 @@ class PreferencesViewModel
init {
viewModelScope.launchIO {
serverRepository.currentUser?.let { user ->
serverRepository.currentUser.value?.let { user ->
allNavDrawerItems = navDrawerItemRepository.getNavDrawerItems()
val pins = serverPreferencesDao.getNavDrawerPinnedItems(user)
val navDrawerPins = allNavDrawerItems.associateWith { pins.isPinned(it.id) }
@ -60,7 +60,7 @@ class PreferencesViewModel
fun updatePins(newSelectedItems: List<NavDrawerItem>) {
viewModelScope.launchIO(ExceptionHandler(true)) {
serverRepository.currentUser?.let { user ->
serverRepository.currentUser.value?.let { user ->
val disabledItems =
mutableListOf<NavDrawerItem>().apply {
addAll(allNavDrawerItems)

View file

@ -49,6 +49,7 @@ fun ServerList(
servers: List<JellyfinServer>,
connectionStatus: Map<UUID, ServerConnectionStatus>,
onSwitchServer: (JellyfinServer) -> Unit,
onTestServer: (JellyfinServer) -> Unit,
onAddServer: () -> Unit,
onRemoveServer: (JellyfinServer) -> Unit,
allowAdd: Boolean,
@ -61,7 +62,7 @@ fun ServerList(
items(servers) { server ->
val status = connectionStatus[server.id] ?: ServerConnectionStatus.Pending
ListItem(
enabled = status == ServerConnectionStatus.Success,
enabled = true,
selected = false,
headlineContent = { Text(text = server.name?.ifBlank { null } ?: server.url) },
supportingContent = { if (server.name.isNotNullOrBlank()) Text(text = server.url) },
@ -77,12 +78,18 @@ fun ServerList(
Icon(
imageVector = Icons.Default.Warning,
contentDescription = status.message,
tint = MaterialTheme.colorScheme.error,
tint = MaterialTheme.colorScheme.errorContainer,
)
}
}
},
onClick = { onSwitchServer.invoke(server) },
onClick = {
when (status) {
ServerConnectionStatus.Success -> onSwitchServer.invoke(server)
ServerConnectionStatus.Pending -> {}
is ServerConnectionStatus.Error -> onTestServer.invoke(server)
}
},
onLongClick = {
if (allowDelete) {
showDeleteDialog = server

View file

@ -43,9 +43,8 @@ import com.github.damontecres.wholphin.util.LoadingState
@Composable
fun SwitchServerContent(
modifier: Modifier = Modifier,
viewModel: SwitchUserViewModel = hiltViewModel(),
viewModel: SwitchServerViewModel = hiltViewModel(),
) {
val currentServer = viewModel.serverRepository.currentServer
val servers by viewModel.servers.observeAsState(listOf())
val serverStatus by viewModel.serverStatus.observeAsState(mapOf())
@ -90,6 +89,9 @@ fun SwitchServerContent(
onSwitchServer = {
viewModel.addServer(it.url)
},
onTestServer = {
viewModel.testServer(it)
},
onAddServer = {
showAddServer = true
},
@ -142,6 +144,9 @@ fun SwitchServerContent(
onSwitchServer = {
viewModel.addServer(it.url)
},
onTestServer = {
viewModel.testServer(it)
},
onAddServer = {},
onRemoveServer = {},
allowAdd = false,

View file

@ -0,0 +1,214 @@
package com.github.damontecres.wholphin.ui.setup
import android.content.Context
import android.widget.Toast
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
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.ui.launchIO
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.nav.NavigationManager
import com.github.damontecres.wholphin.ui.showToast
import com.github.damontecres.wholphin.util.LoadingState
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.Jellyfin
import org.jellyfin.sdk.api.client.HttpClientOptions
import org.jellyfin.sdk.api.client.extensions.quickConnectApi
import org.jellyfin.sdk.api.client.extensions.systemApi
import org.jellyfin.sdk.model.serializer.toUUID
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
@HiltViewModel
class SwitchServerViewModel
@Inject
constructor(
@param:ApplicationContext private val context: Context,
val jellyfin: Jellyfin,
val serverRepository: ServerRepository,
val serverDao: JellyfinServerDao,
val navigationManager: NavigationManager,
) : ViewModel() {
val servers = MutableLiveData<List<JellyfinServer>>(listOf())
val serverStatus = MutableLiveData<Map<UUID, ServerConnectionStatus>>(mapOf())
val serverQuickConnect = MutableLiveData<Map<UUID, Boolean>>(mapOf())
val discoveredServers = MutableLiveData<List<JellyfinServer>>(listOf())
val addServerState = MutableLiveData<LoadingState>(LoadingState.Pending)
fun clearAddServerState() {
addServerState.value = LoadingState.Pending
}
init {
init()
}
fun init() {
viewModelScope.launchIO {
withContext(Dispatchers.Main) {
serverStatus.value = mapOf()
serverQuickConnect.value = mapOf()
}
val allServers =
serverDao
.getServers()
.map { it.server }
.sortedWith(compareBy<JellyfinServer> { it.name }.thenBy { it.url })
withContext(Dispatchers.Main) {
servers.value = allServers
}
allServers.forEach { server ->
internalTestServer(server)
}
}
}
fun testServer(server: JellyfinServer) {
serverStatus.value =
serverStatus.value!!.toMutableMap().apply {
put(
server.id,
ServerConnectionStatus.Pending,
)
}
viewModelScope.launchIO {
delay(1000)
val result = internalTestServer(server)
if (result == ServerConnectionStatus.Success) {
showToast(context, context.getString(R.string.success), Toast.LENGTH_SHORT)
} else if (result is ServerConnectionStatus.Error) {
showToast(context, result.message ?: "Error", Toast.LENGTH_SHORT)
}
}
}
private suspend fun internalTestServer(server: JellyfinServer): ServerConnectionStatus =
try {
jellyfin
.createApi(
server.url,
httpClientOptions =
HttpClientOptions(
requestTimeout = 6.seconds,
connectTimeout = 6.seconds,
socketTimeout = 6.seconds,
),
).systemApi
.getPublicSystemInfo()
withContext(Dispatchers.Main) {
serverStatus.value =
serverStatus.value!!.toMutableMap().apply {
put(
server.id,
ServerConnectionStatus.Success,
)
}
}
ServerConnectionStatus.Success
} catch (ex: Exception) {
val status = ServerConnectionStatus.Error(ex.localizedMessage)
Timber.w(ex, "Error checking server ${server.url}")
withContext(Dispatchers.Main) {
serverStatus.value =
serverStatus.value!!.toMutableMap().apply {
put(
server.id,
status,
)
}
}
status
}
fun addServer(serverUrl: String) {
addServerState.value = LoadingState.Loading
viewModelScope.launchIO {
try {
val serverInfo by jellyfin
.createApi(serverUrl)
.systemApi
.getPublicSystemInfo()
val id = serverInfo.id?.toUUIDOrNull()
if (id != null && serverInfo.startupWizardCompleted == true) {
val server =
JellyfinServer(
id = id,
name = serverInfo.serverName,
url = serverUrl,
)
serverRepository.addAndChangeServer(server)
val quickConnect =
jellyfin
.createApi(serverUrl)
.quickConnectApi
.getQuickConnectEnabled()
.content
withContext(Dispatchers.Main) {
serverQuickConnect.value =
serverQuickConnect.value!!.toMutableMap().apply {
put(id, quickConnect)
}
}
withContext(Dispatchers.Main) {
addServerState.value = LoadingState.Success
navigationManager.navigateTo(Destination.UserList(server))
}
} else {
withContext(Dispatchers.Main) {
addServerState.value =
LoadingState.Error("Server returned invalid response")
}
}
} catch (ex: Exception) {
Timber.w(ex, "Error creating API for $serverUrl")
withContext(Dispatchers.Main) {
addServerState.value =
LoadingState.Error(exception = ex)
}
}
}
}
fun removeServer(server: JellyfinServer) {
viewModelScope.launchIO {
serverRepository.removeServer(server)
init()
}
}
fun discoverServers() {
viewModelScope.launchIO {
jellyfin.discovery.discoverLocalServers().collect { server ->
val newServerList =
discoveredServers.value!!
.toMutableList()
.apply {
add(
JellyfinServer(
server.id.toUUID(),
server.name,
server.address,
),
)
}
withContext(Dispatchers.Main) {
discoveredServers.value = newServerList
}
}
}
}
}

View file

@ -39,6 +39,7 @@ import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.ui.components.BasicDialog
import com.github.damontecres.wholphin.ui.components.CircularProgress
import com.github.damontecres.wholphin.ui.components.EditTextBox
@ -49,17 +50,20 @@ import com.github.damontecres.wholphin.util.LoadingState
@Composable
fun SwitchUserContent(
currentServer: JellyfinServer,
modifier: Modifier = Modifier,
viewModel: SwitchUserViewModel = hiltViewModel(),
viewModel: SwitchUserViewModel =
hiltViewModel<SwitchUserViewModel, SwitchUserViewModel.Factory>(
creationCallback = { it.create(currentServer) },
),
) {
val context = LocalContext.current
val currentServer = viewModel.serverRepository.currentServer
val currentUser = viewModel.serverRepository.currentUser
// val currentServer by viewModel.serverRepository.currentServer.observeAsState()
val currentUser by viewModel.serverRepository.currentUser.observeAsState()
val users by viewModel.users.observeAsState(listOf())
val serverQuickConnect by viewModel.serverQuickConnect.observeAsState(mapOf())
val quickConnectEnabled = currentServer?.let { serverQuickConnect[it.id] ?: false } ?: false
val quickConnectEnabled by viewModel.serverQuickConnect.observeAsState(false)
val quickConnect by viewModel.quickConnectState.observeAsState(null)
var showAddUser by remember { mutableStateOf(false) }
@ -109,7 +113,7 @@ fun SwitchUserContent(
users = users,
currentUser = currentUser,
onSwitchUser = { user ->
viewModel.switchUser(server, user)
viewModel.switchUser(user)
},
onAddUser = { showAddUser = true },
onRemoveUser = { user ->

View file

@ -8,13 +8,18 @@ import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.data.model.JellyfinUser
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.nav.NavigationManager
import com.github.damontecres.wholphin.ui.setValueOnMain
import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.Jellyfin
import org.jellyfin.sdk.api.client.HttpClientOptions
@ -25,42 +30,41 @@ import org.jellyfin.sdk.api.client.extensions.systemApi
import org.jellyfin.sdk.api.client.extensions.userApi
import org.jellyfin.sdk.model.api.QuickConnectDto
import org.jellyfin.sdk.model.api.QuickConnectResult
import org.jellyfin.sdk.model.serializer.toUUID
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
@HiltViewModel
@HiltViewModel(assistedFactory = SwitchUserViewModel.Factory::class)
class SwitchUserViewModel
@Inject
@AssistedInject
constructor(
val jellyfin: Jellyfin,
val serverRepository: ServerRepository,
val serverDao: JellyfinServerDao,
val navigationManager: NavigationManager,
@Assisted val server: JellyfinServer,
) : ViewModel() {
val servers = MutableLiveData<List<JellyfinServer>>(listOf())
val serverStatus = MutableLiveData<Map<UUID, ServerConnectionStatus>>(mapOf())
val serverQuickConnect = MutableLiveData<Map<UUID, Boolean>>(mapOf())
@AssistedFactory
interface Factory {
fun create(server: JellyfinServer): SwitchUserViewModel
}
init {
viewModelScope.launch(Dispatchers.Main + ExceptionHandler()) {
serverRepository.switchServerOrUser()
}
}
val serverQuickConnect = MutableLiveData<Boolean>(false)
val users = MutableLiveData<List<JellyfinUser>>(listOf())
val quickConnectState = MutableLiveData<QuickConnectResult?>(null)
private var quickConnectJob: Job? = null
val discoveredServers = MutableLiveData<List<JellyfinServer>>(listOf())
val addServerState = MutableLiveData<LoadingState>(LoadingState.Pending)
val switchUserState = MutableLiveData<LoadingState>(LoadingState.Pending)
val loginAttempts = MutableLiveData(0)
fun clearAddServerState() {
addServerState.value = LoadingState.Pending
}
fun clearSwitchUserState() {
switchUserState.value = LoadingState.Pending
}
@ -74,91 +78,47 @@ class SwitchUserViewModel
}
fun init() {
quickConnectJob?.cancel()
viewModelScope.launchIO {
quickConnectJob?.cancel()
users.setValueOnMain(listOf())
val serverUsers =
serverDao.getServer(server.id)?.users?.sortedBy { it.name } ?: listOf()
withContext(Dispatchers.Main) {
users.value = listOf()
serverStatus.value = mapOf()
serverQuickConnect.value = mapOf()
}
val allServers =
serverDao
.getServers()
.map { it.server }
.sortedWith(compareBy<JellyfinServer> { it.name }.thenBy { it.url })
withContext(Dispatchers.Main) {
servers.value = allServers
}
allServers.forEach { server ->
try {
jellyfin
.createApi(
server.url,
httpClientOptions =
HttpClientOptions(
requestTimeout = 6.seconds,
connectTimeout = 6.seconds,
socketTimeout = 6.seconds,
),
).systemApi
.getPublicSystemInfo()
val quickConnect by
jellyfin
.createApi(server.url)
.quickConnectApi
.getQuickConnectEnabled()
withContext(Dispatchers.Main) {
serverStatus.value =
serverStatus.value!!.toMutableMap().apply {
put(
server.id,
ServerConnectionStatus.Success,
)
}
serverQuickConnect.value =
serverQuickConnect.value!!.toMutableMap().apply {
put(server.id, quickConnect)
}
}
} catch (ex: Exception) {
Timber.w(ex, "Error checking quick connect for server ${server.url}")
withContext(Dispatchers.Main) {
serverStatus.value =
serverStatus.value!!.toMutableMap().apply {
put(
server.id,
ServerConnectionStatus.Error(ex.localizedMessage),
)
}
}
}
users.setValueOnMain(serverUsers)
}
}
viewModelScope.launchIO {
serverRepository.currentServer?.let {
val quickConnect =
try {
jellyfin
.createApi(
server.url,
httpClientOptions =
HttpClientOptions(
requestTimeout = 6.seconds,
connectTimeout = 6.seconds,
socketTimeout = 6.seconds,
),
).systemApi
.getPublicSystemInfo()
val quickConnect by
jellyfin
.createApi(it.url)
.createApi(server.url)
.quickConnectApi
.getQuickConnectEnabled()
.content
val serverUsers = serverDao.getServer(it.id)?.users?.sortedBy { it.name } ?: listOf()
withContext(Dispatchers.Main) {
serverQuickConnect.value =
serverQuickConnect.value!!.toMutableMap().apply {
put(it.id, quickConnect)
}
users.value = serverUsers
serverQuickConnect.value = quickConnect
}
} catch (ex: Exception) {
Timber.w(ex, "Error checking quick connect for server ${server.url}")
withContext(Dispatchers.Main) {
serverQuickConnect.value = false
}
}
}
}
fun switchUser(
server: JellyfinServer,
user: JellyfinUser,
) {
fun switchUser(user: JellyfinUser) {
viewModelScope.launchIO {
try {
serverRepository.changeUser(server, user)
@ -242,10 +202,7 @@ class SwitchUserViewModel
if (ex is InvalidStatusException && ex.status == 401) {
withContext(Dispatchers.Main) {
quickConnectState.value = null
serverQuickConnect.value =
serverQuickConnect.value!!.toMutableMap().apply {
put(server.id, false)
}
serverQuickConnect.value = false
}
}
setError("Error with Quick Connect", ex)
@ -258,55 +215,6 @@ class SwitchUserViewModel
quickConnectState.value = null
}
fun addServer(serverUrl: String) {
addServerState.value = LoadingState.Loading
viewModelScope.launchIO {
try {
val serverInfo by jellyfin
.createApi(serverUrl)
.systemApi
.getPublicSystemInfo()
val id = serverInfo.id?.toUUIDOrNull()
if (id != null && serverInfo.startupWizardCompleted == true) {
serverRepository.addAndChangeServer(
JellyfinServer(
id = id,
name = serverInfo.serverName,
url = serverUrl,
),
)
val quickConnect =
jellyfin
.createApi(serverUrl)
.quickConnectApi
.getQuickConnectEnabled()
.content
withContext(Dispatchers.Main) {
serverQuickConnect.value =
serverQuickConnect.value!!.toMutableMap().apply {
put(id, quickConnect)
}
}
withContext(Dispatchers.Main) {
addServerState.value = LoadingState.Success
navigationManager.navigateTo(Destination.UserList)
}
} else {
withContext(Dispatchers.Main) {
addServerState.value =
LoadingState.Error("Server returned invalid response")
}
}
} catch (ex: Exception) {
Timber.w(ex, "Error creating API for $serverUrl")
withContext(Dispatchers.Main) {
addServerState.value =
LoadingState.Error(exception = ex)
}
}
}
}
fun removeUser(user: JellyfinUser) {
viewModelScope.launchIO {
serverRepository.removeUser(user)
@ -318,35 +226,6 @@ class SwitchUserViewModel
}
}
fun removeServer(server: JellyfinServer) {
viewModelScope.launchIO {
serverRepository.removeServer(server)
init()
}
}
fun discoverServers() {
viewModelScope.launchIO {
jellyfin.discovery.discoverLocalServers().collect { server ->
val newServerList =
discoveredServers.value!!
.toMutableList()
.apply {
add(
JellyfinServer(
server.id.toUUID(),
server.name,
server.address,
),
)
}
withContext(Dispatchers.Main) {
discoveredServers.value = newServerList
}
}
}
}
private suspend fun setError(
msg: String? = null,
ex: Exception? = null,