mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-09 16:11:20 +02:00
Basic login UI flow
This commit is contained in:
parent
a2884a7225
commit
6046799b91
17 changed files with 577 additions and 69 deletions
|
|
@ -2,7 +2,7 @@
|
||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"database": {
|
"database": {
|
||||||
"version": 20,
|
"version": 20,
|
||||||
"identityHash": "4385c87cd6f510f285ec9802cff1ce2d",
|
"identityHash": "dea51adcc724179afa0174d775f97480",
|
||||||
"entities": [
|
"entities": [
|
||||||
{
|
{
|
||||||
"tableName": "servers",
|
"tableName": "servers",
|
||||||
|
|
@ -427,7 +427,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tableName": "seerr_servers",
|
"tableName": "seerr_servers",
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `version` TEXT)",
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `name` TEXT, `version` TEXT)",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "id",
|
"fieldPath": "id",
|
||||||
|
|
@ -435,17 +435,17 @@
|
||||||
"affinity": "INTEGER",
|
"affinity": "INTEGER",
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldPath": "url",
|
"fieldPath": "url",
|
||||||
"columnName": "url",
|
"columnName": "url",
|
||||||
"affinity": "TEXT",
|
"affinity": "TEXT",
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldPath": "version",
|
"fieldPath": "version",
|
||||||
"columnName": "version",
|
"columnName": "version",
|
||||||
|
|
@ -472,11 +472,11 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tableName": "seerr_users",
|
"tableName": "seerr_users",
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jellyfinRowId` INTEGER NOT NULL, `serverId` INTEGER NOT NULL, `authMethod` TEXT NOT NULL, `username` TEXT, `password` TEXT, `credential` TEXT, PRIMARY KEY(`jellyfinRowId`, `serverId`), FOREIGN KEY(`serverId`) REFERENCES `seerr_servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`jellyfinRowId`) REFERENCES `users`(`rowId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jellyfinUserRowId` INTEGER NOT NULL, `serverId` INTEGER NOT NULL, `authMethod` TEXT NOT NULL, `username` TEXT, `password` TEXT, `credential` TEXT, PRIMARY KEY(`jellyfinUserRowId`, `serverId`), FOREIGN KEY(`serverId`) REFERENCES `seerr_servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`jellyfinUserRowId`) REFERENCES `users`(`rowId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "jellyfinRowId",
|
"fieldPath": "jellyfinUserRowId",
|
||||||
"columnName": "jellyfinRowId",
|
"columnName": "jellyfinUserRowId",
|
||||||
"affinity": "INTEGER",
|
"affinity": "INTEGER",
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
|
|
@ -511,7 +511,7 @@
|
||||||
"primaryKey": {
|
"primaryKey": {
|
||||||
"autoGenerate": false,
|
"autoGenerate": false,
|
||||||
"columnNames": [
|
"columnNames": [
|
||||||
"jellyfinRowId",
|
"jellyfinUserRowId",
|
||||||
"serverId"
|
"serverId"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -532,7 +532,7 @@
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"columns": [
|
"columns": [
|
||||||
"jellyfinRowId"
|
"jellyfinUserRowId"
|
||||||
],
|
],
|
||||||
"referencedColumns": [
|
"referencedColumns": [
|
||||||
"rowId"
|
"rowId"
|
||||||
|
|
@ -543,7 +543,7 @@
|
||||||
],
|
],
|
||||||
"setupQueries": [
|
"setupQueries": [
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4385c87cd6f510f285ec9802cff1ce2d')"
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'dea51adcc724179afa0174d775f97480')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import com.github.damontecres.wholphin.preferences.DefaultUserConfiguration
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.services.AppUpgradeHandler
|
import com.github.damontecres.wholphin.services.AppUpgradeHandler
|
||||||
import com.github.damontecres.wholphin.services.BackdropService
|
import com.github.damontecres.wholphin.services.BackdropService
|
||||||
|
import com.github.damontecres.wholphin.services.DatePlayedInvalidationService
|
||||||
import com.github.damontecres.wholphin.services.DeviceProfileService
|
import com.github.damontecres.wholphin.services.DeviceProfileService
|
||||||
import com.github.damontecres.wholphin.services.ImageUrlService
|
import com.github.damontecres.wholphin.services.ImageUrlService
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
|
|
@ -44,6 +45,7 @@ import com.github.damontecres.wholphin.services.RefreshRateService
|
||||||
import com.github.damontecres.wholphin.services.SetupDestination
|
import com.github.damontecres.wholphin.services.SetupDestination
|
||||||
import com.github.damontecres.wholphin.services.SetupNavigationManager
|
import com.github.damontecres.wholphin.services.SetupNavigationManager
|
||||||
import com.github.damontecres.wholphin.services.UpdateChecker
|
import com.github.damontecres.wholphin.services.UpdateChecker
|
||||||
|
import com.github.damontecres.wholphin.services.UserSwitchListener
|
||||||
import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient
|
import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient
|
||||||
import com.github.damontecres.wholphin.ui.CoilConfig
|
import com.github.damontecres.wholphin.ui.CoilConfig
|
||||||
import com.github.damontecres.wholphin.ui.LocalImageUrlService
|
import com.github.damontecres.wholphin.ui.LocalImageUrlService
|
||||||
|
|
@ -97,6 +99,12 @@ class MainActivity : AppCompatActivity() {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var refreshRateService: RefreshRateService
|
lateinit var refreshRateService: RefreshRateService
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var datePlayedInvalidationService: DatePlayedInvalidationService
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var userSwitchListener: UserSwitchListener
|
||||||
|
|
||||||
private var signInAuto = true
|
private var signInAuto = true
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
@OptIn(ExperimentalTvMaterial3Api::class)
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,24 @@ package com.github.damontecres.wholphin.api.seerr
|
||||||
import com.github.damontecres.wholphin.api.seerr.infrastructure.ApiClient
|
import com.github.damontecres.wholphin.api.seerr.infrastructure.ApiClient
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
|
import okhttp3.Cookie
|
||||||
|
import okhttp3.CookieJar
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
class SeerrApiClient(
|
class SeerrApiClient(
|
||||||
val baseUrl: String,
|
val baseUrl: String,
|
||||||
val apiKey: String?,
|
private val apiKey: String?,
|
||||||
okHttpClient: OkHttpClient,
|
okHttpClient: OkHttpClient,
|
||||||
) {
|
) {
|
||||||
|
private val cookieJar = SeerrCookieJar()
|
||||||
|
|
||||||
private val client =
|
private val client =
|
||||||
okHttpClient
|
okHttpClient
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
|
.cookieJar(cookieJar)
|
||||||
.addInterceptor {
|
.addInterceptor {
|
||||||
Timber.d("SeerrApiClient: ${it.request().method} ${it.request().url}")
|
Timber.d("SeerrApiClient: ${it.request().method} ${it.request().url}")
|
||||||
it.proceed(
|
it.proceed(
|
||||||
|
|
@ -26,6 +33,11 @@ class SeerrApiClient(
|
||||||
)
|
)
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
|
val hasValidCredentials: Boolean
|
||||||
|
get() =
|
||||||
|
apiKey.isNotNullOrBlank() ||
|
||||||
|
cookieJar.hasValidCredentials(baseUrl)
|
||||||
|
|
||||||
private fun <T : ApiClient> create(initializer: (String, Call.Factory) -> T): Lazy<T> =
|
private fun <T : ApiClient> create(initializer: (String, Call.Factory) -> T): Lazy<T> =
|
||||||
lazy {
|
lazy {
|
||||||
initializer.invoke(baseUrl, client)
|
initializer.invoke(baseUrl, client)
|
||||||
|
|
@ -50,3 +62,26 @@ class SeerrApiClient(
|
||||||
val usersApi by create(::UsersApi)
|
val usersApi by create(::UsersApi)
|
||||||
val watchlistApi by create(::WatchlistApi)
|
val watchlistApi by create(::WatchlistApi)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class SeerrCookieJar : CookieJar {
|
||||||
|
private val cookies = mutableMapOf<String, List<Cookie>>()
|
||||||
|
|
||||||
|
override fun saveFromResponse(
|
||||||
|
url: HttpUrl,
|
||||||
|
cookies: List<Cookie>,
|
||||||
|
) {
|
||||||
|
cookies
|
||||||
|
.filter { it.name == "connect.sid" }
|
||||||
|
.groupBy { it.domain }
|
||||||
|
.forEach { (domain, cookies) ->
|
||||||
|
this.cookies[domain] = cookies
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun loadForRequest(url: HttpUrl): List<Cookie> = this.cookies[url.host].orEmpty()
|
||||||
|
|
||||||
|
fun hasValidCredentials(baseUrl: String): Boolean =
|
||||||
|
baseUrl.toHttpUrlOrNull()?.host?.let { domain ->
|
||||||
|
cookies[domain]?.any { it.expiresAt > System.currentTimeMillis() }
|
||||||
|
} == true
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import androidx.room.Update
|
||||||
import com.github.damontecres.wholphin.data.model.SeerrServer
|
import com.github.damontecres.wholphin.data.model.SeerrServer
|
||||||
import com.github.damontecres.wholphin.data.model.SeerrServerUsers
|
import com.github.damontecres.wholphin.data.model.SeerrServerUsers
|
||||||
import com.github.damontecres.wholphin.data.model.SeerrUser
|
import com.github.damontecres.wholphin.data.model.SeerrUser
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface SeerrServerDao {
|
interface SeerrServerDao {
|
||||||
|
|
@ -32,22 +31,25 @@ interface SeerrServerDao {
|
||||||
|
|
||||||
suspend fun updateUser(user: SeerrUser) = addUser(user)
|
suspend fun updateUser(user: SeerrUser) = addUser(user)
|
||||||
|
|
||||||
@Query("SELECT * FROM seerr_users WHERE serverId = :serverId AND jellyfinRowId = :userId")
|
@Query("SELECT * FROM seerr_users WHERE serverId = :serverId AND jellyfinUserRowId = :jellyfinUserRowId")
|
||||||
suspend fun getUser(
|
suspend fun getUser(
|
||||||
serverId: Int,
|
serverId: Int,
|
||||||
userId: UUID,
|
jellyfinUserRowId: Int,
|
||||||
): SeerrUser?
|
): SeerrUser?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM seerr_users WHERE jellyfinUserRowId = :jellyfinUserRowId")
|
||||||
|
suspend fun getUsersByJellyfinUser(jellyfinUserRowId: Int): List<SeerrUser>
|
||||||
|
|
||||||
@Query("DELETE FROM seerr_servers WHERE id = :serverId")
|
@Query("DELETE FROM seerr_servers WHERE id = :serverId")
|
||||||
suspend fun deleteServer(serverId: Int)
|
suspend fun deleteServer(serverId: Int)
|
||||||
|
|
||||||
@Query("DELETE FROM seerr_users WHERE serverId = :serverId AND jellyfinRowId = :jellyfinRowId")
|
@Query("DELETE FROM seerr_users WHERE serverId = :serverId AND jellyfinUserRowId = :jellyfinUserRowId")
|
||||||
suspend fun deleteUser(
|
suspend fun deleteUser(
|
||||||
serverId: Int,
|
serverId: Int,
|
||||||
jellyfinRowId: Int,
|
jellyfinUserRowId: Int,
|
||||||
)
|
)
|
||||||
|
|
||||||
suspend fun deleteUser(user: SeerrUser) = deleteUser(user.serverId, user.jellyfinRowId)
|
suspend fun deleteUser(user: SeerrUser) = deleteUser(user.serverId, user.jellyfinUserRowId)
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT * FROM seerr_servers")
|
@Query("SELECT * FROM seerr_servers")
|
||||||
|
|
|
||||||
|
|
@ -38,14 +38,14 @@ data class SeerrServer(
|
||||||
ForeignKey(
|
ForeignKey(
|
||||||
entity = JellyfinUser::class,
|
entity = JellyfinUser::class,
|
||||||
parentColumns = arrayOf("rowId"),
|
parentColumns = arrayOf("rowId"),
|
||||||
childColumns = arrayOf("jellyfinRowId"),
|
childColumns = arrayOf("jellyfinUserRowId"),
|
||||||
onDelete = ForeignKey.CASCADE,
|
onDelete = ForeignKey.CASCADE,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
primaryKeys = ["jellyfinRowId", "serverId"],
|
primaryKeys = ["jellyfinUserRowId", "serverId"],
|
||||||
)
|
)
|
||||||
data class SeerrUser(
|
data class SeerrUser(
|
||||||
val jellyfinRowId: Int,
|
val jellyfinUserRowId: Int,
|
||||||
val serverId: Int,
|
val serverId: Int,
|
||||||
val authMethod: SeerrAuthMethod,
|
val authMethod: SeerrAuthMethod,
|
||||||
val username: String?,
|
val username: String?,
|
||||||
|
|
@ -53,7 +53,7 @@ data class SeerrUser(
|
||||||
val credential: String?,
|
val credential: String?,
|
||||||
) {
|
) {
|
||||||
override fun toString(): String =
|
override fun toString(): String =
|
||||||
"SeerrUser(jellyfinRowId=$jellyfinRowId, serverId=$serverId, authMethod=$authMethod, username=$username, password?=${password.isNotNullOrBlank()}, credential?=${credential.isNotNullOrBlank()})"
|
"SeerrUser(jellyfinUserRowId=$jellyfinUserRowId, serverId=$serverId, authMethod=$authMethod, username=$username, password?=${password.isNotNullOrBlank()}, credential?=${credential.isNotNullOrBlank()})"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class SeerrAuthMethod {
|
enum class SeerrAuthMethod {
|
||||||
|
|
|
||||||
|
|
@ -842,6 +842,13 @@ sealed interface AppPreference<Pref, T> {
|
||||||
summaryOn = R.string.enabled,
|
summaryOn = R.string.enabled,
|
||||||
summaryOff = R.string.disabled,
|
summaryOff = R.string.disabled,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val SeerrIntegration =
|
||||||
|
AppClickablePreference<AppPreferences>(
|
||||||
|
title = R.string.seerr_integration,
|
||||||
|
getter = { },
|
||||||
|
setter = { prefs, _ -> prefs },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -903,6 +910,7 @@ val basicPreferences =
|
||||||
title = R.string.more,
|
title = R.string.more,
|
||||||
preferences =
|
preferences =
|
||||||
listOf(
|
listOf(
|
||||||
|
AppPreference.SeerrIntegration,
|
||||||
AppPreference.AdvancedSettings,
|
AppPreference.AdvancedSettings,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package com.github.damontecres.wholphin.services
|
package com.github.damontecres.wholphin.services
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
|
||||||
import com.github.damontecres.wholphin.api.seerr.SeerrApiClient
|
import com.github.damontecres.wholphin.api.seerr.SeerrApiClient
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
|
@ -11,18 +10,20 @@ class SeerrApi(
|
||||||
@param:ApplicationContext private val context: Context,
|
@param:ApplicationContext private val context: Context,
|
||||||
private val okHttpClient: OkHttpClient,
|
private val okHttpClient: OkHttpClient,
|
||||||
) {
|
) {
|
||||||
private val prefs: SharedPreferences =
|
// private val prefs: SharedPreferences =
|
||||||
context.getSharedPreferences("seerr", Context.MODE_PRIVATE)
|
// context.getSharedPreferences("seerr", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
var api: SeerrApiClient =
|
var api: SeerrApiClient =
|
||||||
SeerrApiClient(
|
SeerrApiClient(
|
||||||
prefs.getString("baseUrl", null)!! + "/api/v1",
|
// prefs.getString("baseUrl", null)!! + "/api/v1",
|
||||||
prefs.getString("apiKey", null)!!,
|
// prefs.getString("apiKey", null)!!,
|
||||||
okHttpClient,
|
baseUrl = "",
|
||||||
|
apiKey = null,
|
||||||
|
okHttpClient = okHttpClient,
|
||||||
)
|
)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
val active: Boolean get() = api.baseUrl.isNotNullOrBlank() && api.apiKey.isNotNullOrBlank()
|
val active: Boolean get() = api.baseUrl.isNotNullOrBlank()
|
||||||
|
|
||||||
fun update(
|
fun update(
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,29 @@
|
||||||
package com.github.damontecres.wholphin.services
|
package com.github.damontecres.wholphin.services
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.asFlow
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.github.damontecres.wholphin.api.seerr.SeerrApiClient
|
||||||
import com.github.damontecres.wholphin.api.seerr.model.AuthJellyfinPostRequest
|
import com.github.damontecres.wholphin.api.seerr.model.AuthJellyfinPostRequest
|
||||||
import com.github.damontecres.wholphin.api.seerr.model.AuthLocalPostRequest
|
import com.github.damontecres.wholphin.api.seerr.model.AuthLocalPostRequest
|
||||||
|
import com.github.damontecres.wholphin.api.seerr.model.User
|
||||||
import com.github.damontecres.wholphin.data.SeerrServerDao
|
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.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.services.hilt.StandardOkHttpClient
|
||||||
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
|
import dagger.hilt.android.qualifiers.ActivityContext
|
||||||
|
import dagger.hilt.android.scopes.ActivityScoped
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
|
@ -21,6 +34,7 @@ class SeerrServerRepository
|
||||||
private val seerrApi: SeerrApi,
|
private val seerrApi: SeerrApi,
|
||||||
private val seerrServerDao: SeerrServerDao,
|
private val seerrServerDao: SeerrServerDao,
|
||||||
private val serverRepository: ServerRepository,
|
private val serverRepository: ServerRepository,
|
||||||
|
@param:StandardOkHttpClient private val okHttpClient: OkHttpClient,
|
||||||
) {
|
) {
|
||||||
private val _currentServer = MutableStateFlow<SeerrServer?>(null)
|
private val _currentServer = MutableStateFlow<SeerrServer?>(null)
|
||||||
val currentServer: StateFlow<SeerrServer?> = _currentServer
|
val currentServer: StateFlow<SeerrServer?> = _currentServer
|
||||||
|
|
@ -37,6 +51,24 @@ class SeerrServerRepository
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
_currentServer.update { null }
|
||||||
|
_currentUser.update { null }
|
||||||
|
seerrApi.update("", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun set(
|
||||||
|
server: SeerrServer,
|
||||||
|
user: SeerrUser,
|
||||||
|
) {
|
||||||
|
_currentServer.update {
|
||||||
|
_currentUser.update {
|
||||||
|
user
|
||||||
|
}
|
||||||
|
server
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun addAndChangeServer(
|
suspend fun addAndChangeServer(
|
||||||
url: String,
|
url: String,
|
||||||
apiKey: String,
|
apiKey: String,
|
||||||
|
|
@ -51,7 +83,7 @@ class SeerrServerRepository
|
||||||
// TODO test api key
|
// TODO test api key
|
||||||
val user =
|
val user =
|
||||||
SeerrUser(
|
SeerrUser(
|
||||||
jellyfinRowId = jellyfinUser.rowId,
|
jellyfinUserRowId = jellyfinUser.rowId,
|
||||||
serverId = server.id,
|
serverId = server.id,
|
||||||
authMethod = SeerrAuthMethod.API_KEY,
|
authMethod = SeerrAuthMethod.API_KEY,
|
||||||
username = null,
|
username = null,
|
||||||
|
|
@ -59,8 +91,6 @@ class SeerrServerRepository
|
||||||
credential = apiKey,
|
credential = apiKey,
|
||||||
)
|
)
|
||||||
seerrServerDao.addUser(user)
|
seerrServerDao.addUser(user)
|
||||||
// TODO user info
|
|
||||||
seerrApi.api.usersApi.authMeGet()
|
|
||||||
|
|
||||||
seerrApi.update(server.url, apiKey)
|
seerrApi.update(server.url, apiKey)
|
||||||
_currentServer.update {
|
_currentServer.update {
|
||||||
|
|
@ -86,42 +116,43 @@ class SeerrServerRepository
|
||||||
}
|
}
|
||||||
server?.server?.let { server ->
|
server?.server?.let { server ->
|
||||||
serverRepository.currentUser.value?.let { jellyfinUser ->
|
serverRepository.currentUser.value?.let { jellyfinUser ->
|
||||||
val response =
|
// TODO Need to update server early so that cookies are saved
|
||||||
when (authMethod) {
|
seerrApi.update(server.url, null)
|
||||||
SeerrAuthMethod.LOCAL -> {
|
login(seerrApi.api, authMethod, username, password)
|
||||||
seerrApi.api.authApi.authLocalPostWithHttpInfo(
|
|
||||||
AuthLocalPostRequest(
|
|
||||||
email = username,
|
|
||||||
password = password,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
SeerrAuthMethod.JELLYFIN -> {
|
|
||||||
seerrApi.api.authApi.authJellyfinPostWithHttpInfo(
|
|
||||||
AuthJellyfinPostRequest(
|
|
||||||
username = username,
|
|
||||||
password = password,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
SeerrAuthMethod.API_KEY -> {
|
|
||||||
throw IllegalArgumentException("Cannot authenticate with API key using this function")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
response.headers["Cookies"]
|
|
||||||
// TODO get session cookie
|
|
||||||
val user =
|
val user =
|
||||||
SeerrUser(
|
SeerrUser(
|
||||||
jellyfinRowId = jellyfinUser.rowId,
|
jellyfinUserRowId = jellyfinUser.rowId,
|
||||||
serverId = server.id,
|
serverId = server.id,
|
||||||
authMethod = SeerrAuthMethod.API_KEY,
|
authMethod = authMethod,
|
||||||
username = username,
|
username = username,
|
||||||
password = password,
|
password = password,
|
||||||
credential = null,
|
credential = null,
|
||||||
)
|
)
|
||||||
|
seerrServerDao.addUser(user)
|
||||||
|
_currentServer.update {
|
||||||
|
_currentUser.update {
|
||||||
|
user
|
||||||
}
|
}
|
||||||
|
server
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun testConnection(
|
||||||
|
authMethod: SeerrAuthMethod,
|
||||||
|
url: String,
|
||||||
|
username: String?,
|
||||||
|
passwordOrApiKey: String,
|
||||||
|
): LoadingState {
|
||||||
|
val api = SeerrApiClient(url, passwordOrApiKey, okHttpClient)
|
||||||
|
try {
|
||||||
|
login(api, authMethod, username, passwordOrApiKey)
|
||||||
|
return LoadingState.Success
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Timber.w(ex, "Error testing seerr connection")
|
||||||
|
return LoadingState.Error(ex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -130,3 +161,82 @@ data class CurrentSeerr(
|
||||||
val server: SeerrServer,
|
val server: SeerrServer,
|
||||||
val user: SeerrUser,
|
val user: SeerrUser,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private suspend fun login(
|
||||||
|
client: SeerrApiClient,
|
||||||
|
authMethod: SeerrAuthMethod,
|
||||||
|
username: String?,
|
||||||
|
password: String?,
|
||||||
|
): User =
|
||||||
|
when (authMethod) {
|
||||||
|
SeerrAuthMethod.LOCAL -> {
|
||||||
|
client.authApi.authLocalPost(
|
||||||
|
AuthLocalPostRequest(
|
||||||
|
email = username ?: "",
|
||||||
|
password = password ?: "",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
SeerrAuthMethod.JELLYFIN -> {
|
||||||
|
client.authApi.authJellyfinPost(
|
||||||
|
AuthJellyfinPostRequest(
|
||||||
|
username = username ?: "",
|
||||||
|
password = password ?: "",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
SeerrAuthMethod.API_KEY -> {
|
||||||
|
client.usersApi.authMeGet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ActivityScoped
|
||||||
|
class UserSwitchListener
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
@param:ActivityContext private val context: Context,
|
||||||
|
private val serverRepository: ServerRepository,
|
||||||
|
private val seerrServerRepository: SeerrServerRepository,
|
||||||
|
private val seerrServerDao: SeerrServerDao,
|
||||||
|
private val seerrApi: SeerrApi,
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
Timber.v("Launching")
|
||||||
|
context as AppCompatActivity
|
||||||
|
context.lifecycleScope.launchIO {
|
||||||
|
serverRepository.currentUser.asFlow().collect { user ->
|
||||||
|
Timber.v("New user")
|
||||||
|
seerrServerRepository.clear()
|
||||||
|
if (user != null) {
|
||||||
|
seerrServerDao
|
||||||
|
.getUsersByJellyfinUser(user.rowId)
|
||||||
|
.firstOrNull()
|
||||||
|
?.let { seerrUser ->
|
||||||
|
val server = seerrServerDao.getServer(seerrUser.serverId)?.server
|
||||||
|
if (server != null) {
|
||||||
|
Timber.i("Found a seerr user & server")
|
||||||
|
seerrApi.update(server.url, seerrUser.credential)
|
||||||
|
if (seerrUser.authMethod != SeerrAuthMethod.API_KEY) {
|
||||||
|
try {
|
||||||
|
login(
|
||||||
|
seerrApi.api,
|
||||||
|
seerrUser.authMethod,
|
||||||
|
seerrUser.username,
|
||||||
|
seerrUser.password,
|
||||||
|
)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Timber.w(ex, "Error logging into %s", server.url)
|
||||||
|
seerrServerRepository.clear()
|
||||||
|
return@let
|
||||||
|
}
|
||||||
|
}
|
||||||
|
seerrServerRepository.set(server, seerrUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import com.github.damontecres.wholphin.data.JellyfinServerDao
|
||||||
import com.github.damontecres.wholphin.data.LibraryDisplayInfoDao
|
import com.github.damontecres.wholphin.data.LibraryDisplayInfoDao
|
||||||
import com.github.damontecres.wholphin.data.Migrations
|
import com.github.damontecres.wholphin.data.Migrations
|
||||||
import com.github.damontecres.wholphin.data.PlaybackLanguageChoiceDao
|
import com.github.damontecres.wholphin.data.PlaybackLanguageChoiceDao
|
||||||
|
import com.github.damontecres.wholphin.data.SeerrServerDao
|
||||||
import com.github.damontecres.wholphin.data.ServerPreferencesDao
|
import com.github.damontecres.wholphin.data.ServerPreferencesDao
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferencesSerializer
|
import com.github.damontecres.wholphin.preferences.AppPreferencesSerializer
|
||||||
|
|
@ -61,6 +62,10 @@ object DatabaseModule {
|
||||||
@Singleton
|
@Singleton
|
||||||
fun playbackLanguageChoiceDao(db: AppDatabase): PlaybackLanguageChoiceDao = db.playbackLanguageChoiceDao()
|
fun playbackLanguageChoiceDao(db: AppDatabase): PlaybackLanguageChoiceDao = db.playbackLanguageChoiceDao()
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun seerrServerDao(db: AppDatabase): SeerrServerDao = db.seerrServerDao()
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun userPreferencesDataStore(
|
fun userPreferencesDataStore(
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
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.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
|
@ -58,6 +59,7 @@ import com.github.damontecres.wholphin.ui.playSoundOnFocus
|
||||||
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings
|
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings
|
||||||
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleStylePage
|
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleStylePage
|
||||||
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.showToast
|
import com.github.damontecres.wholphin.ui.showToast
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
|
|
@ -84,6 +86,8 @@ fun PreferencesContent(
|
||||||
|
|
||||||
val navDrawerPins by viewModel.navDrawerPins.observeAsState(mapOf())
|
val navDrawerPins by viewModel.navDrawerPins.observeAsState(mapOf())
|
||||||
var cacheUsage by remember { mutableStateOf(CacheUsage(0, 0, 0)) }
|
var cacheUsage by remember { mutableStateOf(CacheUsage(0, 0, 0)) }
|
||||||
|
val seerrIntegrationEnabled by viewModel.seerrEnabled.collectAsState(false)
|
||||||
|
var showSeerrServerDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.preferenceDataStore.data.collect {
|
viewModel.preferenceDataStore.data.collect {
|
||||||
|
|
@ -381,6 +385,22 @@ fun PreferencesContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AppPreference.SeerrIntegration -> {
|
||||||
|
ClickPreference(
|
||||||
|
title = stringResource(pref.title),
|
||||||
|
onClick = { showSeerrServerDialog = true },
|
||||||
|
modifier = Modifier,
|
||||||
|
summary =
|
||||||
|
if (seerrIntegrationEnabled) {
|
||||||
|
stringResource(R.string.enabled)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
onLongClick = {},
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
val value = pref.getter.invoke(preferences)
|
val value = pref.getter.invoke(preferences)
|
||||||
ComposablePreference(
|
ComposablePreference(
|
||||||
|
|
@ -440,6 +460,11 @@ fun PreferencesContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (showSeerrServerDialog) {
|
||||||
|
AddSeerServerDialog(
|
||||||
|
onDismissRequest = { showSeerrServerDialog = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.asFlow
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.github.damontecres.wholphin.data.NavDrawerItemRepository
|
import com.github.damontecres.wholphin.data.NavDrawerItemRepository
|
||||||
import com.github.damontecres.wholphin.data.ServerPreferencesDao
|
import com.github.damontecres.wholphin.data.ServerPreferencesDao
|
||||||
|
|
@ -17,6 +18,7 @@ import com.github.damontecres.wholphin.preferences.resetSubtitles
|
||||||
import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences
|
import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences
|
||||||
import com.github.damontecres.wholphin.services.BackdropService
|
import com.github.damontecres.wholphin.services.BackdropService
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
|
import com.github.damontecres.wholphin.services.SeerrServerRepository
|
||||||
import com.github.damontecres.wholphin.ui.detail.DebugViewModel.Companion.sendAppLogs
|
import com.github.damontecres.wholphin.ui.detail.DebugViewModel.Companion.sendAppLogs
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.nav.NavDrawerItem
|
import com.github.damontecres.wholphin.ui.nav.NavDrawerItem
|
||||||
|
|
@ -25,6 +27,7 @@ import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.RememberTabManager
|
import com.github.damontecres.wholphin.util.RememberTabManager
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
import org.jellyfin.sdk.model.ClientInfo
|
import org.jellyfin.sdk.model.ClientInfo
|
||||||
import org.jellyfin.sdk.model.DeviceInfo
|
import org.jellyfin.sdk.model.DeviceInfo
|
||||||
|
|
@ -43,6 +46,7 @@ class PreferencesViewModel
|
||||||
private val serverRepository: ServerRepository,
|
private val serverRepository: ServerRepository,
|
||||||
private val navDrawerItemRepository: NavDrawerItemRepository,
|
private val navDrawerItemRepository: NavDrawerItemRepository,
|
||||||
private val serverPreferencesDao: ServerPreferencesDao,
|
private val serverPreferencesDao: ServerPreferencesDao,
|
||||||
|
private val seerrServerRepository: SeerrServerRepository,
|
||||||
private val deviceInfo: DeviceInfo,
|
private val deviceInfo: DeviceInfo,
|
||||||
private val clientInfo: ClientInfo,
|
private val clientInfo: ClientInfo,
|
||||||
) : ViewModel(),
|
) : ViewModel(),
|
||||||
|
|
@ -52,6 +56,11 @@ class PreferencesViewModel
|
||||||
|
|
||||||
val currentUser get() = serverRepository.currentUser
|
val currentUser get() = serverRepository.currentUser
|
||||||
|
|
||||||
|
val seerrEnabled =
|
||||||
|
seerrServerRepository.currentUser.combine(currentUser.asFlow()) { seerrUser, jellyfinUser ->
|
||||||
|
seerrUser != null && jellyfinUser != null && seerrUser.jellyfinUserRowId == jellyfinUser.rowId
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
serverRepository.currentUser.value?.let { user ->
|
serverRepository.currentUser.value?.let { user ->
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
|
|
@ -54,7 +53,7 @@ class SeerrDiscoverViewModel
|
||||||
sort = MediaApi.SortMediaGet.MEDIA_ADDED,
|
sort = MediaApi.SortMediaGet.MEDIA_ADDED,
|
||||||
).results
|
).results
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
Timber.v(tv.firstOrNull()?.jellyfinMediaId)
|
// Timber.v(tv.firstOrNull()?.jellyfinMediaId)
|
||||||
}
|
}
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
val movies = seerrService.discoverMovies()
|
val movies = seerrService.discoverMovies()
|
||||||
|
|
|
||||||
|
|
@ -31,14 +31,15 @@ import com.github.damontecres.wholphin.ui.components.EditTextBox
|
||||||
import com.github.damontecres.wholphin.ui.components.TextButton
|
import com.github.damontecres.wholphin.ui.components.TextButton
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AddSeerrServerContent(
|
fun AddSeerrServerApiKey(
|
||||||
onSubmit: (url: String, apiKey: String) -> Unit,
|
onSubmit: (url: String, apiKey: String) -> Unit,
|
||||||
errorMessage: String?,
|
status: LoadingState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
var error by remember(errorMessage) { mutableStateOf(errorMessage) }
|
var error by remember(status) { mutableStateOf((status as? LoadingState.Error)?.localizedMessage) }
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier =
|
modifier =
|
||||||
|
|
@ -135,3 +136,141 @@ fun AddSeerrServerContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AddSeerrServerUsername(
|
||||||
|
onSubmit: (url: String, username: String, password: String) -> Unit,
|
||||||
|
username: String,
|
||||||
|
status: LoadingState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
var error by remember(status) { mutableStateOf((status as? LoadingState.Error)?.localizedMessage) }
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier =
|
||||||
|
modifier
|
||||||
|
.focusGroup()
|
||||||
|
.padding(16.dp)
|
||||||
|
.wrapContentSize(),
|
||||||
|
) {
|
||||||
|
var url by remember { mutableStateOf("") }
|
||||||
|
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() }
|
||||||
|
Text(
|
||||||
|
text = "Enter URL, username, & password",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "URL",
|
||||||
|
modifier = Modifier.padding(end = 8.dp),
|
||||||
|
)
|
||||||
|
EditTextBox(
|
||||||
|
value = url,
|
||||||
|
onValueChange = {
|
||||||
|
error = null
|
||||||
|
url = it
|
||||||
|
},
|
||||||
|
keyboardOptions =
|
||||||
|
KeyboardOptions(
|
||||||
|
capitalization = KeyboardCapitalization.None,
|
||||||
|
autoCorrectEnabled = false,
|
||||||
|
keyboardType = KeyboardType.Text,
|
||||||
|
imeAction = ImeAction.Next,
|
||||||
|
),
|
||||||
|
keyboardActions =
|
||||||
|
KeyboardActions(
|
||||||
|
onNext = {
|
||||||
|
usernameFocusRequester.tryRequestFocus()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
isInputValid = { true },
|
||||||
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Username",
|
||||||
|
modifier = Modifier.padding(end = 8.dp),
|
||||||
|
)
|
||||||
|
EditTextBox(
|
||||||
|
value = username,
|
||||||
|
onValueChange = {
|
||||||
|
error = null
|
||||||
|
username = it
|
||||||
|
},
|
||||||
|
keyboardOptions =
|
||||||
|
KeyboardOptions(
|
||||||
|
capitalization = KeyboardCapitalization.None,
|
||||||
|
autoCorrectEnabled = false,
|
||||||
|
keyboardType = KeyboardType.Text,
|
||||||
|
imeAction = ImeAction.Next,
|
||||||
|
),
|
||||||
|
keyboardActions =
|
||||||
|
KeyboardActions(
|
||||||
|
onNext = {
|
||||||
|
passwordFocusRequester.tryRequestFocus()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
isInputValid = { true },
|
||||||
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Password",
|
||||||
|
modifier = Modifier.padding(end = 8.dp),
|
||||||
|
)
|
||||||
|
EditTextBox(
|
||||||
|
value = password,
|
||||||
|
onValueChange = {
|
||||||
|
error = null
|
||||||
|
password = it
|
||||||
|
},
|
||||||
|
keyboardOptions =
|
||||||
|
KeyboardOptions(
|
||||||
|
capitalization = KeyboardCapitalization.None,
|
||||||
|
autoCorrectEnabled = false,
|
||||||
|
keyboardType = KeyboardType.Password,
|
||||||
|
imeAction = ImeAction.Go,
|
||||||
|
),
|
||||||
|
keyboardActions =
|
||||||
|
KeyboardActions(
|
||||||
|
onGo = { onSubmit.invoke(url, username, password) },
|
||||||
|
),
|
||||||
|
isInputValid = { true },
|
||||||
|
modifier = Modifier.focusRequester(passwordFocusRequester),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
error?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TextButton(
|
||||||
|
stringRes = R.string.submit,
|
||||||
|
onClick = { onSubmit.invoke(url, username, password) },
|
||||||
|
enabled = error.isNullOrBlank() && url.isNotNullOrBlank() && username.isNotNullOrBlank() && password.isNotNullOrBlank(),
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.setup.seerr
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import com.github.damontecres.wholphin.data.model.SeerrAuthMethod
|
||||||
|
import com.github.damontecres.wholphin.ui.components.BasicDialog
|
||||||
|
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.util.LoadingState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AddSeerServerDialog(
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
viewModel: SwitchSeerrViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
val currentUser by viewModel.currentUser.observeAsState()
|
||||||
|
var authMethod by remember { mutableStateOf<SeerrAuthMethod?>(null) }
|
||||||
|
val status by viewModel.serverConnectionStatus.collectAsState(LoadingState.Pending)
|
||||||
|
LaunchedEffect(status) {
|
||||||
|
if (status is LoadingState.Success) {
|
||||||
|
onDismissRequest.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
when (val auth = authMethod) {
|
||||||
|
SeerrAuthMethod.LOCAL,
|
||||||
|
SeerrAuthMethod.JELLYFIN,
|
||||||
|
-> {
|
||||||
|
BasicDialog(
|
||||||
|
onDismissRequest = { authMethod = null },
|
||||||
|
) {
|
||||||
|
AddSeerrServerUsername(
|
||||||
|
onSubmit = { url, username, password ->
|
||||||
|
viewModel.submitServer(
|
||||||
|
url,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
auth,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
username = currentUser?.name ?: "",
|
||||||
|
status = status,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SeerrAuthMethod.API_KEY -> {
|
||||||
|
BasicDialog(
|
||||||
|
onDismissRequest = { authMethod = null },
|
||||||
|
) {
|
||||||
|
AddSeerrServerApiKey(
|
||||||
|
onSubmit = { url, apiKey -> viewModel.submitServer(url, apiKey) },
|
||||||
|
status = status,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
null -> {
|
||||||
|
ChooseSeerrLoginType(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
onChoose = { authMethod = it },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChooseSeerrLoginType(
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onChoose: (SeerrAuthMethod) -> Unit,
|
||||||
|
) {
|
||||||
|
val params =
|
||||||
|
remember {
|
||||||
|
DialogParams(
|
||||||
|
fromLongClick = false,
|
||||||
|
title = "Login to Seerr server",
|
||||||
|
items =
|
||||||
|
listOf(
|
||||||
|
DialogItem(
|
||||||
|
text = "API Key",
|
||||||
|
onClick = { onChoose.invoke(SeerrAuthMethod.API_KEY) },
|
||||||
|
),
|
||||||
|
DialogItem(
|
||||||
|
text = "Jellyfin user",
|
||||||
|
onClick = { onChoose.invoke(SeerrAuthMethod.JELLYFIN) },
|
||||||
|
),
|
||||||
|
DialogItem(
|
||||||
|
text = "Local user",
|
||||||
|
onClick = { onChoose.invoke(SeerrAuthMethod.LOCAL) },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DialogPopup(
|
||||||
|
params = params,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
dismissOnClick = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,18 @@
|
||||||
package com.github.damontecres.wholphin.ui.setup.seerr
|
package com.github.damontecres.wholphin.ui.setup.seerr
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
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.SeerrServerRepository
|
||||||
import com.github.damontecres.wholphin.services.SeerrService
|
import com.github.damontecres.wholphin.services.SeerrService
|
||||||
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import jakarta.inject.Inject
|
import jakarta.inject.Inject
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SwitchSeerrViewModel
|
class SwitchSeerrViewModel
|
||||||
|
|
@ -12,10 +20,65 @@ class SwitchSeerrViewModel
|
||||||
constructor(
|
constructor(
|
||||||
private val seerrServerRepository: SeerrServerRepository,
|
private val seerrServerRepository: SeerrServerRepository,
|
||||||
private val seerrService: SeerrService,
|
private val seerrService: SeerrService,
|
||||||
|
private val serverRepository: ServerRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
val currentUser = serverRepository.currentUser
|
||||||
|
|
||||||
|
val serverConnectionStatus = MutableStateFlow<LoadingState>(LoadingState.Pending)
|
||||||
|
|
||||||
|
private fun cleanUrl(url: String) =
|
||||||
|
if (!url.endsWith("/api/v1")) {
|
||||||
|
url
|
||||||
|
.toHttpUrlOrNull()
|
||||||
|
?.newBuilder()
|
||||||
|
?.apply {
|
||||||
|
addPathSegment("api")
|
||||||
|
addPathSegment("v1")
|
||||||
|
}?.build()
|
||||||
|
.toString()
|
||||||
|
} else {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
|
||||||
fun submitServer(
|
fun submitServer(
|
||||||
url: String,
|
url: String,
|
||||||
apiKey: String,
|
apiKey: String,
|
||||||
) {
|
) {
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
val url = cleanUrl(url)
|
||||||
|
val result =
|
||||||
|
seerrServerRepository.testConnection(
|
||||||
|
authMethod = SeerrAuthMethod.API_KEY,
|
||||||
|
url = url,
|
||||||
|
username = null,
|
||||||
|
passwordOrApiKey = apiKey,
|
||||||
|
)
|
||||||
|
if (result is LoadingState.Success) {
|
||||||
|
seerrServerRepository.addAndChangeServer(url, apiKey)
|
||||||
|
}
|
||||||
|
serverConnectionStatus.update { result }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun submitServer(
|
||||||
|
url: String,
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
authMethod: SeerrAuthMethod,
|
||||||
|
) {
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
val url = cleanUrl(url)
|
||||||
|
val result =
|
||||||
|
seerrServerRepository.testConnection(
|
||||||
|
authMethod = authMethod,
|
||||||
|
url = url,
|
||||||
|
username = username,
|
||||||
|
passwordOrApiKey = password,
|
||||||
|
)
|
||||||
|
if (result is LoadingState.Success) {
|
||||||
|
seerrServerRepository.addAndChangeServer(url, authMethod, username, password)
|
||||||
|
}
|
||||||
|
serverConnectionStatus.update { result }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -402,6 +402,7 @@
|
||||||
<string name="discover">Discover</string>
|
<string name="discover">Discover</string>
|
||||||
<string name="request">Request</string>
|
<string name="request">Request</string>
|
||||||
<string name="pending">Pending</string>
|
<string name="pending">Pending</string>
|
||||||
|
<string name="seerr_integration">Seerr integration</string>
|
||||||
|
|
||||||
<string-array name="theme_song_volume">
|
<string-array name="theme_song_volume">
|
||||||
<item>Disabled</item>
|
<item>Disabled</item>
|
||||||
|
|
|
||||||
|
|
@ -135,9 +135,6 @@ components:
|
||||||
readOnly: true
|
readOnly: true
|
||||||
required:
|
required:
|
||||||
- id
|
- id
|
||||||
- email
|
|
||||||
- createdAt
|
|
||||||
- updatedAt
|
|
||||||
UserSettings:
|
UserSettings:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue