Fix some login issues (#23)

Fixes #21

Might also address #18

Fixes an issue with logging using username/password due to network
access on wrong thread exceptions.

Also adds better handling when deleting users or servers, especially if
it's the current one.
This commit is contained in:
damontecres 2025-10-17 12:05:40 -04:00 committed by GitHub
parent 0e195c18d7
commit bd4d4d5092
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 72 additions and 35 deletions

View file

@ -11,6 +11,7 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@ -118,6 +119,7 @@ class MainActivity : AppCompatActivity() {
) )
} }
} else { } else {
key(server, user) {
val initialDestination = val initialDestination =
if (server != null && user != null) { if (server != null && user != null) {
Destination.Home() Destination.Home()
@ -150,3 +152,4 @@ class MainActivity : AppCompatActivity() {
} }
} }
} }
}

View file

@ -57,6 +57,7 @@ class ServerRepository
apiClient.update(baseUrl = server.url, accessToken = null) apiClient.update(baseUrl = server.url, accessToken = null)
_currentServer = server _currentServer = server
_currentUser = null _currentUser = null
_currentUserDto = null
} }
/** /**
@ -160,6 +161,7 @@ class ServerRepository
suspend fun removeUser(user: JellyfinUser) { suspend fun removeUser(user: JellyfinUser) {
if (currentUser == user) { if (currentUser == user) {
_currentUser = null _currentUser = null
_currentUserDto = null
userPreferencesDataStore.updateData { userPreferencesDataStore.updateData {
it it
.toBuilder() .toBuilder()
@ -177,13 +179,17 @@ class ServerRepository
suspend fun removeServer(server: JellyfinServer) { suspend fun removeServer(server: JellyfinServer) {
if (currentServer == server) { if (currentServer == server) {
_currentServer = null _currentServer = null
_currentUser = null
_currentUserDto = null
userPreferencesDataStore.updateData { userPreferencesDataStore.updateData {
it it
.toBuilder() .toBuilder()
.apply { .apply {
currentServerId = "" currentServerId = ""
currentUserId = ""
}.build() }.build()
} }
apiClient.update(baseUrl = null, accessToken = null)
} }
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
serverDao.deleteServer(server.id) serverDao.deleteServer(server.id)

View file

@ -30,8 +30,12 @@ import androidx.media3.common.Player
import coil3.request.ErrorResult import coil3.request.ErrorResult
import com.github.damontecres.wholphin.ui.data.RowColumn import com.github.damontecres.wholphin.ui.data.RowColumn
import com.github.damontecres.wholphin.ui.data.RowColumnSaver import com.github.damontecres.wholphin.ui.data.RowColumnSaver
import com.github.damontecres.wholphin.util.ExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.extensions.ticks import org.jellyfin.sdk.model.extensions.ticks
@ -39,6 +43,7 @@ import timber.log.Timber
import kotlin.contracts.ExperimentalContracts import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind import kotlin.contracts.InvocationKind
import kotlin.contracts.contract import kotlin.contracts.contract
import kotlin.coroutines.CoroutineContext
import kotlin.math.min import kotlin.math.min
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@ -337,3 +342,12 @@ suspend fun showToast(
) = withContext(Dispatchers.Main) { ) = withContext(Dispatchers.Main) {
Toast.makeText(context, text, Toast.LENGTH_LONG).show() Toast.makeText(context, text, Toast.LENGTH_LONG).show()
} }
/**
* Launches a coroutine with [Dispatchers.IO] plus the provided [CoroutineContext] defaulting to using [ExceptionHandler]
*/
fun CoroutineScope.launchIO(
context: CoroutineContext = ExceptionHandler(),
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit,
): Job = launch(context = Dispatchers.IO + context, start = start, block = block)

View file

@ -7,15 +7,14 @@ import com.github.damontecres.wholphin.data.JellyfinServer
import com.github.damontecres.wholphin.data.JellyfinServerDao import com.github.damontecres.wholphin.data.JellyfinServerDao
import com.github.damontecres.wholphin.data.JellyfinUser import com.github.damontecres.wholphin.data.JellyfinUser
import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.ui.nav.NavigationManager
import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jellyfin.sdk.Jellyfin import org.jellyfin.sdk.Jellyfin
import org.jellyfin.sdk.api.client.HttpClientOptions import org.jellyfin.sdk.api.client.HttpClientOptions
@ -62,7 +61,11 @@ class SwitchUserViewModel
} }
init { init {
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { init()
}
fun init() {
viewModelScope.launchIO {
val allServers = val allServers =
serverDao serverDao
.getServers() .getServers()
@ -116,7 +119,7 @@ class SwitchUserViewModel
} }
} }
} }
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { viewModelScope.launchIO {
serverRepository.currentServer?.let { serverRepository.currentServer?.let {
val quickConnect = val quickConnect =
jellyfin jellyfin
@ -140,11 +143,14 @@ class SwitchUserViewModel
server: JellyfinServer, server: JellyfinServer,
user: JellyfinUser, user: JellyfinUser,
) { ) {
viewModelScope.launch(ExceptionHandler()) { viewModelScope.launchIO {
try { try {
serverRepository.changeUser(server, user) serverRepository.changeUser(server, user)
withContext(Dispatchers.Main) {
navigationManager.goToHome() navigationManager.goToHome()
}
} catch (ex: Exception) { } catch (ex: Exception) {
Timber.e(ex, "Error switching user")
switchUserState.value = LoadingState.Error(exception = ex) switchUserState.value = LoadingState.Error(exception = ex)
} }
} }
@ -156,7 +162,7 @@ class SwitchUserViewModel
password: String, password: String,
) { ) {
quickConnectJob?.cancel() quickConnectJob?.cancel()
viewModelScope.launch(ExceptionHandler()) { viewModelScope.launchIO {
try { try {
val api = jellyfin.createApi(baseUrl = server.url) val api = jellyfin.createApi(baseUrl = server.url)
val authenticationResult by api.userApi.authenticateUserByName( val authenticationResult by api.userApi.authenticateUserByName(
@ -168,6 +174,7 @@ class SwitchUserViewModel
navigationManager.goToHome() navigationManager.goToHome()
} }
} catch (ex: Exception) { } catch (ex: Exception) {
Timber.e(ex, "Error logging in user")
switchUserState.value = LoadingState.Error(ex) switchUserState.value = LoadingState.Error(ex)
} }
} }
@ -178,7 +185,7 @@ class SwitchUserViewModel
onAuthenticated: () -> Unit, onAuthenticated: () -> Unit,
) { ) {
quickConnectJob?.cancel() quickConnectJob?.cancel()
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { viewModelScope.launchIO {
try { try {
val api = jellyfin.createApi(server.url) val api = jellyfin.createApi(server.url)
var state = var state =
@ -191,7 +198,7 @@ class SwitchUserViewModel
} }
quickConnectJob = quickConnectJob =
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { viewModelScope.launchIO {
while (!state.authenticated) { while (!state.authenticated) {
delay(5_000L) delay(5_000L)
state = state =
@ -212,6 +219,7 @@ class SwitchUserViewModel
} }
} }
} catch (ex: Exception) { } catch (ex: Exception) {
Timber.e(ex, "Error during quick connect")
if (ex is InvalidStatusException && ex.status == 401) { if (ex is InvalidStatusException && ex.status == 401) {
quickConnectState.value = null quickConnectState.value = null
serverQuickConnect.value = serverQuickConnect.value =
@ -231,7 +239,7 @@ class SwitchUserViewModel
fun addServer(serverUrl: String) { fun addServer(serverUrl: String) {
addServerState.value = LoadingState.Loading addServerState.value = LoadingState.Loading
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { viewModelScope.launchIO {
try { try {
val serverInfo by jellyfin val serverInfo by jellyfin
.createApi(serverUrl) .createApi(serverUrl)
@ -279,19 +287,25 @@ class SwitchUserViewModel
} }
fun removeUser(user: JellyfinUser) { fun removeUser(user: JellyfinUser) {
viewModelScope.launch(ExceptionHandler()) { viewModelScope.launchIO {
serverRepository.removeUser(user) serverRepository.removeUser(user)
val serverUsers =
serverDao.getServer(user.serverId)?.users?.sortedBy { it.name } ?: listOf()
withContext(Dispatchers.Main) {
users.value = serverUsers
}
} }
} }
fun removeServer(server: JellyfinServer) { fun removeServer(server: JellyfinServer) {
viewModelScope.launch(ExceptionHandler()) { viewModelScope.launchIO {
serverRepository.removeServer(server) serverRepository.removeServer(server)
init()
} }
} }
fun discoverServers() { fun discoverServers() {
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { viewModelScope.launchIO {
jellyfin.discovery.discoverLocalServers().collect { server -> jellyfin.discovery.discoverLocalServers().collect { server ->
val newServerList = val newServerList =
discoveredServers.value!! discoveredServers.value!!