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.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@ -118,31 +119,33 @@ class MainActivity : AppCompatActivity() {
)
}
} else {
val initialDestination =
if (server != null && user != null) {
Destination.Home()
} else {
Destination.ServerList
}
val backStack = rememberNavBackStack(initialDestination)
navigationManager.backStack = backStack
if (appPreferences.autoCheckForUpdates) {
LaunchedEffect(Unit) {
try {
updateChecker.maybeShowUpdateToast(appPreferences.updateUrl)
} catch (ex: Exception) {
Timber.w(ex, "Failed to check for update")
key(server, user) {
val initialDestination =
if (server != null && user != null) {
Destination.Home()
} else {
Destination.ServerList
}
val backStack = rememberNavBackStack(initialDestination)
navigationManager.backStack = backStack
if (appPreferences.autoCheckForUpdates) {
LaunchedEffect(Unit) {
try {
updateChecker.maybeShowUpdateToast(appPreferences.updateUrl)
} catch (ex: Exception) {
Timber.w(ex, "Failed to check for update")
}
}
}
ApplicationContent(
user = user,
server = server,
navigationManager = navigationManager,
preferences = preferences,
deviceProfile = deviceProfile,
modifier = Modifier.fillMaxSize(),
)
}
ApplicationContent(
user = user,
server = server,
navigationManager = navigationManager,
preferences = preferences,
deviceProfile = deviceProfile,
modifier = Modifier.fillMaxSize(),
)
}
}
}

View file

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

View file

@ -30,8 +30,12 @@ import androidx.media3.common.Player
import coil3.request.ErrorResult
import com.github.damontecres.wholphin.ui.data.RowColumn
import com.github.damontecres.wholphin.ui.data.RowColumnSaver
import com.github.damontecres.wholphin.util.ExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.extensions.ticks
@ -39,6 +43,7 @@ import timber.log.Timber
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.coroutines.CoroutineContext
import kotlin.math.min
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
@ -337,3 +342,12 @@ suspend fun showToast(
) = withContext(Dispatchers.Main) {
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.JellyfinUser
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.NavigationManager
import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState
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
@ -62,7 +61,11 @@ class SwitchUserViewModel
}
init {
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
init()
}
fun init() {
viewModelScope.launchIO {
val allServers =
serverDao
.getServers()
@ -116,7 +119,7 @@ class SwitchUserViewModel
}
}
}
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
viewModelScope.launchIO {
serverRepository.currentServer?.let {
val quickConnect =
jellyfin
@ -140,11 +143,14 @@ class SwitchUserViewModel
server: JellyfinServer,
user: JellyfinUser,
) {
viewModelScope.launch(ExceptionHandler()) {
viewModelScope.launchIO {
try {
serverRepository.changeUser(server, user)
navigationManager.goToHome()
withContext(Dispatchers.Main) {
navigationManager.goToHome()
}
} catch (ex: Exception) {
Timber.e(ex, "Error switching user")
switchUserState.value = LoadingState.Error(exception = ex)
}
}
@ -156,7 +162,7 @@ class SwitchUserViewModel
password: String,
) {
quickConnectJob?.cancel()
viewModelScope.launch(ExceptionHandler()) {
viewModelScope.launchIO {
try {
val api = jellyfin.createApi(baseUrl = server.url)
val authenticationResult by api.userApi.authenticateUserByName(
@ -168,6 +174,7 @@ class SwitchUserViewModel
navigationManager.goToHome()
}
} catch (ex: Exception) {
Timber.e(ex, "Error logging in user")
switchUserState.value = LoadingState.Error(ex)
}
}
@ -178,7 +185,7 @@ class SwitchUserViewModel
onAuthenticated: () -> Unit,
) {
quickConnectJob?.cancel()
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
viewModelScope.launchIO {
try {
val api = jellyfin.createApi(server.url)
var state =
@ -191,7 +198,7 @@ class SwitchUserViewModel
}
quickConnectJob =
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
viewModelScope.launchIO {
while (!state.authenticated) {
delay(5_000L)
state =
@ -212,6 +219,7 @@ class SwitchUserViewModel
}
}
} catch (ex: Exception) {
Timber.e(ex, "Error during quick connect")
if (ex is InvalidStatusException && ex.status == 401) {
quickConnectState.value = null
serverQuickConnect.value =
@ -231,7 +239,7 @@ class SwitchUserViewModel
fun addServer(serverUrl: String) {
addServerState.value = LoadingState.Loading
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
viewModelScope.launchIO {
try {
val serverInfo by jellyfin
.createApi(serverUrl)
@ -279,19 +287,25 @@ class SwitchUserViewModel
}
fun removeUser(user: JellyfinUser) {
viewModelScope.launch(ExceptionHandler()) {
viewModelScope.launchIO {
serverRepository.removeUser(user)
val serverUsers =
serverDao.getServer(user.serverId)?.users?.sortedBy { it.name } ?: listOf()
withContext(Dispatchers.Main) {
users.value = serverUsers
}
}
}
fun removeServer(server: JellyfinServer) {
viewModelScope.launch(ExceptionHandler()) {
viewModelScope.launchIO {
serverRepository.removeServer(server)
init()
}
}
fun discoverServers() {
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
viewModelScope.launchIO {
jellyfin.discovery.discoverLocalServers().collect { server ->
val newServerList =
discoveredServers.value!!