Merge Wholphin v1.1.0 while preserving Wolfskin features #27
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