Handle remove users/servers

This commit is contained in:
Damontecres 2025-10-06 23:58:39 -04:00
parent 864fc5b9dd
commit ffc704bdce
No known key found for this signature in database
9 changed files with 294 additions and 94 deletions

View file

@ -72,7 +72,7 @@ class MainActivity : AppCompatActivity() {
val appPreferences by userPreferencesDataStore.data.collectAsState(null) val appPreferences by userPreferencesDataStore.data.collectAsState(null)
appPreferences?.let { appPreferences -> appPreferences?.let { appPreferences ->
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if (appPreferences.currentServerId.isNotBlank() && appPreferences.currentUserId.isNotBlank()) { if (appPreferences.currentServerId.isNotBlank()) {
serverRepository.restoreSession( serverRepository.restoreSession(
appPreferences.currentServerId, appPreferences.currentServerId,
appPreferences.currentUserId, appPreferences.currentUserId,

View file

@ -102,6 +102,7 @@ class ServerRepository
serverDao.getServer(serverId) serverDao.getServer(serverId)
} }
if (serverAndUsers != null) { if (serverAndUsers != null) {
_currentServer = serverAndUsers.server
val user = serverAndUsers.users.firstOrNull { it.id == userId } val user = serverAndUsers.users.firstOrNull { it.id == userId }
if (user != null) { if (user != null) {
changeUser(serverAndUsers.server, user) changeUser(serverAndUsers.server, user)
@ -142,4 +143,37 @@ class ServerRepository
} }
} }
} }
suspend fun removeUser(user: JellyfinUser) {
if (currentUser == user) {
_currentUser = null
userPreferencesDataStore.updateData {
it
.toBuilder()
.apply {
currentUserId = ""
}.build()
}
apiClient.update(accessToken = null)
}
withContext(Dispatchers.IO) {
serverDao.deleteUser(user.serverId, user.id)
}
}
suspend fun removeServer(server: JellyfinServer) {
if (currentServer == server) {
_currentServer = null
userPreferencesDataStore.updateData {
it
.toBuilder()
.apply {
currentServerId = ""
}.build()
}
}
withContext(Dispatchers.IO) {
serverDao.deleteServer(server.id)
}
}
} }

View file

@ -29,6 +29,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.Key
@ -37,11 +38,13 @@ import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.key.type import androidx.compose.ui.input.key.type
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.tv.material3.Icon import androidx.tv.material3.Icon
import androidx.tv.material3.ListItem import androidx.tv.material3.ListItem
import androidx.tv.material3.LocalContentColor
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation import androidx.tv.material3.surfaceColorAtElevation
@ -93,6 +96,7 @@ data class DialogItem(
constructor( constructor(
text: String, text: String,
icon: ImageVector, icon: ImageVector,
iconColor: Color? = null,
onClick: () -> Unit, onClick: () -> Unit,
) : this( ) : this(
headlineContent = { headlineContent = {
@ -105,7 +109,7 @@ data class DialogItem(
Icon( Icon(
imageVector = icon, imageVector = icon,
contentDescription = text, contentDescription = text,
// tint = MaterialTheme.colorScheme.onPrimaryContainer, tint = iconColor ?: LocalContentColor.current,
) )
}, },
onClick = onClick, onClick = onClick,
@ -139,6 +143,7 @@ fun DialogPopup(
dismissOnClick: Boolean = true, dismissOnClick: Boolean = true,
waitToLoad: Boolean = true, waitToLoad: Boolean = true,
properties: DialogProperties = DialogProperties(), properties: DialogProperties = DialogProperties(),
elevation: Dp = 3.dp,
) { ) {
var waiting by remember { mutableStateOf(waitToLoad) } var waiting by remember { mutableStateOf(waitToLoad) }
if (showDialog) { if (showDialog) {
@ -158,7 +163,8 @@ fun DialogPopup(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
properties = properties, properties = properties,
) { ) {
val elevatedContainerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp) val elevatedContainerColor =
MaterialTheme.colorScheme.surfaceColorAtElevation(elevation)
LazyColumn( LazyColumn(
modifier = modifier =
Modifier Modifier

View file

@ -3,16 +3,29 @@ package com.github.damontecres.dolphin.ui.setup
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Warning import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.tv.material3.Icon import androidx.tv.material3.Icon
import androidx.tv.material3.ListItem import androidx.tv.material3.ListItem
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.dolphin.R
import com.github.damontecres.dolphin.data.JellyfinServer import com.github.damontecres.dolphin.data.JellyfinServer
import com.github.damontecres.dolphin.ui.components.CircularProgress import com.github.damontecres.dolphin.ui.components.CircularProgress
import com.github.damontecres.dolphin.ui.components.DialogItem
import com.github.damontecres.dolphin.ui.components.DialogPopup
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
sealed interface ServerConnectionStatus { sealed interface ServerConnectionStatus {
object Success : ServerConnectionStatus object Success : ServerConnectionStatus
@ -33,14 +46,16 @@ fun ServerList(
onRemoveServer: (JellyfinServer) -> Unit, onRemoveServer: (JellyfinServer) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
var showDeleteDialog by remember { mutableStateOf<JellyfinServer?>(null) }
LazyColumn(modifier = modifier) { LazyColumn(modifier = modifier) {
items(servers) { server -> items(servers) { server ->
val status = connectionStatus[server.id] ?: ServerConnectionStatus.Pending val status = connectionStatus[server.id] ?: ServerConnectionStatus.Pending
ListItem( ListItem(
enabled = status == ServerConnectionStatus.Success, enabled = status == ServerConnectionStatus.Success,
selected = false, selected = false,
headlineContent = { Text(text = server.name ?: server.id) }, headlineContent = { Text(text = server.name?.ifBlank { null } ?: server.url) },
supportingContent = { Text(text = server.url) }, supportingContent = { if (server.name.isNotNullOrBlank()) Text(text = server.url) },
leadingContent = { leadingContent = {
when (status) { when (status) {
ServerConnectionStatus.Success -> {} ServerConnectionStatus.Success -> {}
@ -69,10 +84,40 @@ fun ServerList(
enabled = true, enabled = true,
selected = false, selected = false,
headlineContent = { Text(text = "Add Server") }, headlineContent = { Text(text = "Add Server") },
supportingContent = { }, leadingContent = {
Icon(
imageVector = Icons.Default.Add,
tint = Color.Green.copy(alpha = .8f),
contentDescription = null,
)
},
onClick = { onAddServer.invoke() }, onClick = { onAddServer.invoke() },
modifier = Modifier, modifier = Modifier,
) )
} }
} }
showDeleteDialog?.let { server ->
DialogPopup(
showDialog = true,
title = server.name ?: server.url,
dialogItems =
listOf(
DialogItem("Switch", R.string.fa_arrow_left_arrow_right) {
onSwitchServer.invoke(server)
},
DialogItem(
"Delete",
Icons.Default.Delete,
Color.Red.copy(alpha = .8f),
) {
onRemoveServer.invoke(server)
},
),
onDismissRequest = { showDeleteDialog = null },
dismissOnClick = true,
waitToLoad = true,
properties = DialogProperties(),
elevation = 5.dp,
)
}
} }

View file

@ -1,9 +1,12 @@
package com.github.damontecres.dolphin.ui.setup package com.github.damontecres.dolphin.ui.setup
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -19,7 +22,9 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.tv.material3.Button import androidx.tv.material3.Button
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation
import com.github.damontecres.dolphin.ui.components.BasicDialog import com.github.damontecres.dolphin.ui.components.BasicDialog
import com.github.damontecres.dolphin.ui.components.EditTextBox import com.github.damontecres.dolphin.ui.components.EditTextBox
import com.github.damontecres.dolphin.ui.nav.Destination import com.github.damontecres.dolphin.ui.nav.Destination
@ -37,71 +42,91 @@ fun SwitchServerContent(
var showAddServer by remember { mutableStateOf(false) } var showAddServer by remember { mutableStateOf(false) }
Column( Box(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier, modifier = modifier,
) { ) {
Text( Column(
text = "Select Server", horizontalAlignment = Alignment.CenterHorizontally,
) verticalArrangement = Arrangement.spacedBy(8.dp),
ServerList( modifier =
servers = servers, Modifier
connectionStatus = serverStatus, .fillMaxWidth(.5f)
onSwitchServer = { .align(Alignment.Center)
viewModel.addServer(it.url) { .padding(16.dp)
.background(
MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp),
shape = RoundedCornerShape(16.dp),
),
) {
Text(
text = "Select Server",
style = MaterialTheme.typography.displaySmall,
color = MaterialTheme.colorScheme.onSurface,
)
ServerList(
servers = servers,
connectionStatus = serverStatus,
onSwitchServer = {
viewModel.addServer(it.url) {
navigationManager.navigateTo(Destination.UserList)
}
},
onAddServer = {
showAddServer = true
},
onRemoveServer = {
viewModel.removeServer(it)
},
modifier =
Modifier
.fillMaxWidth()
.background(
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
shape = RoundedCornerShape(16.dp),
),
)
}
if (showAddServer) {
var url by remember { mutableStateOf("") }
val submit = {
showAddServer = false
viewModel.addServer(url) {
navigationManager.navigateTo(Destination.UserList) navigationManager.navigateTo(Destination.UserList)
} }
},
onAddServer = {
showAddServer = true
},
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(
BasicDialog( onDismissRequest = { showAddServer = false },
onDismissRequest = { showAddServer = false },
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(16.dp),
) { ) {
Text( Column(
text = "Enter Server URL", horizontalAlignment = Alignment.CenterHorizontally,
) verticalArrangement = Arrangement.spacedBy(8.dp),
EditTextBox( modifier = Modifier.padding(16.dp),
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") 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")
}
} }
} }
} }

View file

@ -1,10 +1,13 @@
package com.github.damontecres.dolphin.ui.setup package com.github.damontecres.dolphin.ui.setup
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -23,6 +26,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.tv.material3.Button import androidx.tv.material3.Button
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation
import com.github.damontecres.dolphin.ui.components.BasicDialog import com.github.damontecres.dolphin.ui.components.BasicDialog
import com.github.damontecres.dolphin.ui.components.EditTextBox import com.github.damontecres.dolphin.ui.components.EditTextBox
import com.github.damontecres.dolphin.ui.isNotNullOrBlank import com.github.damontecres.dolphin.ui.isNotNullOrBlank
@ -45,33 +49,56 @@ fun SwitchUserContent(
var showAddUser by remember { mutableStateOf(false) } var showAddUser by remember { mutableStateOf(false) }
currentServer?.let { server -> currentServer?.let { server ->
Column( Box(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier, modifier = modifier,
) { ) {
Text( Column(
text = server.name ?: server.url, horizontalAlignment = Alignment.CenterHorizontally,
) verticalArrangement = Arrangement.spacedBy(8.dp),
Text( modifier =
text = "Select User", Modifier
) .fillMaxWidth(.5f)
UserList( .align(Alignment.Center)
users = users, .padding(16.dp)
currentUser = currentUser, .background(
onSwitchUser = { user -> MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp),
viewModel.switchUser(server, user) { shape = RoundedCornerShape(16.dp),
navigationManager.goToHome() ),
} ) {
}, Text(
onAddUser = { showAddUser = true }, text = "Select User",
onRemoveUser = { user -> style = MaterialTheme.typography.displaySmall,
}, color = MaterialTheme.colorScheme.onSurface,
onSwitchServer = { )
navigationManager.navigateTo(Destination.ServerList) Text(
}, text = server.name ?: server.url,
modifier = Modifier.fillMaxWidth(.5f), style = MaterialTheme.typography.titleLarge,
) color = MaterialTheme.colorScheme.onSurface,
)
UserList(
users = users,
currentUser = currentUser,
onSwitchUser = { user ->
viewModel.switchUser(server, user) {
navigationManager.goToHome()
}
},
onAddUser = { showAddUser = true },
onRemoveUser = { user ->
viewModel.removeUser(user)
},
onSwitchServer = {
navigationManager.navigateTo(Destination.ServerList)
},
modifier =
Modifier
.fillMaxWidth()
.background(
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
shape = RoundedCornerShape(16.dp),
),
)
}
} }
if (showAddUser) { if (showAddUser) {

View file

@ -241,4 +241,16 @@ class SwitchUserViewModel
} }
} }
} }
fun removeUser(user: JellyfinUser) {
viewModelScope.launch(ExceptionHandler()) {
serverRepository.removeUser(user)
}
}
fun removeServer(server: JellyfinServer) {
viewModelScope.launch(ExceptionHandler()) {
serverRepository.removeServer(server)
}
}
} }

View file

@ -3,14 +3,28 @@ package com.github.damontecres.dolphin.ui.setup
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.tv.material3.Icon import androidx.tv.material3.Icon
import androidx.tv.material3.ListItem import androidx.tv.material3.ListItem
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.dolphin.R
import com.github.damontecres.dolphin.data.JellyfinUser import com.github.damontecres.dolphin.data.JellyfinUser
import com.github.damontecres.dolphin.ui.FontAwesome
import com.github.damontecres.dolphin.ui.components.DialogItem
import com.github.damontecres.dolphin.ui.components.DialogPopup
@Composable @Composable
fun UserList( fun UserList(
@ -22,13 +36,14 @@ fun UserList(
onSwitchServer: () -> Unit, onSwitchServer: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
var showDeleteDialog by remember { mutableStateOf<JellyfinUser?>(null) }
LazyColumn(modifier = modifier) { LazyColumn(modifier = modifier) {
items(users) { user -> items(users) { user ->
ListItem( ListItem(
enabled = true, enabled = true,
selected = false, selected = user == currentUser,
headlineContent = { Text(text = user.name ?: user.id) }, headlineContent = { Text(text = user.name ?: user.id) },
supportingContent = { },
leadingContent = { leadingContent = {
if (user.id == currentUser?.id) { if (user.id == currentUser?.id) {
Icon( Icon(
@ -39,7 +54,7 @@ fun UserList(
}, },
onClick = { onSwitchUser.invoke(user) }, onClick = { onSwitchUser.invoke(user) },
onLongClick = { onLongClick = {
// TODO dialog to remove user showDeleteDialog = user
}, },
modifier = Modifier, modifier = Modifier,
) )
@ -50,7 +65,13 @@ fun UserList(
enabled = true, enabled = true,
selected = false, selected = false,
headlineContent = { Text(text = "Add User") }, headlineContent = { Text(text = "Add User") },
supportingContent = { }, leadingContent = {
Icon(
imageVector = Icons.Default.Add,
tint = Color.Green.copy(alpha = .8f),
contentDescription = null,
)
},
onClick = { onAddUser.invoke() }, onClick = { onAddUser.invoke() },
modifier = Modifier, modifier = Modifier,
) )
@ -61,10 +82,39 @@ fun UserList(
enabled = true, enabled = true,
selected = false, selected = false,
headlineContent = { Text(text = "Switch servers") }, headlineContent = { Text(text = "Switch servers") },
supportingContent = { }, leadingContent = {
Text(
text = stringResource(R.string.fa_arrow_left_arrow_right),
fontFamily = FontAwesome,
)
},
onClick = { onSwitchServer.invoke() }, onClick = { onSwitchServer.invoke() },
modifier = Modifier, modifier = Modifier,
) )
} }
} }
showDeleteDialog?.let { user ->
DialogPopup(
showDialog = true,
title = user.name ?: user.id,
dialogItems =
listOf(
DialogItem("Switch", R.string.fa_arrow_left_arrow_right) {
onSwitchUser.invoke(user)
},
DialogItem(
"Delete",
Icons.Default.Delete,
Color.Red.copy(alpha = .8f),
) {
onRemoveUser.invoke(user)
},
),
onDismissRequest = { showDeleteDialog = null },
dismissOnClick = true,
waitToLoad = true,
properties = DialogProperties(),
elevation = 5.dp,
)
}
} }

View file

@ -30,4 +30,5 @@
<string name="fa_music" translatable="false">&#xf001;</string> <string name="fa_music" translatable="false">&#xf001;</string>
<string name="fa_eye" translatable="false">&#xf06e;</string> <string name="fa_eye" translatable="false">&#xf06e;</string>
<string name="fa_eye_slash" translatable="false">&#xf070;</string> <string name="fa_eye_slash" translatable="false">&#xf070;</string>
<string name="fa_arrow_left_arrow_right" translatable="false">&#xf0ec;</string>
</resources> </resources>