Auto-configure Discover from Wholphin server plugin (#2134)
## Description https://github.com/damontecres/jellyfin-plugin-wholphin/pull/3 added a new endpoint for Seerr configuration in Wholphin server plugin. This PR adds support for reading that configuration and either pre-filling the URL or automatically logging in. Some code was copied from #1418 (thanks @matschi95!), but that PR worked by inserting a DB record for the plugin provided server. Since the local DB was preferred, this meant plugin config changes wouldn't be reflected in the client. This PR instead attempts the connection and only updates the in-memory connection state, so user switching and app restarts will re-fetch from the plugin. If the server plugin is not installed or no Seerr server is configured, nothing in Wholphin changes. ### Related issues #598 This PR replaces #1418 ### Testing Emulator with different scenarios ## Screenshots N/A ## AI or LLM usage None
This commit is contained in:
parent
90a2d6d459
commit
f66746cf47
11 changed files with 454 additions and 160 deletions
|
|
@ -0,0 +1,36 @@
|
||||||
|
package com.github.damontecres.wholphin.data.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed interface SeerrPluginLogin {
|
||||||
|
@SerialName("None")
|
||||||
|
@Serializable
|
||||||
|
data object None : SeerrPluginLogin
|
||||||
|
|
||||||
|
@SerialName("ApiKey")
|
||||||
|
@Serializable
|
||||||
|
data class ApiKey(
|
||||||
|
val apiKey: String,
|
||||||
|
) : SeerrPluginLogin
|
||||||
|
|
||||||
|
@SerialName("Local")
|
||||||
|
@Serializable
|
||||||
|
data class Local(
|
||||||
|
val local: SeerrPluginLocalLogin,
|
||||||
|
) : SeerrPluginLogin
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SeerrPluginLocalLogin(
|
||||||
|
val username: String,
|
||||||
|
val password: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SeerrPluginSettings(
|
||||||
|
val version: Int = 1,
|
||||||
|
val serverUrl: String? = null,
|
||||||
|
val login: SeerrPluginLogin? = null,
|
||||||
|
)
|
||||||
|
|
@ -9,20 +9,24 @@ import com.github.damontecres.wholphin.data.SeerrServerDao
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.SeerrAuthMethod
|
import com.github.damontecres.wholphin.data.model.SeerrAuthMethod
|
||||||
import com.github.damontecres.wholphin.data.model.SeerrPermission
|
import com.github.damontecres.wholphin.data.model.SeerrPermission
|
||||||
|
import com.github.damontecres.wholphin.data.model.SeerrPluginLogin
|
||||||
|
import com.github.damontecres.wholphin.data.model.SeerrPluginSettings
|
||||||
import com.github.damontecres.wholphin.data.model.SeerrServer
|
import com.github.damontecres.wholphin.data.model.SeerrServer
|
||||||
import com.github.damontecres.wholphin.data.model.SeerrUser
|
import com.github.damontecres.wholphin.data.model.SeerrUser
|
||||||
import com.github.damontecres.wholphin.data.model.hasPermission
|
import com.github.damontecres.wholphin.data.model.hasPermission
|
||||||
import com.github.damontecres.wholphin.services.hilt.StandardOkHttpClient
|
import com.github.damontecres.wholphin.services.hilt.StandardOkHttpClient
|
||||||
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
import com.github.damontecres.wholphin.ui.setup.seerr.createSeerrApiUrl
|
import com.github.damontecres.wholphin.ui.setup.seerr.createSeerrApiUrl
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
@ -39,26 +43,25 @@ class SeerrServerRepository
|
||||||
private val serverRepository: ServerRepository,
|
private val serverRepository: ServerRepository,
|
||||||
@param:StandardOkHttpClient private val okHttpClient: OkHttpClient,
|
@param:StandardOkHttpClient private val okHttpClient: OkHttpClient,
|
||||||
) {
|
) {
|
||||||
private val _connection =
|
private val _connection = MutableStateFlow<SeerrState>(SeerrState())
|
||||||
MutableStateFlow<SeerrConnectionStatus>(SeerrConnectionStatus.NotConfigured)
|
val connection: StateFlow<SeerrState> = _connection
|
||||||
val connection: StateFlow<SeerrConnectionStatus> = _connection
|
|
||||||
|
|
||||||
val current: Flow<CurrentSeerr?> =
|
val current: Flow<CurrentSeerr?> =
|
||||||
_connection.map { (it as? SeerrConnectionStatus.Success)?.current }
|
_connection.map { (it.connection as? SeerrConnectionStatus.Success)?.current }
|
||||||
val currentServer: Flow<SeerrServer?> =
|
val currentServer: Flow<SeerrServer?> =
|
||||||
connection.map { (it as? SeerrConnectionStatus.Success)?.current?.server }
|
connection.map { (it.connection as? SeerrConnectionStatus.Success)?.current?.server }
|
||||||
val currentUser: Flow<SeerrUser?> =
|
val currentUser: Flow<SeerrUser?> =
|
||||||
connection.map { (it as? SeerrConnectionStatus.Success)?.current?.user }
|
connection.map { (it.connection as? SeerrConnectionStatus.Success)?.current?.user }
|
||||||
val currentUserId: Flow<Int?> = current.map { it?.config?.id }
|
val currentUserId: Flow<Int?> = current.map { it?.config?.id }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether Seerr integration is currently active of not
|
* Whether Seerr integration is currently active of not
|
||||||
*/
|
*/
|
||||||
val active: Flow<Boolean> =
|
val active: Flow<Boolean> =
|
||||||
connection.map { it is SeerrConnectionStatus.Success && seerrApi.active }
|
connection.map { it.connection is SeerrConnectionStatus.Success && seerrApi.active }
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
_connection.update { SeerrConnectionStatus.NotConfigured }
|
_connection.update { SeerrState() }
|
||||||
seerrApi.update("", null)
|
seerrApi.update("", null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,7 +70,7 @@ class SeerrServerRepository
|
||||||
user: SeerrUser,
|
user: SeerrUser,
|
||||||
exception: Exception,
|
exception: Exception,
|
||||||
) {
|
) {
|
||||||
_connection.update { SeerrConnectionStatus.Error(server, user, exception) }
|
_connection.update { it.copy(connection = SeerrConnectionStatus.Error(server, user, exception)) }
|
||||||
seerrApi.update("", null)
|
seerrApi.update("", null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,8 +81,9 @@ class SeerrServerRepository
|
||||||
) {
|
) {
|
||||||
val publicSettings = seerrApi.api.settingsApi.settingsPublicGet()
|
val publicSettings = seerrApi.api.settingsApi.settingsPublicGet()
|
||||||
_connection.update {
|
_connection.update {
|
||||||
SeerrConnectionStatus.Success(
|
it.copy(
|
||||||
CurrentSeerr(server, user, userConfig, publicSettings),
|
connection =
|
||||||
|
SeerrConnectionStatus.Success(CurrentSeerr(server, user, userConfig, publicSettings)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -169,7 +173,7 @@ class SeerrServerRepository
|
||||||
|
|
||||||
suspend fun removeServerForCurrentUser(): Boolean {
|
suspend fun removeServerForCurrentUser(): Boolean {
|
||||||
val user =
|
val user =
|
||||||
when (val conn = connection.first()) {
|
when (val conn = connection.value.connection) {
|
||||||
SeerrConnectionStatus.NotConfigured -> return false
|
SeerrConnectionStatus.NotConfigured -> return false
|
||||||
is SeerrConnectionStatus.Error -> conn.user
|
is SeerrConnectionStatus.Error -> conn.user
|
||||||
is SeerrConnectionStatus.Success -> conn.current.user
|
is SeerrConnectionStatus.Success -> conn.current.user
|
||||||
|
|
@ -178,6 +182,81 @@ class SeerrServerRepository
|
||||||
clear()
|
clear()
|
||||||
return rows > 0
|
return rows > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given the [SeerrPluginSettings], try to connect to the Seerr server
|
||||||
|
*
|
||||||
|
* @return true if successfully updated the SeerrApi and logged in
|
||||||
|
*/
|
||||||
|
suspend fun usePluginProvidedServer(settings: SeerrPluginSettings): Boolean {
|
||||||
|
val currentUser = serverRepository.currentUser ?: return false
|
||||||
|
if (settings.serverUrl.isNotNullOrBlank() && settings.login != null && settings.login != SeerrPluginLogin.None) {
|
||||||
|
val server =
|
||||||
|
SeerrServer(
|
||||||
|
id = -1,
|
||||||
|
url = settings.serverUrl,
|
||||||
|
)
|
||||||
|
val user =
|
||||||
|
SeerrUser(
|
||||||
|
jellyfinUserRowId = currentUser.rowId,
|
||||||
|
serverId = server.id,
|
||||||
|
authMethod =
|
||||||
|
when (settings.login) {
|
||||||
|
is SeerrPluginLogin.ApiKey -> SeerrAuthMethod.API_KEY
|
||||||
|
is SeerrPluginLogin.Local -> SeerrAuthMethod.LOCAL
|
||||||
|
},
|
||||||
|
username =
|
||||||
|
when (settings.login) {
|
||||||
|
is SeerrPluginLogin.ApiKey -> null
|
||||||
|
is SeerrPluginLogin.Local -> settings.login.local.username
|
||||||
|
},
|
||||||
|
password = null,
|
||||||
|
credential = null,
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
val userConfig =
|
||||||
|
when (settings.login) {
|
||||||
|
is SeerrPluginLogin.ApiKey -> {
|
||||||
|
Timber.v("maybeAddPluginServer, using API key")
|
||||||
|
seerrApi.update(settings.serverUrl, settings.login.apiKey)
|
||||||
|
seerrLogin(seerrApi.api, SeerrAuthMethod.API_KEY, null, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
is SeerrPluginLogin.Local -> {
|
||||||
|
Timber.v("maybeAddPluginServer, using local user")
|
||||||
|
seerrApi.update(settings.serverUrl, null)
|
||||||
|
seerrLogin(
|
||||||
|
seerrApi.api,
|
||||||
|
SeerrAuthMethod.LOCAL,
|
||||||
|
settings.login.local.username,
|
||||||
|
settings.login.local.password,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
SeerrPluginLogin.None -> {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Success
|
||||||
|
_connection.update { it.copy(pluginProvided = true) }
|
||||||
|
set(server, user, userConfig)
|
||||||
|
return true
|
||||||
|
} catch (ex: CancellationException) {
|
||||||
|
throw ex
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
clear()
|
||||||
|
Timber.w(ex, "Error adding plugin provided seerr")
|
||||||
|
_connection.update {
|
||||||
|
it.copy(
|
||||||
|
pluginProvided = true,
|
||||||
|
connection = SeerrConnectionStatus.Error(server, user, ex),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -185,6 +264,11 @@ class SeerrServerRepository
|
||||||
*/
|
*/
|
||||||
typealias SeerrUserConfig = User
|
typealias SeerrUserConfig = User
|
||||||
|
|
||||||
|
data class SeerrState(
|
||||||
|
val connection: SeerrConnectionStatus = SeerrConnectionStatus.NotConfigured,
|
||||||
|
val pluginProvided: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
sealed interface SeerrConnectionStatus {
|
sealed interface SeerrConnectionStatus {
|
||||||
data object NotConfigured : SeerrConnectionStatus
|
data object NotConfigured : SeerrConnectionStatus
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
package com.github.damontecres.wholphin.services
|
package com.github.damontecres.wholphin.services
|
||||||
|
|
||||||
import com.github.damontecres.wholphin.data.model.HomePageSettings
|
import com.github.damontecres.wholphin.data.model.HomePageSettings
|
||||||
|
import com.github.damontecres.wholphin.data.model.SeerrPluginSettings
|
||||||
import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient
|
import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient
|
||||||
import com.github.damontecres.wholphin.util.WholphinDispatchers
|
import com.github.damontecres.wholphin.util.WholphinDispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.json.decodeFromStream
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okio.IOException
|
import okio.IOException
|
||||||
|
|
@ -32,6 +34,7 @@ class ServerPluginApi
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val HOME_CONFIG_PATH = "homesettings"
|
private const val HOME_CONFIG_PATH = "homesettings"
|
||||||
|
private const val SEERR_CONFIG_PATH = "seerrsettings"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -57,32 +60,37 @@ class ServerPluginApi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private suspend inline fun <reified T> executeRequest(path: String): T? {
|
||||||
* Get the default home page settings from the plugin
|
val url = createUrl(path) ?: return null
|
||||||
*
|
val request = Request(url.toHttpUrl())
|
||||||
* @return the home page settings or null if no rows are configured or the server isn't installed
|
return executeRequest<T>(request)
|
||||||
*/
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
suspend fun fetchHomePageSettings(): HomePageSettings? =
|
private suspend inline fun <reified T> executeRequest(request: Request): T? =
|
||||||
withContext(WholphinDispatchers.IO) {
|
withContext(WholphinDispatchers.IO) {
|
||||||
val url = createUrl(HOME_CONFIG_PATH) ?: return@withContext null
|
okHttpClient.newCall(request).execute().use { res ->
|
||||||
val request =
|
|
||||||
Request
|
|
||||||
.Builder()
|
|
||||||
.url(url)
|
|
||||||
.get()
|
|
||||||
.build()
|
|
||||||
return@withContext okHttpClient.newCall(request).execute().use { res ->
|
|
||||||
if (res.isSuccessful) {
|
if (res.isSuccessful) {
|
||||||
json
|
json.decodeFromStream<T>(res.body.byteStream())
|
||||||
.decodeFromStream<HomePageSettings>(res.body.byteStream())
|
|
||||||
.takeIf { it.rows.isNotEmpty() }
|
|
||||||
} else if (res.code == 404) {
|
} else if (res.code == 404) {
|
||||||
Timber.w("fetchHomePageSettings returned 404")
|
Timber.w("Plugin 404: %s", request.url.encodedPath)
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
throw ApiClientException(res.code.toString() + " " + res.body.string())
|
throw ApiClientException(res.code.toString() + " " + res.body.string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default home page settings from the plugin
|
||||||
|
*
|
||||||
|
* @return the home page settings or null if no rows are configured or the server isn't installed
|
||||||
|
*/
|
||||||
|
suspend fun fetchHomePageSettings(): HomePageSettings? =
|
||||||
|
executeRequest<HomePageSettings>(HOME_CONFIG_PATH)?.takeIf { it.rows.isNotEmpty() }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the [SeerrPluginSettings] as configured by teh server plugin, or null if none are configured
|
||||||
|
*/
|
||||||
|
suspend fun fetchSeerrSettings(): SeerrPluginSettings? = executeRequest(SEERR_CONFIG_PATH)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ class UserSwitchListener
|
||||||
AppCompatDelegate.setApplicationLocales(localeList)
|
AppCompatDelegate.setApplicationLocales(localeList)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if plugin is installed, then for home settings
|
// Check if plugin is installed, then for home settings & seerr settings
|
||||||
launchIO {
|
launchIO {
|
||||||
val serverPluginInstalled =
|
val serverPluginInstalled =
|
||||||
try {
|
try {
|
||||||
|
|
@ -81,48 +81,64 @@ class UserSwitchListener
|
||||||
Timber.i("Server plugin installed: %s", serverPluginInstalled)
|
Timber.i("Server plugin installed: %s", serverPluginInstalled)
|
||||||
serverRepository.serverPluginInstalled.value = serverPluginInstalled
|
serverRepository.serverPluginInstalled.value = serverPluginInstalled
|
||||||
|
|
||||||
// Check for home settings
|
|
||||||
homeSettingsService.loadCurrentSettings(user)
|
|
||||||
}
|
|
||||||
if (BuildConfig.DISCOVER_ENABLED) {
|
|
||||||
// Check for seerr server
|
|
||||||
launchIO {
|
launchIO {
|
||||||
seerrServerDao
|
// Check for home settings
|
||||||
.getUsersByJellyfinUser(user.rowId)
|
homeSettingsService.loadCurrentSettings(user)
|
||||||
.lastOrNull()
|
}
|
||||||
?.let { seerrUser ->
|
|
||||||
val server =
|
if (BuildConfig.DISCOVER_ENABLED) {
|
||||||
seerrServerDao.getServer(seerrUser.serverId)?.server
|
// Check for seerr server
|
||||||
if (server != null) {
|
launchIO {
|
||||||
Timber.i("Found a seerr user & server")
|
if (serverPluginInstalled) {
|
||||||
try {
|
serverPluginApi.fetchSeerrSettings()?.let {
|
||||||
seerrApi.update(server.url, seerrUser.credential)
|
Timber.d("Trying seerr auto login")
|
||||||
val userConfig =
|
val result = seerrServerRepository.usePluginProvidedServer(it)
|
||||||
if (seerrUser.authMethod != SeerrAuthMethod.API_KEY) {
|
if (result) {
|
||||||
seerrLogin(
|
// Successfully used server plugin provided seerr
|
||||||
seerrApi.api,
|
Timber.i("Seerr auto login succeeded")
|
||||||
seerrUser.authMethod,
|
return@launchIO
|
||||||
seerrUser.username,
|
|
||||||
seerrUser.password,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
seerrApi.api.usersApi.authMeGet()
|
|
||||||
}
|
|
||||||
seerrServerRepository.set(
|
|
||||||
server,
|
|
||||||
seerrUser,
|
|
||||||
userConfig,
|
|
||||||
)
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
Timber.w(
|
|
||||||
ex,
|
|
||||||
"Error logging into %s",
|
|
||||||
server.url,
|
|
||||||
)
|
|
||||||
seerrServerRepository.error(server, seerrUser, ex)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Timber.v("Checking for existing configured seerr server")
|
||||||
|
// Either no server plugin seerr or there was an issue with it
|
||||||
|
seerrServerDao
|
||||||
|
.getUsersByJellyfinUser(user.rowId)
|
||||||
|
.lastOrNull()
|
||||||
|
?.let { seerrUser ->
|
||||||
|
val server =
|
||||||
|
seerrServerDao.getServer(seerrUser.serverId)?.server
|
||||||
|
if (server != null) {
|
||||||
|
Timber.i("Found a seerr user & server")
|
||||||
|
try {
|
||||||
|
seerrApi.update(server.url, seerrUser.credential)
|
||||||
|
val userConfig =
|
||||||
|
if (seerrUser.authMethod != SeerrAuthMethod.API_KEY) {
|
||||||
|
seerrLogin(
|
||||||
|
seerrApi.api,
|
||||||
|
seerrUser.authMethod,
|
||||||
|
seerrUser.username,
|
||||||
|
seerrUser.password,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
seerrApi.api.usersApi.authMeGet()
|
||||||
|
}
|
||||||
|
seerrServerRepository.set(
|
||||||
|
server,
|
||||||
|
seerrUser,
|
||||||
|
userConfig,
|
||||||
|
)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Timber.w(
|
||||||
|
ex,
|
||||||
|
"Error logging into %s",
|
||||||
|
server.url,
|
||||||
|
)
|
||||||
|
seerrServerRepository.error(server, seerrUser, ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -450,12 +450,14 @@ fun ConfirmDialog(
|
||||||
properties: DialogProperties = DialogProperties(),
|
properties: DialogProperties = DialogProperties(),
|
||||||
elevation: Dp = 1.dp,
|
elevation: Dp = 1.dp,
|
||||||
bodyColor: Color = MaterialTheme.colorScheme.onSurface,
|
bodyColor: Color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
@StringRes cancelStringRes: Int = R.string.cancel,
|
||||||
|
@StringRes confirmStringRes: Int = R.string.confirm,
|
||||||
) = BasicDialog(
|
) = BasicDialog(
|
||||||
onDismissRequest = onCancel,
|
onDismissRequest = onCancel,
|
||||||
properties = properties,
|
properties = properties,
|
||||||
elevation = elevation,
|
elevation = elevation,
|
||||||
content = {
|
content = {
|
||||||
ConfirmDialogContent(title, body, onCancel, onConfirm, Modifier, bodyColor)
|
ConfirmDialogContent(title, body, onCancel, onConfirm, Modifier, bodyColor, cancelStringRes, confirmStringRes)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -470,6 +472,8 @@ fun ConfirmDialogContent(
|
||||||
onConfirm: () -> Unit,
|
onConfirm: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
bodyColor: Color = MaterialTheme.colorScheme.onSurface,
|
bodyColor: Color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
@StringRes cancelStringRes: Int = R.string.cancel,
|
||||||
|
@StringRes confirmStringRes: Int = R.string.confirm,
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
|
@ -500,11 +504,11 @@ fun ConfirmDialogContent(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
TextButton(
|
TextButton(
|
||||||
stringRes = R.string.cancel,
|
stringRes = cancelStringRes,
|
||||||
onClick = onCancel,
|
onClick = onCancel,
|
||||||
)
|
)
|
||||||
TextButton(
|
TextButton(
|
||||||
stringRes = R.string.confirm,
|
stringRes = confirmStringRes,
|
||||||
onClick = onConfirm,
|
onClick = onConfirm,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ enum class PreferenceScreenOption {
|
||||||
fun rememberSeerrDialogBody(
|
fun rememberSeerrDialogBody(
|
||||||
user: SeerrUser,
|
user: SeerrUser,
|
||||||
server: SeerrServer,
|
server: SeerrServer,
|
||||||
|
pluginProvided: Boolean,
|
||||||
ex: Exception? = null,
|
ex: Exception? = null,
|
||||||
): String {
|
): String {
|
||||||
val username =
|
val username =
|
||||||
|
|
@ -81,8 +82,13 @@ fun rememberSeerrDialogBody(
|
||||||
${stringResource(R.string.url)}: ${server.url}
|
${stringResource(R.string.url)}: ${server.url}
|
||||||
${stringResource(R.string.username)}: $username$method
|
${stringResource(R.string.username)}: $username$method
|
||||||
""".trimIndent() +
|
""".trimIndent() +
|
||||||
|
if (pluginProvided) {
|
||||||
|
"\n\n${stringResource(R.string.managed_by_server)}"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
} +
|
||||||
if (ex != null) {
|
if (ex != null) {
|
||||||
"\n\n${stringResource(R.string.voice_error_server)}: ${ex.localizedMessage}"
|
"\n\n${stringResource(R.string.error)}: ${ex.localizedMessage}"
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,6 @@ import androidx.tv.material3.surfaceColorAtElevation
|
||||||
import coil3.SingletonImageLoader
|
import coil3.SingletonImageLoader
|
||||||
import coil3.imageLoader
|
import coil3.imageLoader
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.SeerrServer
|
|
||||||
import com.github.damontecres.wholphin.data.model.SeerrUser
|
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.AppSwitchPreference
|
import com.github.damontecres.wholphin.preferences.AppSwitchPreference
|
||||||
|
|
@ -66,7 +64,6 @@ import com.github.damontecres.wholphin.preferences.experimentalPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.lazyListWrapScrolling
|
import com.github.damontecres.wholphin.preferences.lazyListWrapScrolling
|
||||||
import com.github.damontecres.wholphin.preferences.screensaverPreferences
|
import com.github.damontecres.wholphin.preferences.screensaverPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.updatePlaybackPreferences
|
import com.github.damontecres.wholphin.preferences.updatePlaybackPreferences
|
||||||
import com.github.damontecres.wholphin.services.CurrentSeerr
|
|
||||||
import com.github.damontecres.wholphin.services.Release
|
import com.github.damontecres.wholphin.services.Release
|
||||||
import com.github.damontecres.wholphin.services.SeerrConnectionStatus
|
import com.github.damontecres.wholphin.services.SeerrConnectionStatus
|
||||||
import com.github.damontecres.wholphin.services.UpdateChecker
|
import com.github.damontecres.wholphin.services.UpdateChecker
|
||||||
|
|
@ -83,7 +80,8 @@ import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings
|
||||||
import com.github.damontecres.wholphin.ui.serverSupportedMessage
|
import com.github.damontecres.wholphin.ui.serverSupportedMessage
|
||||||
import com.github.damontecres.wholphin.ui.setup.ReleaseNotes
|
import com.github.damontecres.wholphin.ui.setup.ReleaseNotes
|
||||||
import com.github.damontecres.wholphin.ui.setup.UpdateViewModel
|
import com.github.damontecres.wholphin.ui.setup.UpdateViewModel
|
||||||
import com.github.damontecres.wholphin.ui.setup.seerr.AddSeerServerDialog
|
import com.github.damontecres.wholphin.ui.setup.seerr.SeerrDialog
|
||||||
|
import com.github.damontecres.wholphin.ui.setup.seerr.SeerrDialogMode
|
||||||
import com.github.damontecres.wholphin.ui.setup.seerr.SwitchSeerrViewModel
|
import com.github.damontecres.wholphin.ui.setup.seerr.SwitchSeerrViewModel
|
||||||
import com.github.damontecres.wholphin.ui.showToast
|
import com.github.damontecres.wholphin.ui.showToast
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
|
@ -501,7 +499,7 @@ fun PreferencesContent(
|
||||||
title = stringResource(pref.title),
|
title = stringResource(pref.title),
|
||||||
onClick = {
|
onClick = {
|
||||||
seerrDialogMode =
|
seerrDialogMode =
|
||||||
when (val conn = seerrConnection) {
|
when (val conn = seerrConnection.connection) {
|
||||||
is SeerrConnectionStatus.Error -> {
|
is SeerrConnectionStatus.Error -> {
|
||||||
SeerrDialogMode.Error(
|
SeerrDialogMode.Error(
|
||||||
conn.server,
|
conn.server,
|
||||||
|
|
@ -523,15 +521,13 @@ fun PreferencesContent(
|
||||||
},
|
},
|
||||||
modifier = focusModifier,
|
modifier = focusModifier,
|
||||||
summary =
|
summary =
|
||||||
when (seerrConnection) {
|
when (seerrConnection.connection) {
|
||||||
is SeerrConnectionStatus.Error -> {
|
is SeerrConnectionStatus.Error -> {
|
||||||
stringResource(R.string.voice_error_server)
|
stringResource(R.string.voice_error_server)
|
||||||
}
|
}
|
||||||
|
|
||||||
SeerrConnectionStatus.NotConfigured -> {
|
SeerrConnectionStatus.NotConfigured -> {
|
||||||
stringResource(
|
stringResource(R.string.add_server)
|
||||||
R.string.add_server,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is SeerrConnectionStatus.Success -> {
|
is SeerrConnectionStatus.Success -> {
|
||||||
|
|
@ -726,59 +722,15 @@ fun PreferencesContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
when (val mode = seerrDialogMode) {
|
|
||||||
is SeerrDialogMode.Remove -> {
|
|
||||||
val body = rememberSeerrDialogBody(mode.current.user, mode.current.server)
|
|
||||||
ConfirmDialog(
|
|
||||||
title = stringResource(R.string.remove_seerr_server),
|
|
||||||
body = body,
|
|
||||||
onCancel = { seerrDialogMode = SeerrDialogMode.None },
|
|
||||||
onConfirm = {
|
|
||||||
seerrVm.removeServer()
|
|
||||||
seerrDialogMode = SeerrDialogMode.None
|
|
||||||
},
|
|
||||||
elevation = 3.dp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
SeerrDialogMode.Add -> {
|
|
||||||
val currentUser by seerrVm.currentUser.collectAsState(null)
|
|
||||||
val status by seerrVm.serverConnectionStatus.collectAsState(LoadingState.Pending)
|
|
||||||
val serverAddedMessage = stringResource(R.string.seerr_server_added)
|
|
||||||
LaunchedEffect(status) {
|
|
||||||
if (status == LoadingState.Success) {
|
|
||||||
Toast.makeText(context, serverAddedMessage, Toast.LENGTH_SHORT).show()
|
|
||||||
seerrDialogMode = SeerrDialogMode.None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AddSeerServerDialog(
|
|
||||||
currentUsername = currentUser?.name,
|
|
||||||
status = status,
|
|
||||||
onSubmit = seerrVm::submitServer,
|
|
||||||
onResetStatus = seerrVm::resetStatus,
|
|
||||||
onDismissRequest = { seerrDialogMode = SeerrDialogMode.None },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is SeerrDialogMode.Error -> {
|
|
||||||
val body = rememberSeerrDialogBody(mode.user, mode.server, mode.ex)
|
|
||||||
ConfirmDialog(
|
|
||||||
title = stringResource(R.string.remove_seerr_server),
|
|
||||||
body = body,
|
|
||||||
onCancel = { seerrDialogMode = SeerrDialogMode.None },
|
|
||||||
onConfirm = {
|
|
||||||
seerrVm.removeServer()
|
|
||||||
seerrDialogMode = SeerrDialogMode.None
|
|
||||||
},
|
|
||||||
bodyColor = MaterialTheme.colorScheme.error,
|
|
||||||
elevation = 3.dp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
SeerrDialogMode.None -> {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SeerrDialog(
|
||||||
|
seerrDialogMode = seerrDialogMode,
|
||||||
|
seerrConnection = seerrConnection,
|
||||||
|
onDismissRequest = { seerrDialogMode = SeerrDialogMode.None },
|
||||||
|
seerrVm = seerrVm,
|
||||||
|
)
|
||||||
|
|
||||||
if (showQuickConnectDialog) {
|
if (showQuickConnectDialog) {
|
||||||
val quickConnectStatus by viewModel.quickConnectStatus.collectAsState(LoadingState.Pending)
|
val quickConnectStatus by viewModel.quickConnectStatus.collectAsState(LoadingState.Pending)
|
||||||
val successMessage = stringResource(R.string.quick_connect_success)
|
val successMessage = stringResource(R.string.quick_connect_success)
|
||||||
|
|
@ -880,19 +832,3 @@ data class CacheUsage(
|
||||||
val imageMemoryMax: Long,
|
val imageMemoryMax: Long,
|
||||||
val imageDiskUsed: Long,
|
val imageDiskUsed: Long,
|
||||||
)
|
)
|
||||||
|
|
||||||
private sealed interface SeerrDialogMode {
|
|
||||||
data object None : SeerrDialogMode
|
|
||||||
|
|
||||||
data object Add : SeerrDialogMode
|
|
||||||
|
|
||||||
data class Remove(
|
|
||||||
val current: CurrentSeerr,
|
|
||||||
) : SeerrDialogMode
|
|
||||||
|
|
||||||
data class Error(
|
|
||||||
val server: SeerrServer,
|
|
||||||
val user: SeerrUser,
|
|
||||||
val ex: Exception,
|
|
||||||
) : SeerrDialogMode
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ import com.github.damontecres.wholphin.util.LoadingState
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AddSeerrServerApiKey(
|
fun AddSeerrServerApiKey(
|
||||||
|
initialUrl: String,
|
||||||
onSubmit: (url: String, apiKey: String) -> Unit,
|
onSubmit: (url: String, apiKey: String) -> Unit,
|
||||||
status: LoadingState,
|
status: LoadingState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
@ -54,12 +55,18 @@ fun AddSeerrServerApiKey(
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
.wrapContentSize(),
|
.wrapContentSize(),
|
||||||
) {
|
) {
|
||||||
var url by remember { mutableStateOf("") }
|
var url by remember { mutableStateOf(initialUrl) }
|
||||||
var apiKey by remember { mutableStateOf("") }
|
var apiKey by remember { mutableStateOf("") }
|
||||||
|
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val passwordFocusRequester = remember { FocusRequester() }
|
val passwordFocusRequester = remember { FocusRequester() }
|
||||||
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
LaunchedEffect(Unit) {
|
||||||
|
if (initialUrl.isBlank()) {
|
||||||
|
focusRequester.tryRequestFocus()
|
||||||
|
} else {
|
||||||
|
passwordFocusRequester.tryRequestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.enter_url_api_key),
|
text = stringResource(R.string.enter_url_api_key),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
|
@ -152,6 +159,7 @@ fun AddSeerrServerApiKey(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AddSeerrServerUsername(
|
fun AddSeerrServerUsername(
|
||||||
|
initialUrl: String,
|
||||||
onSubmit: (url: String, username: String, password: String) -> Unit,
|
onSubmit: (url: String, username: String, password: String) -> Unit,
|
||||||
username: String,
|
username: String,
|
||||||
status: LoadingState,
|
status: LoadingState,
|
||||||
|
|
@ -166,14 +174,22 @@ fun AddSeerrServerUsername(
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
.wrapContentSize(),
|
.wrapContentSize(),
|
||||||
) {
|
) {
|
||||||
var url by remember { mutableStateOf("") }
|
var url by remember { mutableStateOf(initialUrl) }
|
||||||
var username by remember { mutableStateOf(username) }
|
var username by remember { mutableStateOf(username) }
|
||||||
var password by remember { mutableStateOf("") }
|
var password by remember { mutableStateOf("") }
|
||||||
|
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val usernameFocusRequester = remember { FocusRequester() }
|
val usernameFocusRequester = remember { FocusRequester() }
|
||||||
val passwordFocusRequester = remember { FocusRequester() }
|
val passwordFocusRequester = remember { FocusRequester() }
|
||||||
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
LaunchedEffect(Unit) {
|
||||||
|
val focusRequesterToUse =
|
||||||
|
when {
|
||||||
|
url.isNotNullOrBlank() && username.isNotNullOrBlank() -> passwordFocusRequester
|
||||||
|
url.isNotNullOrBlank() -> usernameFocusRequester
|
||||||
|
else -> focusRequester
|
||||||
|
}
|
||||||
|
focusRequesterToUse.tryRequestFocus()
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.username_or_password),
|
text = stringResource(R.string.username_or_password),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
|
@ -317,6 +333,7 @@ fun AddSeerrServerUsername(
|
||||||
private fun AddSeerrServerUsernamePreview() {
|
private fun AddSeerrServerUsernamePreview() {
|
||||||
WholphinTheme {
|
WholphinTheme {
|
||||||
AddSeerrServerUsername(
|
AddSeerrServerUsername(
|
||||||
|
initialUrl = "",
|
||||||
onSubmit = { string: String, string1: String, string2: String -> },
|
onSubmit = { string: String, string1: String, string2: String -> },
|
||||||
username = "test",
|
username = "test",
|
||||||
status = LoadingState.Pending,
|
status = LoadingState.Pending,
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,161 @@
|
||||||
package com.github.damontecres.wholphin.ui.setup.seerr
|
package com.github.damontecres.wholphin.ui.setup.seerr
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
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
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import androidx.tv.material3.Text
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.SeerrAuthMethod
|
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 com.github.damontecres.wholphin.services.CurrentSeerr
|
||||||
|
import com.github.damontecres.wholphin.services.SeerrState
|
||||||
import com.github.damontecres.wholphin.ui.components.BasicDialog
|
import com.github.damontecres.wholphin.ui.components.BasicDialog
|
||||||
|
import com.github.damontecres.wholphin.ui.components.ConfirmDialog
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogItem
|
import com.github.damontecres.wholphin.ui.components.DialogItem
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
||||||
|
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
|
import com.github.damontecres.wholphin.ui.components.ScrollableDialog
|
||||||
|
import com.github.damontecres.wholphin.ui.preferences.rememberSeerrDialogBody
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SeerrDialog(
|
||||||
|
seerrDialogMode: SeerrDialogMode,
|
||||||
|
seerrConnection: SeerrState,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
seerrVm: SwitchSeerrViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
when (val mode = seerrDialogMode) {
|
||||||
|
is SeerrDialogMode.Remove -> {
|
||||||
|
val body = rememberSeerrDialogBody(mode.current.user, mode.current.server, seerrConnection.pluginProvided)
|
||||||
|
if (seerrConnection.pluginProvided) {
|
||||||
|
ScrollableDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Text(body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ConfirmDialog(
|
||||||
|
title = stringResource(R.string.remove_seerr_server),
|
||||||
|
body = body,
|
||||||
|
onCancel = onDismissRequest,
|
||||||
|
onConfirm = {
|
||||||
|
seerrVm.removeServer()
|
||||||
|
onDismissRequest.invoke()
|
||||||
|
},
|
||||||
|
elevation = 3.dp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SeerrDialogMode.Add -> {
|
||||||
|
val currentUser by seerrVm.currentUser.collectAsState(null)
|
||||||
|
val state by seerrVm.state.collectAsState()
|
||||||
|
val serverAddedMessage = stringResource(R.string.seerr_server_added)
|
||||||
|
LaunchedEffect(state.serverConnectionStatus) {
|
||||||
|
if (state.serverConnectionStatus == LoadingState.Success) {
|
||||||
|
Toast.makeText(context, serverAddedMessage, Toast.LENGTH_SHORT).show()
|
||||||
|
onDismissRequest.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
when (state.loading) {
|
||||||
|
is LoadingState.Error,
|
||||||
|
LoadingState.Loading,
|
||||||
|
LoadingState.Pending,
|
||||||
|
-> {
|
||||||
|
BasicDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
) {
|
||||||
|
LoadingPage(Modifier.padding(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadingState.Success -> {
|
||||||
|
AddSeerServerDialog(
|
||||||
|
initialUrl = state.initialUrl,
|
||||||
|
currentUsername = currentUser?.name,
|
||||||
|
status = state.serverConnectionStatus,
|
||||||
|
onSubmit = seerrVm::submitServer,
|
||||||
|
onResetStatus = seerrVm::resetStatus,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is SeerrDialogMode.Error -> {
|
||||||
|
val body = rememberSeerrDialogBody(mode.user, mode.server, seerrConnection.pluginProvided, mode.ex)
|
||||||
|
if (seerrConnection.pluginProvided) {
|
||||||
|
ConfirmDialog(
|
||||||
|
title = stringResource(R.string.error),
|
||||||
|
body = body,
|
||||||
|
onCancel = onDismissRequest,
|
||||||
|
onConfirm = {
|
||||||
|
seerrVm.retryServerFromPlugin()
|
||||||
|
onDismissRequest.invoke()
|
||||||
|
},
|
||||||
|
bodyColor = MaterialTheme.colorScheme.error,
|
||||||
|
elevation = 3.dp,
|
||||||
|
confirmStringRes = R.string.retry,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ConfirmDialog(
|
||||||
|
title = stringResource(R.string.remove_seerr_server),
|
||||||
|
body = body,
|
||||||
|
onCancel = onDismissRequest,
|
||||||
|
onConfirm = {
|
||||||
|
seerrVm.removeServer()
|
||||||
|
onDismissRequest.invoke()
|
||||||
|
},
|
||||||
|
bodyColor = MaterialTheme.colorScheme.error,
|
||||||
|
elevation = 3.dp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SeerrDialogMode.None -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface SeerrDialogMode {
|
||||||
|
data object None : SeerrDialogMode
|
||||||
|
|
||||||
|
data object Add : SeerrDialogMode
|
||||||
|
|
||||||
|
data class Remove(
|
||||||
|
val current: CurrentSeerr,
|
||||||
|
) : SeerrDialogMode
|
||||||
|
|
||||||
|
data class Error(
|
||||||
|
val server: SeerrServer,
|
||||||
|
val user: SeerrUser,
|
||||||
|
val ex: Exception,
|
||||||
|
) : SeerrDialogMode
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AddSeerServerDialog(
|
fun AddSeerServerDialog(
|
||||||
currentUsername: String?,
|
currentUsername: String?,
|
||||||
|
initialUrl: String?,
|
||||||
status: LoadingState,
|
status: LoadingState,
|
||||||
onSubmit: (url: String, username: String, passwordOrApiKey: String, method: SeerrAuthMethod) -> Unit,
|
onSubmit: (url: String, username: String, passwordOrApiKey: String, method: SeerrAuthMethod) -> Unit,
|
||||||
onResetStatus: () -> Unit,
|
onResetStatus: () -> Unit,
|
||||||
|
|
@ -42,6 +176,7 @@ fun AddSeerServerDialog(
|
||||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||||
) {
|
) {
|
||||||
AddSeerrServerUsername(
|
AddSeerrServerUsername(
|
||||||
|
initialUrl = initialUrl ?: "",
|
||||||
onSubmit = { url, username, password ->
|
onSubmit = { url, username, password ->
|
||||||
onSubmit.invoke(url, username, password, auth)
|
onSubmit.invoke(url, username, password, auth)
|
||||||
},
|
},
|
||||||
|
|
@ -57,6 +192,7 @@ fun AddSeerServerDialog(
|
||||||
onDismissRequest = { authMethod = null },
|
onDismissRequest = { authMethod = null },
|
||||||
) {
|
) {
|
||||||
AddSeerrServerApiKey(
|
AddSeerrServerApiKey(
|
||||||
|
initialUrl = initialUrl ?: "",
|
||||||
onSubmit = { url, apiKey ->
|
onSubmit = { url, apiKey ->
|
||||||
onSubmit.invoke(url, "", apiKey, SeerrAuthMethod.API_KEY)
|
onSubmit.invoke(url, "", apiKey, SeerrAuthMethod.API_KEY)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@ package com.github.damontecres.wholphin.ui.setup.seerr
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.api.seerr.infrastructure.ClientException
|
import com.github.damontecres.wholphin.api.seerr.infrastructure.ClientException
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.SeerrAuthMethod
|
import com.github.damontecres.wholphin.data.model.SeerrAuthMethod
|
||||||
import com.github.damontecres.wholphin.services.SeerrServerRepository
|
import com.github.damontecres.wholphin.services.SeerrServerRepository
|
||||||
import com.github.damontecres.wholphin.services.SeerrService
|
import com.github.damontecres.wholphin.services.SeerrService
|
||||||
|
import com.github.damontecres.wholphin.services.ServerPluginApi
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.showToast
|
import com.github.damontecres.wholphin.ui.showToast
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
|
|
@ -15,6 +17,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import jakarta.inject.Inject
|
import jakarta.inject.Inject
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
|
@ -28,11 +31,29 @@ class SwitchSeerrViewModel
|
||||||
private val seerrServerRepository: SeerrServerRepository,
|
private val seerrServerRepository: SeerrServerRepository,
|
||||||
private val seerrService: SeerrService,
|
private val seerrService: SeerrService,
|
||||||
private val serverRepository: ServerRepository,
|
private val serverRepository: ServerRepository,
|
||||||
|
private val serverPluginApi: ServerPluginApi,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val currentUser = serverRepository.currentUserFlow
|
val currentUser = serverRepository.currentUserFlow
|
||||||
val currentSeerrServer = seerrServerRepository.currentServer
|
val currentSeerrServer = seerrServerRepository.currentServer
|
||||||
|
|
||||||
val serverConnectionStatus = MutableStateFlow<LoadingState>(LoadingState.Pending)
|
private val _state = MutableStateFlow<SwitchSeerrState>(SwitchSeerrState())
|
||||||
|
val state: StateFlow<SwitchSeerrState> = _state
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (serverRepository.serverPluginInstalled.value) {
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
try {
|
||||||
|
serverPluginApi.fetchSeerrSettings()?.serverUrl?.let { serverUrl ->
|
||||||
|
_state.update { it.copy(initialUrl = serverUrl) }
|
||||||
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Timber.e(ex, "Error fetching seerr settings")
|
||||||
|
} finally {
|
||||||
|
_state.update { it.copy(loading = LoadingState.Success) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun submitServer(
|
fun submitServer(
|
||||||
url: String,
|
url: String,
|
||||||
|
|
@ -41,13 +62,21 @@ class SwitchSeerrViewModel
|
||||||
authMethod: SeerrAuthMethod,
|
authMethod: SeerrAuthMethod,
|
||||||
) {
|
) {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
serverConnectionStatus.update { LoadingState.Loading }
|
_state.update { it.copy(serverConnectionStatus = LoadingState.Loading) }
|
||||||
val urls =
|
val urls =
|
||||||
try {
|
try {
|
||||||
createUrls(url)
|
createUrls(url)
|
||||||
} catch (ex: IllegalArgumentException) {
|
} catch (ex: IllegalArgumentException) {
|
||||||
showToast(context, "Invalid URL")
|
showToast(context, "Invalid URL")
|
||||||
serverConnectionStatus.update { LoadingState.Error("Invalid URL", ex) }
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
serverConnectionStatus =
|
||||||
|
LoadingState.Error(
|
||||||
|
"Invalid URL",
|
||||||
|
ex,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
return@launchIO
|
return@launchIO
|
||||||
}
|
}
|
||||||
Timber.v("Urls to try: %s", urls)
|
Timber.v("Urls to try: %s", urls)
|
||||||
|
|
@ -98,7 +127,7 @@ class SwitchSeerrViewModel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
serverConnectionStatus.update { LoadingState.Success }
|
_state.update { it.copy(serverConnectionStatus = LoadingState.Success) }
|
||||||
} else {
|
} else {
|
||||||
val message =
|
val message =
|
||||||
results
|
results
|
||||||
|
|
@ -107,7 +136,7 @@ class SwitchSeerrViewModel
|
||||||
"$url - ${s?.localizedMessage}"
|
"$url - ${s?.localizedMessage}"
|
||||||
}.joinToString("\n")
|
}.joinToString("\n")
|
||||||
showToast(context, "Could not connect")
|
showToast(context, "Could not connect")
|
||||||
serverConnectionStatus.update { LoadingState.Error(message) }
|
_state.update { it.copy(serverConnectionStatus = LoadingState.Error(message)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -121,11 +150,31 @@ class SwitchSeerrViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun retryServerFromPlugin() {
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
val settings = serverPluginApi.fetchSeerrSettings()
|
||||||
|
if (settings != null) {
|
||||||
|
val result = seerrServerRepository.usePluginProvidedServer(settings)
|
||||||
|
if (!result) {
|
||||||
|
showToast(context, context.getString(R.string.voice_error_server))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
seerrServerRepository.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun resetStatus() {
|
fun resetStatus() {
|
||||||
serverConnectionStatus.update { LoadingState.Pending }
|
_state.update { it.copy(serverConnectionStatus = LoadingState.Pending) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class SwitchSeerrState(
|
||||||
|
val loading: LoadingState = LoadingState.Pending,
|
||||||
|
val serverConnectionStatus: LoadingState = LoadingState.Pending,
|
||||||
|
val initialUrl: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
fun createUrls(url: String): List<HttpUrl> {
|
fun createUrls(url: String): List<HttpUrl> {
|
||||||
val urls = mutableListOf<HttpUrl>()
|
val urls = mutableListOf<HttpUrl>()
|
||||||
if (url.startsWith("http://") || url.startsWith("https://")) {
|
if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||||
|
|
|
||||||
|
|
@ -977,4 +977,6 @@
|
||||||
<string name="home_settings_source_server_profile">Server user profile</string>
|
<string name="home_settings_source_server_profile">Server user profile</string>
|
||||||
<string name="home_settings_source_plugin">Server default</string>
|
<string name="home_settings_source_plugin">Server default</string>
|
||||||
<string name="home_settings_source_web">User web client</string>
|
<string name="home_settings_source_web">User web client</string>
|
||||||
|
<string name="managed_by_server">Managed by server</string>
|
||||||
|
<string name="error">Error</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue