From 2161a9de5679d00fc8222faf69b06345072c2668 Mon Sep 17 00:00:00 2001 From: Damontecres Date: Sat, 20 Dec 2025 20:32:46 -0500 Subject: [PATCH] WIP login workflow --- .../wholphin/data/model/SeerrServer.kt | 6 +- .../damontecres/wholphin/services/SeerrApi.kt | 2 +- .../services/SeerrServerRepository.kt | 132 +++++++++++++++++ .../wholphin/ui/setup/seerr/AddSeerrServer.kt | 137 ++++++++++++++++++ .../ui/setup/seerr/SwitchSeerrViewModel.kt | 21 +++ 5 files changed, 294 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/wholphin/services/SeerrServerRepository.kt create mode 100644 app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/AddSeerrServer.kt create mode 100644 app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/SwitchSeerrViewModel.kt diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/SeerrServer.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/SeerrServer.kt index b5588457..0eb79efd 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/SeerrServer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/SeerrServer.kt @@ -20,10 +20,10 @@ import org.jellyfin.sdk.model.serializer.UUIDSerializer @Serializable data class SeerrServer( @PrimaryKey(autoGenerate = true) - val id: Int, - val name: String?, + val id: Int = 0, val url: String, - val version: String?, + val name: String? = null, + val version: String? = null, ) @Entity( diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrApi.kt b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrApi.kt index b3a3dcef..2e7cbd30 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrApi.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrApi.kt @@ -26,7 +26,7 @@ class SeerrApi( fun update( baseUrl: String, - apiKey: String, + apiKey: String?, ) { api = SeerrApiClient(baseUrl, apiKey, okHttpClient) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrServerRepository.kt b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrServerRepository.kt new file mode 100644 index 00000000..8103cbfc --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrServerRepository.kt @@ -0,0 +1,132 @@ +package com.github.damontecres.wholphin.services + +import com.github.damontecres.wholphin.api.seerr.model.AuthJellyfinPostRequest +import com.github.damontecres.wholphin.api.seerr.model.AuthLocalPostRequest +import com.github.damontecres.wholphin.data.SeerrServerDao +import com.github.damontecres.wholphin.data.ServerRepository +import com.github.damontecres.wholphin.data.model.SeerrAuthMethod +import com.github.damontecres.wholphin.data.model.SeerrServer +import com.github.damontecres.wholphin.data.model.SeerrUser +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.update +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class SeerrServerRepository + @Inject + constructor( + private val seerrApi: SeerrApi, + private val seerrServerDao: SeerrServerDao, + private val serverRepository: ServerRepository, + ) { + private val _currentServer = MutableStateFlow(null) + val currentServer: StateFlow = _currentServer + + private val _currentUser = MutableStateFlow(null) + val currentUser: StateFlow = _currentUser + + val current = + currentServer.combine(currentUser) { server, user -> + if (server != null && user != null) { + CurrentSeerr(server, user) + } else { + null + } + } + + suspend fun addAndChangeServer( + url: String, + apiKey: String, + ) { + var server = seerrServerDao.getServer(url) + if (server == null) { + seerrServerDao.addServer(SeerrServer(url = url)) + server = seerrServerDao.getServer(url) + } + server?.server?.let { server -> + serverRepository.currentUser.value?.let { jellyfinUser -> + // TODO test api key + val user = + SeerrUser( + jellyfinRowId = jellyfinUser.rowId, + serverId = server.id, + authMethod = SeerrAuthMethod.API_KEY, + username = null, + password = null, + credential = apiKey, + ) + seerrServerDao.addUser(user) + // TODO user info + seerrApi.api.usersApi.authMeGet() + + seerrApi.update(server.url, apiKey) + _currentServer.update { + _currentUser.update { + user + } + server + } + } + } + } + + suspend fun addAndChangeServer( + url: String, + authMethod: SeerrAuthMethod, + username: String, + password: String, + ) { + var server = seerrServerDao.getServer(url) + if (server == null) { + seerrServerDao.addServer(SeerrServer(url = url)) + server = seerrServerDao.getServer(url) + } + server?.server?.let { server -> + serverRepository.currentUser.value?.let { jellyfinUser -> + val response = + when (authMethod) { + SeerrAuthMethod.LOCAL -> { + seerrApi.api.authApi.authLocalPostWithHttpInfo( + AuthLocalPostRequest( + email = username, + password = password, + ), + ) + } + + SeerrAuthMethod.JELLYFIN -> { + seerrApi.api.authApi.authJellyfinPostWithHttpInfo( + AuthJellyfinPostRequest( + username = username, + password = password, + ), + ) + } + + SeerrAuthMethod.API_KEY -> { + throw IllegalArgumentException("Cannot authenticate with API key using this function") + } + } + response.headers["Cookies"] + // TODO get session cookie + val user = + SeerrUser( + jellyfinRowId = jellyfinUser.rowId, + serverId = server.id, + authMethod = SeerrAuthMethod.API_KEY, + username = username, + password = password, + credential = null, + ) + } + } + } + } + +data class CurrentSeerr( + val server: SeerrServer, + val user: SeerrUser, +) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/AddSeerrServer.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/AddSeerrServer.kt new file mode 100644 index 00000000..1f632a1f --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/AddSeerrServer.kt @@ -0,0 +1,137 @@ +package com.github.damontecres.wholphin.ui.setup.seerr + +import androidx.compose.foundation.focusGroup +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +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.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Text +import com.github.damontecres.wholphin.R +import com.github.damontecres.wholphin.ui.components.EditTextBox +import com.github.damontecres.wholphin.ui.components.TextButton +import com.github.damontecres.wholphin.ui.isNotNullOrBlank +import com.github.damontecres.wholphin.ui.tryRequestFocus + +@Composable +fun AddSeerrServerContent( + onSubmit: (url: String, apiKey: String) -> Unit, + errorMessage: String?, + modifier: Modifier = Modifier, +) { + var error by remember(errorMessage) { mutableStateOf(errorMessage) } + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = + modifier + .focusGroup() + .padding(16.dp) + .wrapContentSize(), + ) { + var url by remember { mutableStateOf("") } + var apiKey by remember { mutableStateOf("") } + + val focusRequester = remember { FocusRequester() } + val passwordFocusRequester = remember { FocusRequester() } + LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } + Text( + text = "Enter URL & API Key", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.align(Alignment.CenterHorizontally), + ) { + Text( + text = "URL", + modifier = Modifier.padding(end = 8.dp), + ) + EditTextBox( + value = url, + onValueChange = { + error = null + url = it + }, + keyboardOptions = + KeyboardOptions( + capitalization = KeyboardCapitalization.None, + autoCorrectEnabled = false, + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Next, + ), + keyboardActions = + KeyboardActions( + onNext = { + passwordFocusRequester.tryRequestFocus() + }, + ), + isInputValid = { true }, + modifier = Modifier.focusRequester(focusRequester), + ) + } + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.align(Alignment.CenterHorizontally), + ) { + Text( + text = "API Key", + modifier = Modifier.padding(end = 8.dp), + ) + EditTextBox( + value = apiKey, + onValueChange = { + error = null + apiKey = it + }, + keyboardOptions = + KeyboardOptions( + capitalization = KeyboardCapitalization.None, + autoCorrectEnabled = false, + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Go, + ), + keyboardActions = + KeyboardActions( + onGo = { onSubmit.invoke(url, apiKey) }, + ), + isInputValid = { true }, + modifier = Modifier.focusRequester(passwordFocusRequester), + ) + } + error?.let { + Text( + text = it, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.titleLarge, + ) + } + TextButton( + stringRes = R.string.submit, + onClick = { onSubmit.invoke(url, apiKey) }, + enabled = error.isNullOrBlank() && url.isNotNullOrBlank() && apiKey.isNotNullOrBlank(), + modifier = Modifier.align(Alignment.CenterHorizontally), + ) + } +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/SwitchSeerrViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/SwitchSeerrViewModel.kt new file mode 100644 index 00000000..b1f890e3 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/SwitchSeerrViewModel.kt @@ -0,0 +1,21 @@ +package com.github.damontecres.wholphin.ui.setup.seerr + +import androidx.lifecycle.ViewModel +import com.github.damontecres.wholphin.services.SeerrServerRepository +import com.github.damontecres.wholphin.services.SeerrService +import dagger.hilt.android.lifecycle.HiltViewModel +import jakarta.inject.Inject + +@HiltViewModel +class SwitchSeerrViewModel + @Inject + constructor( + private val seerrServerRepository: SeerrServerRepository, + private val seerrService: SeerrService, + ) : ViewModel() { + fun submitServer( + url: String, + apiKey: String, + ) { + } + }