mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Support switching users & servers
This commit is contained in:
parent
d3eeb67877
commit
d7dde352bc
16 changed files with 787 additions and 226 deletions
|
|
@ -19,14 +19,18 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.RectangleShape
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.navigation3.runtime.rememberNavBackStack
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Surface
|
import androidx.tv.material3.Surface
|
||||||
import com.github.damontecres.dolphin.data.ServerRepository
|
import com.github.damontecres.dolphin.data.ServerRepository
|
||||||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.dolphin.ui.CoilConfig
|
import com.github.damontecres.dolphin.ui.CoilConfig
|
||||||
import com.github.damontecres.dolphin.ui.ServerLoginPage
|
|
||||||
import com.github.damontecres.dolphin.ui.nav.ApplicationContent
|
import com.github.damontecres.dolphin.ui.nav.ApplicationContent
|
||||||
|
import com.github.damontecres.dolphin.ui.nav.Destination
|
||||||
|
import com.github.damontecres.dolphin.ui.nav.NavigationManager
|
||||||
|
import com.github.damontecres.dolphin.ui.setup.SwitchServerContent
|
||||||
|
import com.github.damontecres.dolphin.ui.setup.SwitchUserContent
|
||||||
import com.github.damontecres.dolphin.ui.theme.DolphinTheme
|
import com.github.damontecres.dolphin.ui.theme.DolphinTheme
|
||||||
import com.github.damontecres.dolphin.util.profile.createDeviceProfile
|
import com.github.damontecres.dolphin.util.profile.createDeviceProfile
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
|
@ -69,13 +73,19 @@ class MainActivity : AppCompatActivity() {
|
||||||
isRestoringSession = false
|
isRestoringSession = false
|
||||||
}
|
}
|
||||||
val deviceProfile =
|
val deviceProfile =
|
||||||
remember {
|
remember(preferences) {
|
||||||
createDeviceProfile(this@MainActivity, preferences, false)
|
createDeviceProfile(this@MainActivity, preferences, false)
|
||||||
}
|
}
|
||||||
val server = serverRepository.currentServer
|
val server = serverRepository.currentServer
|
||||||
val user = serverRepository.currentUser
|
val user = serverRepository.currentUser
|
||||||
|
val backStack = rememberNavBackStack(Destination.Main)
|
||||||
|
val navigationManager = NavigationManager(backStack)
|
||||||
|
|
||||||
if (server != null && user != null) {
|
if (server != null && user != null) {
|
||||||
ApplicationContent(
|
ApplicationContent(
|
||||||
|
user = user,
|
||||||
|
server = server,
|
||||||
|
navigationManager = navigationManager,
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
deviceProfile = deviceProfile,
|
deviceProfile = deviceProfile,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
|
@ -90,8 +100,17 @@ class MainActivity : AppCompatActivity() {
|
||||||
modifier = Modifier.align(Alignment.Center),
|
modifier = Modifier.align(Alignment.Center),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} else if (server != null) {
|
||||||
|
// Have server but no user, go to user selection
|
||||||
|
SwitchUserContent(
|
||||||
|
navigationManager = navigationManager,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
ServerLoginPage(modifier = Modifier.fillMaxSize())
|
SwitchServerContent(
|
||||||
|
navigationManager = navigationManager,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ data class JellyfinServerUsers(
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface JellyfinServerDao {
|
interface JellyfinServerDao {
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
fun addServer(vararg servers: JellyfinServer)
|
fun addServer(vararg servers: JellyfinServer)
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,14 @@ package com.github.damontecres.dolphin.data
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
import org.jellyfin.sdk.api.client.exception.InvalidStatusException
|
import org.jellyfin.sdk.api.client.exception.InvalidStatusException
|
||||||
import org.jellyfin.sdk.api.client.extensions.userApi
|
import org.jellyfin.sdk.api.client.extensions.userApi
|
||||||
|
import org.jellyfin.sdk.model.api.AuthenticationResult
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
@ -18,6 +21,7 @@ class ServerRepository
|
||||||
constructor(
|
constructor(
|
||||||
val serverDao: JellyfinServerDao,
|
val serverDao: JellyfinServerDao,
|
||||||
val apiClient: ApiClient,
|
val apiClient: ApiClient,
|
||||||
|
val userPreferencesDataStore: DataStore<UserPreferences>,
|
||||||
) {
|
) {
|
||||||
private var _currentServer by mutableStateOf<JellyfinServer?>(null)
|
private var _currentServer by mutableStateOf<JellyfinServer?>(null)
|
||||||
val currentServer get() = _currentServer
|
val currentServer get() = _currentServer
|
||||||
|
|
@ -103,4 +107,44 @@ class ServerRepository
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun changeUser(
|
||||||
|
serverUrl: String,
|
||||||
|
authenticationResult: AuthenticationResult,
|
||||||
|
) {
|
||||||
|
val accessToken = authenticationResult.accessToken
|
||||||
|
if (accessToken != null) {
|
||||||
|
val authedUser = authenticationResult.user
|
||||||
|
val server =
|
||||||
|
authenticationResult.serverId?.let {
|
||||||
|
JellyfinServer(
|
||||||
|
id = it,
|
||||||
|
name = authedUser?.serverName,
|
||||||
|
url = serverUrl,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (server != null) {
|
||||||
|
val user =
|
||||||
|
authedUser?.let {
|
||||||
|
JellyfinUser(
|
||||||
|
id = it.id.toString(),
|
||||||
|
name = it.name,
|
||||||
|
serverId = server.id,
|
||||||
|
accessToken = accessToken,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (user != null) {
|
||||||
|
userPreferencesDataStore.updateData {
|
||||||
|
it
|
||||||
|
.toBuilder()
|
||||||
|
.apply {
|
||||||
|
currentServerId = server.id
|
||||||
|
currentUserId = user.id
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
addAndChangeUser(server, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,160 +0,0 @@
|
||||||
package com.github.damontecres.dolphin.ui
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.TextField
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.datastore.core.DataStore
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import androidx.tv.material3.Button
|
|
||||||
import androidx.tv.material3.Text
|
|
||||||
import com.github.damontecres.dolphin.data.JellyfinServer
|
|
||||||
import com.github.damontecres.dolphin.data.JellyfinUser
|
|
||||||
import com.github.damontecres.dolphin.data.ServerRepository
|
|
||||||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.jellyfin.sdk.Jellyfin
|
|
||||||
import org.jellyfin.sdk.api.client.exception.InvalidStatusException
|
|
||||||
import org.jellyfin.sdk.api.client.extensions.authenticateUserByName
|
|
||||||
import org.jellyfin.sdk.api.client.extensions.userApi
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class ServerManagementViewModel
|
|
||||||
@Inject
|
|
||||||
constructor(
|
|
||||||
val serverRepository: ServerRepository,
|
|
||||||
val userPreferencesDataStore: DataStore<UserPreferences>,
|
|
||||||
val jellyfin: Jellyfin,
|
|
||||||
) : ViewModel() {
|
|
||||||
fun addServer() {
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun loginWithPassword(
|
|
||||||
serverUrl: String,
|
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
val api = jellyfin.createApi(baseUrl = serverUrl)
|
|
||||||
val authenticationResult by api.userApi.authenticateUserByName(
|
|
||||||
username = username,
|
|
||||||
password = password,
|
|
||||||
)
|
|
||||||
|
|
||||||
val accessToken = authenticationResult.accessToken
|
|
||||||
if (accessToken != null) {
|
|
||||||
val authedUser = authenticationResult.user
|
|
||||||
val server =
|
|
||||||
authenticationResult.serverId?.let {
|
|
||||||
JellyfinServer(
|
|
||||||
id = it,
|
|
||||||
name = authedUser?.serverName,
|
|
||||||
url = serverUrl,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (server != null) {
|
|
||||||
val user =
|
|
||||||
authedUser?.let {
|
|
||||||
JellyfinUser(
|
|
||||||
id = it.id.toString(),
|
|
||||||
name = it.name,
|
|
||||||
serverId = server.id,
|
|
||||||
accessToken = accessToken,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (user != null) {
|
|
||||||
userPreferencesDataStore.updateData {
|
|
||||||
it
|
|
||||||
.toBuilder()
|
|
||||||
.apply {
|
|
||||||
currentServerId = server.id
|
|
||||||
currentUserId = user.id
|
|
||||||
}.build()
|
|
||||||
}
|
|
||||||
serverRepository.addAndChangeUser(server, user)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print session information
|
|
||||||
println(authenticationResult.sessionInfo)
|
|
||||||
} catch (err: InvalidStatusException) {
|
|
||||||
if (err.status == 401) {
|
|
||||||
// Username or password is incorrect
|
|
||||||
println("Invalid user")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ServerLoginPage(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
viewModel: ServerManagementViewModel = viewModel(),
|
|
||||||
) {
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
ServerLoginForm(
|
|
||||||
modifier = modifier,
|
|
||||||
onSubmit = { serverUrl, username, password ->
|
|
||||||
scope.launch {
|
|
||||||
// Handle form submission
|
|
||||||
viewModel.loginWithPassword(serverUrl, username, password)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ServerLoginForm(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
onSubmit: (serverUrl: String, username: String, password: String) -> Unit,
|
|
||||||
) {
|
|
||||||
var serverUrl by remember { mutableStateOf("") }
|
|
||||||
var username by remember { mutableStateOf("") }
|
|
||||||
var password by remember { mutableStateOf("") }
|
|
||||||
|
|
||||||
Column(modifier = modifier.padding(16.dp)) {
|
|
||||||
TextField(
|
|
||||||
value = serverUrl,
|
|
||||||
onValueChange = { serverUrl = it },
|
|
||||||
label = { Text("Server URL") },
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
TextField(
|
|
||||||
value = username,
|
|
||||||
onValueChange = { username = it },
|
|
||||||
label = { Text("Username") },
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
TextField(
|
|
||||||
value = password,
|
|
||||||
onValueChange = { password = it },
|
|
||||||
label = { Text("Password") },
|
|
||||||
visualTransformation = PasswordVisualTransformation(),
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
Button(
|
|
||||||
onClick = { onSubmit(serverUrl, username, password) },
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
Text("Submit")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
package com.github.damontecres.dolphin.ui
|
package com.github.damontecres.dolphin.ui
|
||||||
|
|
||||||
|
import android.content.res.Configuration.UI_MODE_TYPE_TELEVISION
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.Font
|
import androidx.compose.ui.text.font.Font
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import com.github.damontecres.dolphin.R
|
import com.github.damontecres.dolphin.R
|
||||||
import org.jellyfin.sdk.model.api.ItemFields
|
import org.jellyfin.sdk.model.api.ItemFields
|
||||||
|
|
||||||
|
|
@ -24,3 +26,10 @@ val DefaultItemFields =
|
||||||
ItemFields.SEASON_USER_DATA,
|
ItemFields.SEASON_USER_DATA,
|
||||||
ItemFields.CHILD_COUNT,
|
ItemFields.CHILD_COUNT,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Preview(
|
||||||
|
device = "spec:parent=tv_1080p",
|
||||||
|
backgroundColor = 0xFF383535,
|
||||||
|
uiMode = UI_MODE_TYPE_TELEVISION,
|
||||||
|
)
|
||||||
|
annotation class PreviewTvSpec
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.focusable
|
import androidx.compose.foundation.focusable
|
||||||
import androidx.compose.foundation.gestures.scrollBy
|
import androidx.compose.foundation.gestures.scrollBy
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.BoxScope
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
|
@ -46,8 +47,6 @@ import androidx.tv.material3.surfaceColorAtElevation
|
||||||
import com.github.damontecres.dolphin.ui.FontAwesome
|
import com.github.damontecres.dolphin.ui.FontAwesome
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.time.Duration.Companion.minutes
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
|
||||||
|
|
||||||
sealed interface DialogItemEntry
|
sealed interface DialogItemEntry
|
||||||
|
|
||||||
|
|
@ -266,33 +265,24 @@ fun ScrollableDialog(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MarkerDurationDialog(
|
fun BasicDialog(
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onClick: (Long) -> Unit,
|
properties: DialogProperties = DialogProperties(),
|
||||||
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
val dialogItems =
|
Dialog(
|
||||||
remember {
|
|
||||||
listOf(
|
|
||||||
15.seconds,
|
|
||||||
20.seconds,
|
|
||||||
30.seconds,
|
|
||||||
60.seconds,
|
|
||||||
5.minutes,
|
|
||||||
10.minutes,
|
|
||||||
20.minutes,
|
|
||||||
).map {
|
|
||||||
DialogItem(it.toString()) {
|
|
||||||
onClick.invoke(it.inWholeMilliseconds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DialogPopup(
|
|
||||||
showDialog = true,
|
|
||||||
title = "How long?",
|
|
||||||
dialogItems = dialogItems,
|
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
dismissOnClick = false,
|
properties = properties,
|
||||||
waitToLoad = false,
|
) {
|
||||||
properties = DialogProperties(),
|
Box(
|
||||||
)
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,40 +5,26 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||||
import androidx.navigation3.runtime.NavEntry
|
import androidx.navigation3.runtime.NavEntry
|
||||||
import androidx.navigation3.runtime.rememberNavBackStack
|
|
||||||
import androidx.navigation3.runtime.rememberSavedStateNavEntryDecorator
|
import androidx.navigation3.runtime.rememberSavedStateNavEntryDecorator
|
||||||
import androidx.navigation3.scene.rememberSceneSetupNavEntryDecorator
|
import androidx.navigation3.scene.rememberSceneSetupNavEntryDecorator
|
||||||
import androidx.navigation3.ui.NavDisplay
|
import androidx.navigation3.ui.NavDisplay
|
||||||
|
import com.github.damontecres.dolphin.data.JellyfinServer
|
||||||
|
import com.github.damontecres.dolphin.data.JellyfinUser
|
||||||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||||
import org.jellyfin.sdk.model.api.DeviceProfile
|
import org.jellyfin.sdk.model.api.DeviceProfile
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ApplicationContent(
|
fun ApplicationContent(
|
||||||
|
server: JellyfinServer,
|
||||||
|
user: JellyfinUser,
|
||||||
|
navigationManager: NavigationManager,
|
||||||
preferences: UserPreferences,
|
preferences: UserPreferences,
|
||||||
deviceProfile: DeviceProfile,
|
deviceProfile: DeviceProfile,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val backStack = rememberNavBackStack(Destination.Main)
|
|
||||||
|
|
||||||
val navigationManager =
|
|
||||||
object : NavigationManager {
|
|
||||||
override fun navigateTo(destination: Destination) {
|
|
||||||
backStack.add(destination)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun goBack() {
|
|
||||||
backStack.removeLastOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun goToHome() {
|
|
||||||
backStack.clear()
|
|
||||||
backStack.add(Destination.Main)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NavDisplay(
|
NavDisplay(
|
||||||
backStack = backStack,
|
backStack = navigationManager.backStack,
|
||||||
onBack = { backStack.removeLastOrNull() },
|
onBack = { navigationManager.goBack() },
|
||||||
entryDecorators =
|
entryDecorators =
|
||||||
listOf(
|
listOf(
|
||||||
rememberSceneSetupNavEntryDecorator(),
|
rememberSceneSetupNavEntryDecorator(),
|
||||||
|
|
@ -46,8 +32,9 @@ fun ApplicationContent(
|
||||||
rememberViewModelStoreNavEntryDecorator(),
|
rememberViewModelStoreNavEntryDecorator(),
|
||||||
),
|
),
|
||||||
entryProvider = { key ->
|
entryProvider = { key ->
|
||||||
NavEntry(key) {
|
|
||||||
key as Destination
|
key as Destination
|
||||||
|
val contentKey = "${key}_${server.id}_${user.id}"
|
||||||
|
NavEntry(key, contentKey = contentKey) {
|
||||||
if (key.fullScreen) {
|
if (key.fullScreen) {
|
||||||
DestinationContent(
|
DestinationContent(
|
||||||
destination = key,
|
destination = key,
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,12 @@ sealed class Destination(
|
||||||
@Serializable
|
@Serializable
|
||||||
data object Setup : Destination(true)
|
data object Setup : Destination(true)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object ServerList : Destination(true)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object UserList : Destination(true)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object Main : Destination()
|
data object Main : Destination()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.dolphin.ui.detail.MediaItemContent
|
import com.github.damontecres.dolphin.ui.detail.MediaItemContent
|
||||||
import com.github.damontecres.dolphin.ui.main.MainPage
|
import com.github.damontecres.dolphin.ui.main.MainPage
|
||||||
import com.github.damontecres.dolphin.ui.playback.PlaybackContent
|
import com.github.damontecres.dolphin.ui.playback.PlaybackContent
|
||||||
|
import com.github.damontecres.dolphin.ui.setup.SwitchServerContent
|
||||||
|
import com.github.damontecres.dolphin.ui.setup.SwitchUserContent
|
||||||
import org.jellyfin.sdk.model.api.DeviceProfile
|
import org.jellyfin.sdk.model.api.DeviceProfile
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -17,23 +19,22 @@ fun DestinationContent(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
when (destination) {
|
when (destination) {
|
||||||
Destination.Main -> {
|
Destination.Main ->
|
||||||
MainPage(
|
MainPage(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
navigationManager = navigationManager,
|
navigationManager = navigationManager,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
is Destination.MediaItem -> {
|
is Destination.MediaItem ->
|
||||||
MediaItemContent(
|
MediaItemContent(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
navigationManager = navigationManager,
|
navigationManager = navigationManager,
|
||||||
destination = destination,
|
destination = destination,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
is Destination.Playback -> {
|
is Destination.Playback ->
|
||||||
PlaybackContent(
|
PlaybackContent(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
navigationManager = navigationManager,
|
navigationManager = navigationManager,
|
||||||
|
|
@ -41,7 +42,10 @@ fun DestinationContent(
|
||||||
destination = destination,
|
destination = destination,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
Destination.ServerList -> SwitchServerContent(navigationManager, modifier)
|
||||||
|
Destination.UserList -> SwitchUserContent(navigationManager, modifier)
|
||||||
|
|
||||||
Destination.Search -> TODO()
|
Destination.Search -> TODO()
|
||||||
Destination.Settings -> TODO()
|
Destination.Settings -> TODO()
|
||||||
Destination.Setup -> TODO()
|
Destination.Setup -> TODO()
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ fun NavDrawer(
|
||||||
IconNavItem(
|
IconNavItem(
|
||||||
text = user?.name ?: "",
|
text = user?.name ?: "",
|
||||||
icon = Icons.Default.AccountCircle,
|
icon = Icons.Default.AccountCircle,
|
||||||
onClick = { navigationManager.navigateTo(Destination.Setup) },
|
onClick = { navigationManager.navigateTo(Destination.UserList) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,21 @@
|
||||||
package com.github.damontecres.dolphin.ui.nav
|
package com.github.damontecres.dolphin.ui.nav
|
||||||
|
|
||||||
interface NavigationManager {
|
import androidx.navigation3.runtime.NavBackStack
|
||||||
fun navigateTo(destination: Destination)
|
import androidx.navigation3.runtime.NavKey
|
||||||
|
|
||||||
fun goBack()
|
class NavigationManager(
|
||||||
|
val backStack: NavBackStack<NavKey>,
|
||||||
fun goToHome()
|
) {
|
||||||
|
fun navigateTo(destination: Destination) {
|
||||||
|
backStack.add(destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun goBack() {
|
||||||
|
backStack.removeLastOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun goToHome() {
|
||||||
|
backStack.clear()
|
||||||
|
backStack.add(Destination.Main)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
package com.github.damontecres.dolphin.ui.setup
|
||||||
|
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Warning
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.tv.material3.Icon
|
||||||
|
import androidx.tv.material3.ListItem
|
||||||
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.dolphin.data.JellyfinServer
|
||||||
|
import com.github.damontecres.dolphin.ui.components.CircularProgress
|
||||||
|
|
||||||
|
sealed interface ServerConnectionStatus {
|
||||||
|
object Success : ServerConnectionStatus
|
||||||
|
|
||||||
|
object Pending : ServerConnectionStatus
|
||||||
|
|
||||||
|
data class Error(
|
||||||
|
val message: String?,
|
||||||
|
) : ServerConnectionStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ServerList(
|
||||||
|
servers: List<JellyfinServer>,
|
||||||
|
connectionStatus: Map<String, ServerConnectionStatus>,
|
||||||
|
onSwitchServer: (JellyfinServer) -> Unit,
|
||||||
|
onAddServer: () -> Unit,
|
||||||
|
onRemoveServer: (JellyfinServer) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
LazyColumn(modifier = modifier) {
|
||||||
|
items(servers) { server ->
|
||||||
|
val status = connectionStatus[server.id] ?: ServerConnectionStatus.Pending
|
||||||
|
ListItem(
|
||||||
|
enabled = status == ServerConnectionStatus.Success,
|
||||||
|
selected = false,
|
||||||
|
headlineContent = { Text(text = server.name ?: server.id) },
|
||||||
|
supportingContent = { Text(text = server.url) },
|
||||||
|
leadingContent = {
|
||||||
|
when (status) {
|
||||||
|
ServerConnectionStatus.Success -> {}
|
||||||
|
ServerConnectionStatus.Pending -> {
|
||||||
|
CircularProgress()
|
||||||
|
}
|
||||||
|
is ServerConnectionStatus.Error -> {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Warning,
|
||||||
|
contentDescription = status.message,
|
||||||
|
tint = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClick = { onSwitchServer.invoke(server) },
|
||||||
|
onLongClick = {
|
||||||
|
// TODO dialog to remove server
|
||||||
|
},
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
HorizontalDivider()
|
||||||
|
ListItem(
|
||||||
|
enabled = true,
|
||||||
|
selected = false,
|
||||||
|
headlineContent = { Text(text = "Add User") },
|
||||||
|
supportingContent = { },
|
||||||
|
onClick = { onAddServer.invoke() },
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
package com.github.damontecres.dolphin.ui.setup
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
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.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import androidx.tv.material3.Button
|
||||||
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.dolphin.ui.components.BasicDialog
|
||||||
|
import com.github.damontecres.dolphin.ui.components.EditTextBox
|
||||||
|
import com.github.damontecres.dolphin.ui.nav.Destination
|
||||||
|
import com.github.damontecres.dolphin.ui.nav.NavigationManager
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SwitchServerContent(
|
||||||
|
navigationManager: NavigationManager,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: SwitchUserViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
val currentServer = viewModel.serverRepository.currentServer
|
||||||
|
val servers by viewModel.servers.observeAsState(listOf())
|
||||||
|
val serverStatus by viewModel.serverStatus.observeAsState(mapOf())
|
||||||
|
|
||||||
|
var showAddServer by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Select Server",
|
||||||
|
)
|
||||||
|
ServerList(
|
||||||
|
servers = servers,
|
||||||
|
connectionStatus = serverStatus,
|
||||||
|
onSwitchServer = {},
|
||||||
|
onAddServer = {
|
||||||
|
},
|
||||||
|
onRemoveServer = {
|
||||||
|
// TODO
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(.5f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showAddServer) {
|
||||||
|
var url by remember { mutableStateOf("") }
|
||||||
|
val submit = {
|
||||||
|
showAddServer = false
|
||||||
|
viewModel.addServer(url) {
|
||||||
|
navigationManager.navigateTo(Destination.UserList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BasicDialog(
|
||||||
|
onDismissRequest = { showAddServer = false },
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Enter Server URL",
|
||||||
|
)
|
||||||
|
EditTextBox(
|
||||||
|
value = url,
|
||||||
|
onValueChange = { url = it },
|
||||||
|
keyboardOptions =
|
||||||
|
KeyboardOptions(
|
||||||
|
capitalization = KeyboardCapitalization.None,
|
||||||
|
autoCorrectEnabled = false,
|
||||||
|
keyboardType = KeyboardType.Uri,
|
||||||
|
),
|
||||||
|
keyboardActions =
|
||||||
|
KeyboardActions(
|
||||||
|
onDone = { submit.invoke() },
|
||||||
|
),
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
Button(
|
||||||
|
onClick = { submit.invoke() },
|
||||||
|
modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Text(text = "Submit")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,182 @@
|
||||||
|
package com.github.damontecres.dolphin.ui.setup
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import androidx.tv.material3.Button
|
||||||
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.dolphin.ui.components.BasicDialog
|
||||||
|
import com.github.damontecres.dolphin.ui.components.EditTextBox
|
||||||
|
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
|
||||||
|
import com.github.damontecres.dolphin.ui.nav.Destination
|
||||||
|
import com.github.damontecres.dolphin.ui.nav.NavigationManager
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SwitchUserContent(
|
||||||
|
navigationManager: NavigationManager,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: SwitchUserViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
val currentServer = viewModel.serverRepository.currentServer
|
||||||
|
val currentUser = viewModel.serverRepository.currentUser
|
||||||
|
val users by viewModel.users.observeAsState(listOf())
|
||||||
|
|
||||||
|
val quickConnectEnabled by viewModel.quickConnectEnabled.observeAsState(false)
|
||||||
|
val quickConnect by viewModel.quickConnectState.observeAsState(null)
|
||||||
|
var showAddUser by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
currentServer?.let { server ->
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = server.name ?: server.url,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Select User",
|
||||||
|
)
|
||||||
|
UserList(
|
||||||
|
users = users,
|
||||||
|
currentUser = currentUser,
|
||||||
|
onSwitchUser = { user ->
|
||||||
|
viewModel.switchUser(server, user) {
|
||||||
|
navigationManager.goToHome()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onAddUser = { showAddUser = true },
|
||||||
|
onRemoveUser = { user ->
|
||||||
|
},
|
||||||
|
onSwitchServer = {
|
||||||
|
navigationManager.navigateTo(Destination.ServerList)
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(.5f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showAddUser) {
|
||||||
|
var useQuickConnect by remember { mutableStateOf(quickConnectEnabled) }
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
if (quickConnectEnabled) {
|
||||||
|
viewModel.initiateQuickConnect(server) {
|
||||||
|
navigationManager.goToHome()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BasicDialog(
|
||||||
|
onDismissRequest = {
|
||||||
|
viewModel.cancelQuickConnect()
|
||||||
|
showAddUser = false
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = Modifier.padding(16.dp).fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
if (useQuickConnect) {
|
||||||
|
quickConnect?.let { qc ->
|
||||||
|
Text(
|
||||||
|
text = "Use Quick Connect on your device to authenticate to ${server.name ?: server.url}",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = qc.code,
|
||||||
|
style = MaterialTheme.typography.displayMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||||
|
)
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
useQuickConnect = false
|
||||||
|
},
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||||
|
) {
|
||||||
|
Text(text = "Use username/password")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var username by remember { mutableStateOf("") }
|
||||||
|
var password by remember { mutableStateOf("") }
|
||||||
|
val onSubmit = {
|
||||||
|
viewModel.login(server, username, password) {
|
||||||
|
showAddUser = false
|
||||||
|
navigationManager.goToHome()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Username",
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
EditTextBox(
|
||||||
|
value = username,
|
||||||
|
onValueChange = { username = it },
|
||||||
|
keyboardOptions =
|
||||||
|
KeyboardOptions(
|
||||||
|
capitalization = KeyboardCapitalization.None,
|
||||||
|
autoCorrectEnabled = false,
|
||||||
|
keyboardType = KeyboardType.Text,
|
||||||
|
),
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Password",
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
EditTextBox(
|
||||||
|
value = password,
|
||||||
|
onValueChange = { password = it },
|
||||||
|
keyboardOptions =
|
||||||
|
KeyboardOptions(
|
||||||
|
capitalization = KeyboardCapitalization.None,
|
||||||
|
autoCorrectEnabled = false,
|
||||||
|
keyboardType = KeyboardType.Password,
|
||||||
|
),
|
||||||
|
keyboardActions =
|
||||||
|
KeyboardActions(
|
||||||
|
onDone = { onSubmit.invoke() },
|
||||||
|
),
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Button(
|
||||||
|
onClick = { onSubmit.invoke() },
|
||||||
|
enabled = username.isNotNullOrBlank() && password.isNotNullOrBlank(),
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||||
|
) {
|
||||||
|
Text("Login")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,216 @@
|
||||||
|
package com.github.damontecres.dolphin.ui.setup
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.github.damontecres.dolphin.data.JellyfinServer
|
||||||
|
import com.github.damontecres.dolphin.data.JellyfinServerDao
|
||||||
|
import com.github.damontecres.dolphin.data.JellyfinUser
|
||||||
|
import com.github.damontecres.dolphin.data.ServerRepository
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.jellyfin.sdk.Jellyfin
|
||||||
|
import org.jellyfin.sdk.api.client.exception.InvalidStatusException
|
||||||
|
import org.jellyfin.sdk.api.client.extensions.authenticateUserByName
|
||||||
|
import org.jellyfin.sdk.api.client.extensions.quickConnectApi
|
||||||
|
import org.jellyfin.sdk.api.client.extensions.systemApi
|
||||||
|
import org.jellyfin.sdk.api.client.extensions.userApi
|
||||||
|
import org.jellyfin.sdk.model.api.QuickConnectDto
|
||||||
|
import org.jellyfin.sdk.model.api.QuickConnectResult
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class SwitchUserViewModel
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
val jellyfin: Jellyfin,
|
||||||
|
val serverRepository: ServerRepository,
|
||||||
|
val serverDao: JellyfinServerDao,
|
||||||
|
) : ViewModel() {
|
||||||
|
val servers = MutableLiveData<List<JellyfinServer>>(listOf())
|
||||||
|
val serverStatus = MutableLiveData<Map<String, ServerConnectionStatus>>(mapOf())
|
||||||
|
|
||||||
|
val users = MutableLiveData<List<JellyfinUser>>(listOf())
|
||||||
|
val quickConnectEnabled = MutableLiveData(false)
|
||||||
|
val quickConnectState = MutableLiveData<QuickConnectResult?>(null)
|
||||||
|
|
||||||
|
private var quickConnectJob: Job? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val allServers =
|
||||||
|
serverDao
|
||||||
|
.getServers()
|
||||||
|
.map { it.server }
|
||||||
|
.sortedWith(compareBy<JellyfinServer> { it.name }.thenBy { it.url })
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
servers.value = allServers
|
||||||
|
}
|
||||||
|
allServers.forEach { server ->
|
||||||
|
try {
|
||||||
|
jellyfin
|
||||||
|
.createApi(server.url)
|
||||||
|
.systemApi
|
||||||
|
.getPublicSystemInfo()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
serverStatus.value =
|
||||||
|
serverStatus.value!!.toMutableMap().apply {
|
||||||
|
put(
|
||||||
|
server.id,
|
||||||
|
ServerConnectionStatus.Success,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Timber.w(ex, "Error checking quick connect for server ${server.url}")
|
||||||
|
serverStatus.value =
|
||||||
|
serverStatus.value!!.toMutableMap().apply {
|
||||||
|
put(
|
||||||
|
server.id,
|
||||||
|
ServerConnectionStatus.Error(ex.localizedMessage),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
serverRepository.currentServer?.let {
|
||||||
|
val quickConnect =
|
||||||
|
jellyfin
|
||||||
|
.createApi(it.url)
|
||||||
|
.quickConnectApi
|
||||||
|
.getQuickConnectEnabled()
|
||||||
|
.content
|
||||||
|
val serverUsers = serverDao.getServer(it.id)?.users?.sortedBy { it.name } ?: listOf()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
quickConnectEnabled.value = quickConnect
|
||||||
|
users.value = serverUsers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun switchUser(
|
||||||
|
server: JellyfinServer,
|
||||||
|
user: JellyfinUser,
|
||||||
|
callback: () -> Unit,
|
||||||
|
) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
serverRepository.changeUser(server, user)
|
||||||
|
callback.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun login(
|
||||||
|
server: JellyfinServer,
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
onAuthenticated: () -> Unit,
|
||||||
|
) {
|
||||||
|
quickConnectJob?.cancel()
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val api = jellyfin.createApi(baseUrl = server.url)
|
||||||
|
val authenticationResult by api.userApi.authenticateUserByName(
|
||||||
|
username = username,
|
||||||
|
password = password,
|
||||||
|
)
|
||||||
|
serverRepository.changeUser(server.url, authenticationResult)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onAuthenticated.invoke()
|
||||||
|
}
|
||||||
|
} catch (err: InvalidStatusException) {
|
||||||
|
if (err.status == 401) {
|
||||||
|
// TODO set state to notify user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun initiateQuickConnect(
|
||||||
|
server: JellyfinServer,
|
||||||
|
onAuthenticated: () -> Unit,
|
||||||
|
) {
|
||||||
|
quickConnectJob?.cancel()
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val api = jellyfin.createApi(server.url)
|
||||||
|
var state =
|
||||||
|
api
|
||||||
|
.quickConnectApi
|
||||||
|
.initiateQuickConnect()
|
||||||
|
.content
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
quickConnectState.value = state
|
||||||
|
}
|
||||||
|
|
||||||
|
quickConnectJob =
|
||||||
|
viewModelScope.launch {
|
||||||
|
while (!state.authenticated) {
|
||||||
|
delay(5_000L)
|
||||||
|
state =
|
||||||
|
api.quickConnectApi
|
||||||
|
.getQuickConnectState(
|
||||||
|
secret = state.secret,
|
||||||
|
).content
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
quickConnectState.value = state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val authenticationResult by api.userApi.authenticateWithQuickConnect(
|
||||||
|
QuickConnectDto(secret = state.secret),
|
||||||
|
)
|
||||||
|
serverRepository.changeUser(server.url, authenticationResult)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onAuthenticated.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err: InvalidStatusException) {
|
||||||
|
if (err.status == 401) {
|
||||||
|
quickConnectState.value = null
|
||||||
|
quickConnectEnabled.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelQuickConnect() {
|
||||||
|
quickConnectJob?.cancel()
|
||||||
|
quickConnectState.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addServer(
|
||||||
|
serverUrl: String,
|
||||||
|
callback: () -> Unit,
|
||||||
|
) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val serverInfo by jellyfin
|
||||||
|
.createApi(serverUrl)
|
||||||
|
.systemApi
|
||||||
|
.getPublicSystemInfo()
|
||||||
|
val id = serverInfo.id
|
||||||
|
if (id != null) {
|
||||||
|
serverRepository.addAndChangeServer(
|
||||||
|
JellyfinServer(
|
||||||
|
id = id,
|
||||||
|
name = serverInfo.serverName,
|
||||||
|
url = serverUrl,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
callback.invoke()
|
||||||
|
} else {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Timber.w(ex, "Error creating API for $serverUrl")
|
||||||
|
// TODO notify user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
package com.github.damontecres.dolphin.ui.setup
|
||||||
|
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.tv.material3.Icon
|
||||||
|
import androidx.tv.material3.ListItem
|
||||||
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.dolphin.data.JellyfinUser
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UserList(
|
||||||
|
users: List<JellyfinUser>,
|
||||||
|
currentUser: JellyfinUser?,
|
||||||
|
onSwitchUser: (JellyfinUser) -> Unit,
|
||||||
|
onAddUser: () -> Unit,
|
||||||
|
onRemoveUser: (JellyfinUser) -> Unit,
|
||||||
|
onSwitchServer: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
LazyColumn(modifier = modifier) {
|
||||||
|
items(users) { user ->
|
||||||
|
ListItem(
|
||||||
|
enabled = true,
|
||||||
|
selected = false,
|
||||||
|
headlineContent = { Text(text = user.name ?: user.id) },
|
||||||
|
supportingContent = { },
|
||||||
|
leadingContent = {
|
||||||
|
if (user.id == currentUser?.id) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Check,
|
||||||
|
contentDescription = "current user",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClick = { onSwitchUser.invoke(user) },
|
||||||
|
onLongClick = {
|
||||||
|
// TODO dialog to remove user
|
||||||
|
},
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
HorizontalDivider()
|
||||||
|
ListItem(
|
||||||
|
enabled = true,
|
||||||
|
selected = false,
|
||||||
|
headlineContent = { Text(text = "Add User") },
|
||||||
|
supportingContent = { },
|
||||||
|
onClick = { onAddUser.invoke() },
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
HorizontalDivider()
|
||||||
|
ListItem(
|
||||||
|
enabled = true,
|
||||||
|
selected = false,
|
||||||
|
headlineContent = { Text(text = "Switch servers") },
|
||||||
|
supportingContent = { },
|
||||||
|
onClick = { onSwitchServer.invoke() },
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue