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.model.SeerrAuthMethod
|
||||
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.SeerrUser
|
||||
import com.github.damontecres.wholphin.data.model.hasPermission
|
||||
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.util.LoadingState
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import okhttp3.OkHttpClient
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
|
@ -39,26 +43,25 @@ class SeerrServerRepository
|
|||
private val serverRepository: ServerRepository,
|
||||
@param:StandardOkHttpClient private val okHttpClient: OkHttpClient,
|
||||
) {
|
||||
private val _connection =
|
||||
MutableStateFlow<SeerrConnectionStatus>(SeerrConnectionStatus.NotConfigured)
|
||||
val connection: StateFlow<SeerrConnectionStatus> = _connection
|
||||
private val _connection = MutableStateFlow<SeerrState>(SeerrState())
|
||||
val connection: StateFlow<SeerrState> = _connection
|
||||
|
||||
val current: Flow<CurrentSeerr?> =
|
||||
_connection.map { (it as? SeerrConnectionStatus.Success)?.current }
|
||||
_connection.map { (it.connection as? SeerrConnectionStatus.Success)?.current }
|
||||
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?> =
|
||||
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 }
|
||||
|
||||
/**
|
||||
* Whether Seerr integration is currently active of not
|
||||
*/
|
||||
val active: Flow<Boolean> =
|
||||
connection.map { it is SeerrConnectionStatus.Success && seerrApi.active }
|
||||
connection.map { it.connection is SeerrConnectionStatus.Success && seerrApi.active }
|
||||
|
||||
fun clear() {
|
||||
_connection.update { SeerrConnectionStatus.NotConfigured }
|
||||
_connection.update { SeerrState() }
|
||||
seerrApi.update("", null)
|
||||
}
|
||||
|
||||
|
|
@ -67,7 +70,7 @@ class SeerrServerRepository
|
|||
user: SeerrUser,
|
||||
exception: Exception,
|
||||
) {
|
||||
_connection.update { SeerrConnectionStatus.Error(server, user, exception) }
|
||||
_connection.update { it.copy(connection = SeerrConnectionStatus.Error(server, user, exception)) }
|
||||
seerrApi.update("", null)
|
||||
}
|
||||
|
||||
|
|
@ -78,8 +81,9 @@ class SeerrServerRepository
|
|||
) {
|
||||
val publicSettings = seerrApi.api.settingsApi.settingsPublicGet()
|
||||
_connection.update {
|
||||
SeerrConnectionStatus.Success(
|
||||
CurrentSeerr(server, user, userConfig, publicSettings),
|
||||
it.copy(
|
||||
connection =
|
||||
SeerrConnectionStatus.Success(CurrentSeerr(server, user, userConfig, publicSettings)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -169,7 +173,7 @@ class SeerrServerRepository
|
|||
|
||||
suspend fun removeServerForCurrentUser(): Boolean {
|
||||
val user =
|
||||
when (val conn = connection.first()) {
|
||||
when (val conn = connection.value.connection) {
|
||||
SeerrConnectionStatus.NotConfigured -> return false
|
||||
is SeerrConnectionStatus.Error -> conn.user
|
||||
is SeerrConnectionStatus.Success -> conn.current.user
|
||||
|
|
@ -178,6 +182,81 @@ class SeerrServerRepository
|
|||
clear()
|
||||
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
|
||||
|
||||
data class SeerrState(
|
||||
val connection: SeerrConnectionStatus = SeerrConnectionStatus.NotConfigured,
|
||||
val pluginProvided: Boolean = false,
|
||||
)
|
||||
|
||||
sealed interface SeerrConnectionStatus {
|
||||
data object NotConfigured : SeerrConnectionStatus
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
package com.github.damontecres.wholphin.services
|
||||
|
||||
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.util.WholphinDispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okio.IOException
|
||||
|
|
@ -32,6 +34,7 @@ class ServerPluginApi
|
|||
|
||||
companion object {
|
||||
private const val HOME_CONFIG_PATH = "homesettings"
|
||||
private const val SEERR_CONFIG_PATH = "seerrsettings"
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -57,32 +60,37 @@ class ServerPluginApi
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
private suspend inline fun <reified T> executeRequest(path: String): T? {
|
||||
val url = createUrl(path) ?: return null
|
||||
val request = Request(url.toHttpUrl())
|
||||
return executeRequest<T>(request)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
suspend fun fetchHomePageSettings(): HomePageSettings? =
|
||||
private suspend inline fun <reified T> executeRequest(request: Request): T? =
|
||||
withContext(WholphinDispatchers.IO) {
|
||||
val url = createUrl(HOME_CONFIG_PATH) ?: return@withContext null
|
||||
val request =
|
||||
Request
|
||||
.Builder()
|
||||
.url(url)
|
||||
.get()
|
||||
.build()
|
||||
return@withContext okHttpClient.newCall(request).execute().use { res ->
|
||||
okHttpClient.newCall(request).execute().use { res ->
|
||||
if (res.isSuccessful) {
|
||||
json
|
||||
.decodeFromStream<HomePageSettings>(res.body.byteStream())
|
||||
.takeIf { it.rows.isNotEmpty() }
|
||||
json.decodeFromStream<T>(res.body.byteStream())
|
||||
} else if (res.code == 404) {
|
||||
Timber.w("fetchHomePageSettings returned 404")
|
||||
Timber.w("Plugin 404: %s", request.url.encodedPath)
|
||||
null
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
|
||||
// Check if plugin is installed, then for home settings
|
||||
// Check if plugin is installed, then for home settings & seerr settings
|
||||
launchIO {
|
||||
val serverPluginInstalled =
|
||||
try {
|
||||
|
|
@ -81,48 +81,64 @@ class UserSwitchListener
|
|||
Timber.i("Server plugin installed: %s", serverPluginInstalled)
|
||||
serverRepository.serverPluginInstalled.value = serverPluginInstalled
|
||||
|
||||
// Check for home settings
|
||||
homeSettingsService.loadCurrentSettings(user)
|
||||
}
|
||||
if (BuildConfig.DISCOVER_ENABLED) {
|
||||
// Check for seerr server
|
||||
launchIO {
|
||||
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)
|
||||
// Check for home settings
|
||||
homeSettingsService.loadCurrentSettings(user)
|
||||
}
|
||||
|
||||
if (BuildConfig.DISCOVER_ENABLED) {
|
||||
// Check for seerr server
|
||||
launchIO {
|
||||
if (serverPluginInstalled) {
|
||||
serverPluginApi.fetchSeerrSettings()?.let {
|
||||
Timber.d("Trying seerr auto login")
|
||||
val result = seerrServerRepository.usePluginProvidedServer(it)
|
||||
if (result) {
|
||||
// Successfully used server plugin provided seerr
|
||||
Timber.i("Seerr auto login succeeded")
|
||||
return@launchIO
|
||||
}
|
||||
}
|
||||
}
|
||||
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(),
|
||||
elevation: Dp = 1.dp,
|
||||
bodyColor: Color = MaterialTheme.colorScheme.onSurface,
|
||||
@StringRes cancelStringRes: Int = R.string.cancel,
|
||||
@StringRes confirmStringRes: Int = R.string.confirm,
|
||||
) = BasicDialog(
|
||||
onDismissRequest = onCancel,
|
||||
properties = properties,
|
||||
elevation = elevation,
|
||||
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,
|
||||
modifier: Modifier = Modifier,
|
||||
bodyColor: Color = MaterialTheme.colorScheme.onSurface,
|
||||
@StringRes cancelStringRes: Int = R.string.cancel,
|
||||
@StringRes confirmStringRes: Int = R.string.confirm,
|
||||
) {
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
|
|
@ -500,11 +504,11 @@ fun ConfirmDialogContent(
|
|||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
TextButton(
|
||||
stringRes = R.string.cancel,
|
||||
stringRes = cancelStringRes,
|
||||
onClick = onCancel,
|
||||
)
|
||||
TextButton(
|
||||
stringRes = R.string.confirm,
|
||||
stringRes = confirmStringRes,
|
||||
onClick = onConfirm,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ enum class PreferenceScreenOption {
|
|||
fun rememberSeerrDialogBody(
|
||||
user: SeerrUser,
|
||||
server: SeerrServer,
|
||||
pluginProvided: Boolean,
|
||||
ex: Exception? = null,
|
||||
): String {
|
||||
val username =
|
||||
|
|
@ -81,8 +82,13 @@ fun rememberSeerrDialogBody(
|
|||
${stringResource(R.string.url)}: ${server.url}
|
||||
${stringResource(R.string.username)}: $username$method
|
||||
""".trimIndent() +
|
||||
if (pluginProvided) {
|
||||
"\n\n${stringResource(R.string.managed_by_server)}"
|
||||
} else {
|
||||
""
|
||||
} +
|
||||
if (ex != null) {
|
||||
"\n\n${stringResource(R.string.voice_error_server)}: ${ex.localizedMessage}"
|
||||
"\n\n${stringResource(R.string.error)}: ${ex.localizedMessage}"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,8 +48,6 @@ import androidx.tv.material3.surfaceColorAtElevation
|
|||
import coil3.SingletonImageLoader
|
||||
import coil3.imageLoader
|
||||
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.AppPreferences
|
||||
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.screensaverPreferences
|
||||
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.SeerrConnectionStatus
|
||||
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.setup.ReleaseNotes
|
||||
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.showToast
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
|
|
@ -501,7 +499,7 @@ fun PreferencesContent(
|
|||
title = stringResource(pref.title),
|
||||
onClick = {
|
||||
seerrDialogMode =
|
||||
when (val conn = seerrConnection) {
|
||||
when (val conn = seerrConnection.connection) {
|
||||
is SeerrConnectionStatus.Error -> {
|
||||
SeerrDialogMode.Error(
|
||||
conn.server,
|
||||
|
|
@ -523,15 +521,13 @@ fun PreferencesContent(
|
|||
},
|
||||
modifier = focusModifier,
|
||||
summary =
|
||||
when (seerrConnection) {
|
||||
when (seerrConnection.connection) {
|
||||
is SeerrConnectionStatus.Error -> {
|
||||
stringResource(R.string.voice_error_server)
|
||||
}
|
||||
|
||||
SeerrConnectionStatus.NotConfigured -> {
|
||||
stringResource(
|
||||
R.string.add_server,
|
||||
)
|
||||
stringResource(R.string.add_server)
|
||||
}
|
||||
|
||||
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) {
|
||||
val quickConnectStatus by viewModel.quickConnectStatus.collectAsState(LoadingState.Pending)
|
||||
val successMessage = stringResource(R.string.quick_connect_success)
|
||||
|
|
@ -880,19 +832,3 @@ data class CacheUsage(
|
|||
val imageMemoryMax: 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
|
||||
fun AddSeerrServerApiKey(
|
||||
initialUrl: String,
|
||||
onSubmit: (url: String, apiKey: String) -> Unit,
|
||||
status: LoadingState,
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -54,12 +55,18 @@ fun AddSeerrServerApiKey(
|
|||
.padding(16.dp)
|
||||
.wrapContentSize(),
|
||||
) {
|
||||
var url by remember { mutableStateOf("") }
|
||||
var url by remember { mutableStateOf(initialUrl) }
|
||||
var apiKey by remember { mutableStateOf("") }
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val passwordFocusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||
LaunchedEffect(Unit) {
|
||||
if (initialUrl.isBlank()) {
|
||||
focusRequester.tryRequestFocus()
|
||||
} else {
|
||||
passwordFocusRequester.tryRequestFocus()
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = stringResource(R.string.enter_url_api_key),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
|
|
@ -152,6 +159,7 @@ fun AddSeerrServerApiKey(
|
|||
|
||||
@Composable
|
||||
fun AddSeerrServerUsername(
|
||||
initialUrl: String,
|
||||
onSubmit: (url: String, username: String, password: String) -> Unit,
|
||||
username: String,
|
||||
status: LoadingState,
|
||||
|
|
@ -166,14 +174,22 @@ fun AddSeerrServerUsername(
|
|||
.padding(16.dp)
|
||||
.wrapContentSize(),
|
||||
) {
|
||||
var url by remember { mutableStateOf("") }
|
||||
var url by remember { mutableStateOf(initialUrl) }
|
||||
var username by remember { mutableStateOf(username) }
|
||||
var password by remember { mutableStateOf("") }
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val usernameFocusRequester = 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 = stringResource(R.string.username_or_password),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
|
|
@ -317,6 +333,7 @@ fun AddSeerrServerUsername(
|
|||
private fun AddSeerrServerUsernamePreview() {
|
||||
WholphinTheme {
|
||||
AddSeerrServerUsername(
|
||||
initialUrl = "",
|
||||
onSubmit = { string: String, string1: String, string2: String -> },
|
||||
username = "test",
|
||||
status = LoadingState.Pending,
|
||||
|
|
|
|||
|
|
@ -1,27 +1,161 @@
|
|||
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.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.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.ConfirmDialog
|
||||
import com.github.damontecres.wholphin.ui.components.DialogItem
|
||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||
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
|
||||
|
||||
@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
|
||||
fun AddSeerServerDialog(
|
||||
currentUsername: String?,
|
||||
initialUrl: String?,
|
||||
status: LoadingState,
|
||||
onSubmit: (url: String, username: String, passwordOrApiKey: String, method: SeerrAuthMethod) -> Unit,
|
||||
onResetStatus: () -> Unit,
|
||||
|
|
@ -42,6 +176,7 @@ fun AddSeerServerDialog(
|
|||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||
) {
|
||||
AddSeerrServerUsername(
|
||||
initialUrl = initialUrl ?: "",
|
||||
onSubmit = { url, username, password ->
|
||||
onSubmit.invoke(url, username, password, auth)
|
||||
},
|
||||
|
|
@ -57,6 +192,7 @@ fun AddSeerServerDialog(
|
|||
onDismissRequest = { authMethod = null },
|
||||
) {
|
||||
AddSeerrServerApiKey(
|
||||
initialUrl = initialUrl ?: "",
|
||||
onSubmit = { url, apiKey ->
|
||||
onSubmit.invoke(url, "", apiKey, SeerrAuthMethod.API_KEY)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ package com.github.damontecres.wholphin.ui.setup.seerr
|
|||
import android.content.Context
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.api.seerr.infrastructure.ClientException
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.data.model.SeerrAuthMethod
|
||||
import com.github.damontecres.wholphin.services.SeerrServerRepository
|
||||
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.showToast
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
|
|
@ -15,6 +17,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import jakarta.inject.Inject
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
|
|
@ -28,11 +31,29 @@ class SwitchSeerrViewModel
|
|||
private val seerrServerRepository: SeerrServerRepository,
|
||||
private val seerrService: SeerrService,
|
||||
private val serverRepository: ServerRepository,
|
||||
private val serverPluginApi: ServerPluginApi,
|
||||
) : ViewModel() {
|
||||
val currentUser = serverRepository.currentUserFlow
|
||||
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(
|
||||
url: String,
|
||||
|
|
@ -41,13 +62,21 @@ class SwitchSeerrViewModel
|
|||
authMethod: SeerrAuthMethod,
|
||||
) {
|
||||
viewModelScope.launchIO {
|
||||
serverConnectionStatus.update { LoadingState.Loading }
|
||||
_state.update { it.copy(serverConnectionStatus = LoadingState.Loading) }
|
||||
val urls =
|
||||
try {
|
||||
createUrls(url)
|
||||
} catch (ex: IllegalArgumentException) {
|
||||
showToast(context, "Invalid URL")
|
||||
serverConnectionStatus.update { LoadingState.Error("Invalid URL", ex) }
|
||||
_state.update {
|
||||
it.copy(
|
||||
serverConnectionStatus =
|
||||
LoadingState.Error(
|
||||
"Invalid URL",
|
||||
ex,
|
||||
),
|
||||
)
|
||||
}
|
||||
return@launchIO
|
||||
}
|
||||
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 {
|
||||
val message =
|
||||
results
|
||||
|
|
@ -107,7 +136,7 @@ class SwitchSeerrViewModel
|
|||
"$url - ${s?.localizedMessage}"
|
||||
}.joinToString("\n")
|
||||
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() {
|
||||
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> {
|
||||
val urls = mutableListOf<HttpUrl>()
|
||||
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_plugin">Server default</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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue