diff --git a/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/components/PanelBottomSheet.kt b/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/components/PanelBottomSheet.kt new file mode 100644 index 00000000..a67d3635 --- /dev/null +++ b/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/components/PanelBottomSheet.kt @@ -0,0 +1,74 @@ +package com.redmadrobot.debug.uikit.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.redmadrobot.debug.uikit.theme.DebugPanelTheme +import com.redmadrobot.debug.uikit.theme.SystemBarsColors +import com.redmadrobot.debug.uikit.theme.SystemBarsEffect + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +public fun PanelBottomSheet( + title: String, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable ColumnScope.() -> Unit, +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp), + containerColor = DebugPanelTheme.colors.surface.secondary, + modifier = modifier, + dragHandle = { SheetHandle() }, + ) { + SystemBarsEffect( + systemBarsColors = SystemBarsColors( + statusBarColor = DebugPanelTheme.colors.background.primary, + navigationBarColor = DebugPanelTheme.colors.surface.secondary, + ), + ) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + ) { + Text( + text = title, + style = DebugPanelTheme.typography.titleLarge, + color = DebugPanelTheme.colors.content.primary, + modifier = Modifier.padding(bottom = 20.dp), + ) + content() + } + } +} + +@Composable +private fun SheetHandle(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .padding(top = 12.dp, bottom = 20.dp) + .size(width = 32.dp, height = 4.dp) + .background( + color = DebugPanelTheme.colors.stroke.secondary, + shape = RoundedCornerShape(2.dp), + ), + ) +} diff --git a/plugins/plugin-servers/src/main/kotlin/com/redmadrobot/debug/plugin/servers/ServersPluginContainer.kt b/plugins/plugin-servers/src/main/kotlin/com/redmadrobot/debug/plugin/servers/ServersPluginContainer.kt index 471497c7..3f23d2ce 100644 --- a/plugins/plugin-servers/src/main/kotlin/com/redmadrobot/debug/plugin/servers/ServersPluginContainer.kt +++ b/plugins/plugin-servers/src/main/kotlin/com/redmadrobot/debug/plugin/servers/ServersPluginContainer.kt @@ -20,9 +20,10 @@ internal class ServersPluginContainer( ) } - fun createServersViewModel(): ServersViewModel { + fun createServersViewModel(isEditMode: Boolean): ServersViewModel { return ServersViewModel( - serversRepository, + isEditMode = isEditMode, + serversRepository = serversRepository, ) } } diff --git a/plugins/plugin-servers/src/main/kotlin/com/redmadrobot/debug/plugin/servers/data/DebugServerRepository.kt b/plugins/plugin-servers/src/main/kotlin/com/redmadrobot/debug/plugin/servers/data/DebugServerRepository.kt index 9faef005..64a360a9 100644 --- a/plugins/plugin-servers/src/main/kotlin/com/redmadrobot/debug/plugin/servers/data/DebugServerRepository.kt +++ b/plugins/plugin-servers/src/main/kotlin/com/redmadrobot/debug/plugin/servers/data/DebugServerRepository.kt @@ -23,7 +23,10 @@ internal class DebugServerRepository( suspend fun getServers(): List = serversDataStore.getAll() - suspend fun removeServer(server: DebugServer) = serversDataStore.remove(server) + suspend fun removeServer(server: DebugServer) { + if (server == getSelectedServer()) saveSelectedServer(getDefault()) + serversDataStore.remove(server) + } suspend fun updateServer(oldServer: DebugServer, newServer: DebugServer) = serversDataStore.update(oldServer, newServer) diff --git a/plugins/plugin-servers/src/main/kotlin/com/redmadrobot/debug/plugin/servers/ui/ServersScreen.kt b/plugins/plugin-servers/src/main/kotlin/com/redmadrobot/debug/plugin/servers/ui/ServersScreen.kt index 1bfc4840..33afda59 100644 --- a/plugins/plugin-servers/src/main/kotlin/com/redmadrobot/debug/plugin/servers/ui/ServersScreen.kt +++ b/plugins/plugin-servers/src/main/kotlin/com/redmadrobot/debug/plugin/servers/ui/ServersScreen.kt @@ -1,93 +1,104 @@ package com.redmadrobot.debug.plugin.servers.ui -import android.annotation.SuppressLint -import androidx.annotation.StringRes +import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Button -import androidx.compose.material.Card -import androidx.compose.material.ExtendedFloatingActionButton -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.MaterialTheme -import androidx.compose.material.OutlinedTextField -import androidx.compose.material.Scaffold -import androidx.compose.material.Surface -import androidx.compose.material.Text +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties -import androidx.lifecycle.Lifecycle -import com.redmadrobot.debug.core.extension.OnLifecycleEvent import com.redmadrobot.debug.core.extension.getPlugin import com.redmadrobot.debug.core.extension.provideViewModel import com.redmadrobot.debug.plugin.servers.R import com.redmadrobot.debug.plugin.servers.ServersPlugin import com.redmadrobot.debug.plugin.servers.ServersPluginContainer import com.redmadrobot.debug.plugin.servers.data.model.DebugServer +import com.redmadrobot.debug.uikit.components.PanelBottomSheet +import com.redmadrobot.debug.uikit.theme.DebugPanelDimensions +import com.redmadrobot.debug.uikit.theme.DebugPanelShapes +import com.redmadrobot.debug.uikit.theme.DebugPanelTheme -@SuppressLint("UnusedMaterialScaffoldPaddingParameter") @Composable internal fun ServersScreen( isEditMode: Boolean, viewModel: ServersViewModel = provideViewModel { getPlugin() .getContainer() - .createServersViewModel() + .createServersViewModel(isEditMode = isEditMode) }, ) { val state by viewModel.state.collectAsState() + val snackbarHostState = remember { SnackbarHostState() } + val selectedServerMessage = stringResource(R.string.server_selected) + + LaunchedEffect(Unit) { + viewModel.events.collect { messageEvent -> + messageEvent?.let { + val message = selectedServerMessage.format(messageEvent.serverName) + snackbarHostState.showSnackbar(message = message) + } + } + } Scaffold( - floatingActionButton = { - if (isEditMode) { - ExtendedFloatingActionButton( - onClick = { viewModel.onAddClicked() }, - text = { Text("Add") }, - icon = { - Icon( - painterResource(R.drawable.icon_add_server), - contentDescription = null - ) - } + containerColor = DebugPanelTheme.colors.background.primary, + snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + ) { paddingValues -> + LazyColumn( + modifier = Modifier.padding(paddingValues = paddingValues), + contentPadding = PaddingValues(bottom = 16.dp), + ) { + item { if (state.isEditMode) AddServerButton(onAddClick = viewModel::onAddClicked) } + if (state.preInstalledServers.isNotEmpty()) { + preInstalledServersItems( + preInstalledServers = state.preInstalledServers, + onClick = { item -> viewModel.onServerClicked(debugServer = item.server) } + ) + } + + if (state.addedServers.isNotEmpty()) { + addedServersItems( + isDeleteAvailable = state.isEditMode, + addedServers = state.addedServers, + onClick = { item -> viewModel.onServerClicked(debugServer = item.server) }, + onRemoveServerClick = viewModel::onRemoveServerClicked ) } } - ) { - ServersScreenLayout( - state = state, - isEditMode = isEditMode, - onServerClick = if (!isEditMode) viewModel::onServerClicked else viewModel::onEditServerClicked, - onServerDeleteClick = viewModel::onRemoveServerClicked - ) } + if (state.serverDialogState.show) { - ServerDialog( + ServerBottomSheet( state = state.serverDialogState, onNameChange = viewModel::onServerNameChanged, onUrlChange = viewModel::onServerUrlChanged, @@ -95,181 +106,239 @@ internal fun ServersScreen( onSaveClick = viewModel::onSaveServerClicked, ) } - OnLifecycleEvent { event -> - if (event == Lifecycle.Event.ON_RESUME) { - viewModel.loadServers() +} + +private fun LazyListScope.preInstalledServersItems( + preInstalledServers: List, + onClick: (ServerItemData) -> Unit +) { + item { SectionHeader(stringResource(R.string.pre_installed_servers)) } + items(items = preInstalledServers, key = { it.server.url }) { item -> + ServerInfoRow( + serverName = item.server.name, + serverUrl = item.server.url, + isSelected = item.isSelected, + isDeleteAvailable = false, + onClick = { onClick.invoke(item) }, + ) + } +} + +private fun LazyListScope.addedServersItems( + isDeleteAvailable: Boolean, + addedServers: List, + onClick: (ServerItemData) -> Unit, + onRemoveServerClick: (DebugServer) -> Unit +) { + item { SectionHeader(stringResource(R.string.added_servers)) } + items(items = addedServers, key = { it.server.url }) { item -> + ServerInfoRow( + serverName = item.server.name, + serverUrl = item.server.url, + isSelected = item.isSelected, + isDeleteAvailable = isDeleteAvailable, + onClick = { onClick.invoke(item) }, + onDelete = { onRemoveServerClick.invoke(item.server) }, + ) + } +} + +@Composable +private fun SectionHeader(title: String, modifier: Modifier = Modifier) { + Text( + text = title.uppercase(), + style = DebugPanelTheme.typography.labelSmall, + color = DebugPanelTheme.colors.content.tertiary, + modifier = modifier.padding(top = 12.dp, bottom = 8.dp, start = 12.dp), + ) +} + +@Composable +private fun AddServerButton(onAddClick: () -> Unit, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 8.dp) + ) { + TextButton( + contentPadding = PaddingValues(all = 0.dp), + onClick = { onAddClick.invoke() } + ) { + Text( + text = stringResource(R.string.add_server), + style = DebugPanelTheme.typography.labelLarge, + color = DebugPanelTheme.colors.content.accent, + ) } } } @Composable -private fun ServersScreenLayout( - state: ServersViewState, - isEditMode: Boolean, - onServerClick: (DebugServer) -> Unit, - onServerDeleteClick: (DebugServer) -> Unit, +private fun ServerInfoRow( + serverName: String, + serverUrl: String, + isSelected: Boolean, + isDeleteAvailable: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, + onDelete: (() -> Unit)? = null, ) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(8.dp), - contentPadding = PaddingValues(start = 16.dp, end = 16.dp, bottom = 64.dp), + Row( + modifier = modifier + .fillMaxWidth() + .clip(shape = DebugPanelShapes.medium) + .clickable(onClick = onClick) + .padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, ) { - serverItems( - items = state.preInstalledServers, - titleRes = R.string.pre_installed_servers, - isSelectable = !isEditMode, - showDelete = false, - onItemClick = onServerClick.takeIf { !isEditMode }, - onDeleteClick = onServerDeleteClick, - ) - serverItems( - items = state.addedServers, - titleRes = R.string.added_servers, - isSelectable = !isEditMode, - showDelete = isEditMode, - onItemClick = onServerClick, - onDeleteClick = onServerDeleteClick, + ServerSelectionIndicator(isSelected = isSelected) + ServerInfo( + serverName = serverName, + serverUrl = serverUrl, ) + if (isDeleteAvailable && onDelete != null) { + ServerDeleteButton(onDelete = onDelete) + } } } -private fun LazyListScope.serverItems( - items: List, - @StringRes titleRes: Int, - isSelectable: Boolean, - showDelete: Boolean, - onDeleteClick: (DebugServer) -> Unit, - onItemClick: ((DebugServer) -> Unit)? = null, -) { - if (items.isEmpty()) return +@Composable +private fun ServerSelectionIndicator(isSelected: Boolean, modifier: Modifier = Modifier) { + val color = with(DebugPanelTheme.colors) { if (isSelected) content.teal else stroke.primary } - titleItem(titleRes) - items(items) { item -> - ServerItem( - server = item.server, - selected = item.isSelected && isSelectable, - showDelete = showDelete, - onItemClick = onItemClick, - onDeleteClick = onDeleteClick, - ) - } + Box( + modifier = modifier + .size(size = DebugPanelDimensions.dotSize) + .background(color = color, shape = CircleShape), + ) } -private fun LazyListScope.titleItem(@StringRes titleRes: Int) { - item { - Text( - text = stringResource(id = titleRes).uppercase(), - modifier = Modifier - .fillParentMaxWidth() - .padding(vertical = 16.dp), - fontSize = 16.sp, +@Composable +private fun ServerDeleteButton(onDelete: () -> Unit, modifier: Modifier = Modifier) { + IconButton( + onClick = onDelete, + modifier = modifier.size(DebugPanelDimensions.iconSizeLarge), + ) { + Icon( + painter = painterResource(R.drawable.icon_delete), + contentDescription = null, + tint = DebugPanelTheme.colors.content.error, + modifier = Modifier.size(DebugPanelDimensions.iconSizeMedium), ) } } @Composable -private fun ServerItem( - server: DebugServer, - selected: Boolean, - showDelete: Boolean, - onDeleteClick: (DebugServer) -> Unit, - onItemClick: ((DebugServer) -> Unit)? = null, +private fun RowScope.ServerInfo( + serverName: String, + serverUrl: String, + modifier: Modifier = Modifier ) { - Card( - modifier = Modifier - .fillMaxWidth() - .defaultMinSize(minHeight = 56.dp) - .clickable { onItemClick?.invoke(server) } + Column( + modifier = modifier + .weight(1f) + .padding(start = 12.dp) ) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp) - ) { - Row( - modifier = Modifier.fillMaxSize(), - horizontalArrangement = Arrangement.spacedBy(32.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - painter = painterResource(R.drawable.icon_router), - contentDescription = null, - tint = MaterialTheme.colors.primary - ) - Text(server.name, fontWeight = FontWeight.SemiBold) - Box(modifier = Modifier.weight(1f)) { - if (selected) { - Icon( - painterResource(R.drawable.icon_selected), - contentDescription = null, - modifier = Modifier.align(Alignment.CenterEnd) - ) - } else if (showDelete) { - IconButton( - modifier = Modifier.align(Alignment.CenterEnd), - onClick = { onDeleteClick(server) }, - ) { - Icon( - painterResource(R.drawable.icon_delete), - contentDescription = null, - tint = Color.Red - ) - } - } - } - } - Spacer(modifier = Modifier.height(8.dp)) - Text(text = "url: ${server.url}", color = Color.Gray, fontSize = 12.sp) - } + Text( + text = serverName, + style = DebugPanelTheme.typography.titleSmall, + color = DebugPanelTheme.colors.content.primary, + ) + Text( + text = serverUrl, + style = DebugPanelTheme.typography.bodySmall, + color = DebugPanelTheme.colors.content.secondary, + maxLines = 1, + ) } } @Composable -private fun ServerDialog( +private fun ServerBottomSheet( state: ServerDialogState, onNameChange: (String) -> Unit, onUrlChange: (String) -> Unit, onDismiss: () -> Unit, onSaveClick: () -> Unit, + modifier: Modifier = Modifier ) { - Dialog( - onDismissRequest = onDismiss, - properties = DialogProperties(usePlatformDefaultWidth = false) + val title = if (state.editableServer != null) { + stringResource(R.string.edit_server) + } else { + stringResource(R.string.add_server) + } + + PanelBottomSheet( + modifier = modifier, + title = title, + onDismiss = onDismiss, ) { - Surface( - shape = RoundedCornerShape(16.dp), - color = Color.White, - modifier = Modifier.wrapContentHeight() + ServerTextField( + value = state.serverName, + onValueChange = onNameChange, + label = stringResource(R.string.name), + isError = state.inputErrors?.nameError != null, + errorMessage = state.inputErrors?.nameError?.let { stringResource(it) }, + ) + Spacer(modifier = Modifier.height(16.dp)) + ServerTextField( + value = state.serverUrl, + onValueChange = onUrlChange, + label = stringResource(R.string.server_host_hint), + isError = state.inputErrors?.urlError != null, + errorMessage = state.inputErrors?.urlError?.let { stringResource(it) }, + ) + Spacer(modifier = Modifier.height(20.dp)) + Button( + onClick = onSaveClick, + modifier = Modifier.fillMaxWidth(), + shape = DebugPanelShapes.medium, ) { - Column( - modifier = Modifier.padding(16.dp) - ) { - OutlinedTextField( - value = state.serverName, - onValueChange = onNameChange, - label = { Text(stringResource(R.string.name)) }, - isError = state.inputErrors?.nameError != null - ) - if (state.inputErrors?.nameError != null) { - Text(text = stringResource(id = state.inputErrors.nameError), color = Color.Red) - } - Spacer(modifier = Modifier.height(16.dp)) - OutlinedTextField( - value = state.serverUrl, - onValueChange = onUrlChange, - label = { Text(stringResource(R.string.server_host_hint)) }, - isError = state.inputErrors?.urlError != null - ) - if (state.inputErrors?.urlError != null) { - Text(text = stringResource(id = state.inputErrors.urlError), color = Color.Red) - } - Button( - onClick = onSaveClick, - modifier = Modifier.align(Alignment.End) - ) { - Text(stringResource(R.string.save_server).uppercase()) - } - } + Text( + text = stringResource(R.string.save_server), + style = DebugPanelTheme.typography.labelLarge, + ) + } + Spacer(modifier = Modifier.height(24.dp)) + } +} + +@Composable +private fun ServerTextField( + value: String, + label: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + isError: Boolean = false, + errorMessage: String? = null, +) { + Column(modifier = modifier.fillMaxWidth()) { + OutlinedTextField( + value = value, + onValueChange = onValueChange, + modifier = Modifier.fillMaxWidth(), + label = { Text(label, style = DebugPanelTheme.typography.bodyMedium) }, + isError = isError, + singleLine = true, + textStyle = DebugPanelTheme.typography.bodyMedium.copy( + color = DebugPanelTheme.colors.content.primary, + ), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = DebugPanelTheme.colors.content.accent, + unfocusedBorderColor = DebugPanelTheme.colors.stroke.secondary, + focusedLabelColor = DebugPanelTheme.colors.content.accent, + unfocusedLabelColor = DebugPanelTheme.colors.content.tertiary, + errorBorderColor = DebugPanelTheme.colors.content.error, + cursorColor = DebugPanelTheme.colors.content.accent, + ), + ) + if (isError && errorMessage != null) { + Text( + text = errorMessage, + style = DebugPanelTheme.typography.bodySmall, + color = DebugPanelTheme.colors.content.error, + modifier = Modifier.padding(top = 4.dp), + ) } } } diff --git a/plugins/plugin-servers/src/main/kotlin/com/redmadrobot/debug/plugin/servers/ui/ServersViewModel.kt b/plugins/plugin-servers/src/main/kotlin/com/redmadrobot/debug/plugin/servers/ui/ServersViewModel.kt index 3d6457b6..ec54ea3f 100644 --- a/plugins/plugin-servers/src/main/kotlin/com/redmadrobot/debug/plugin/servers/ui/ServersViewModel.kt +++ b/plugins/plugin-servers/src/main/kotlin/com/redmadrobot/debug/plugin/servers/ui/ServersViewModel.kt @@ -2,6 +2,7 @@ package com.redmadrobot.debug.plugin.servers.ui import android.util.Patterns import androidx.lifecycle.viewModelScope +import com.redmadrobot.debug.core.DebugEvent import com.redmadrobot.debug.core.extension.getPlugin import com.redmadrobot.debug.core.extension.safeLaunch import com.redmadrobot.debug.core.internal.PluginViewModel @@ -11,27 +12,35 @@ import com.redmadrobot.debug.plugin.servers.ServersPlugin import com.redmadrobot.debug.plugin.servers.data.DebugServerRepository import com.redmadrobot.debug.plugin.servers.data.model.DebugServer import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch internal class ServersViewModel( + private val isEditMode: Boolean, private val serversRepository: DebugServerRepository, ) : PluginViewModel() { - private val _state = MutableStateFlow(ServersViewState()) + private val _state = MutableStateFlow(ServersViewState(isEditMode = isEditMode)) val state: StateFlow = _state.asStateFlow() - fun loadServers() { - viewModelScope.launch(Dispatchers.IO) { - _state.update { serversState -> - serversState.copy( - preInstalledServers = serversRepository.getPreInstalledServers() - .mapToServerItems(), - addedServers = serversRepository.getServers().mapToServerItems(), - ) - } + private val _events = MutableSharedFlow() + val events = _events.asSharedFlow().distinctUntilChanged() + + init { + loadServers() + } + + fun onServerClicked(debugServer: DebugServer) { + if (!isEditMode) { + selectServer(debugServer) + viewModelScope.launch { _events.emit(ServerSelectionMessageEvent(serverName = debugServer.name)) } + } else { + editServer(debugServer) } } @@ -90,6 +99,18 @@ internal class ServersViewModel( } } + private fun loadServers() { + viewModelScope.launch(Dispatchers.IO) { + _state.update { serversState -> + serversState.copy( + preInstalledServers = serversRepository.getPreInstalledServers() + .mapToServerItems(), + addedServers = serversRepository.getServers().mapToServerItems(), + ) + } + } + } + private fun checkInputErrors(dialogState: ServerDialogState): ServerDialogErrors? { val nameError = R.string.error_empty_name.takeIf { dialogState.serverName.isEmpty() } val urlError = R.string.error_wrong_host.takeIf { @@ -106,11 +127,7 @@ internal class ServersViewModel( fun onRemoveServerClicked(debugServer: DebugServer) { viewModelScope.safeLaunch { serversRepository.removeServer(debugServer) - _state.update { serversState -> - serversState.copy( - addedServers = serversRepository.getServers().mapToServerItems() - ) - } + loadServers() } } @@ -139,7 +156,7 @@ internal class ServersViewModel( } } - fun onEditServerClicked(debugServer: DebugServer) { + private fun editServer(debugServer: DebugServer) { _state.update { serversState -> serversState.copy( serverDialogState = ServerDialogState( @@ -152,7 +169,7 @@ internal class ServersViewModel( } } - fun onServerClicked(debugServer: DebugServer) { + private fun selectServer(debugServer: DebugServer) { viewModelScope.launch { val selectedServer = serversRepository.getSelectedServer() if (debugServer != selectedServer) { @@ -171,3 +188,5 @@ internal class ServersViewModel( } } } + +internal data class ServerSelectionMessageEvent(val serverName: String) : DebugEvent diff --git a/plugins/plugin-servers/src/main/kotlin/com/redmadrobot/debug/plugin/servers/ui/ServersViewState.kt b/plugins/plugin-servers/src/main/kotlin/com/redmadrobot/debug/plugin/servers/ui/ServersViewState.kt index bbd4a8f2..9aa65348 100644 --- a/plugins/plugin-servers/src/main/kotlin/com/redmadrobot/debug/plugin/servers/ui/ServersViewState.kt +++ b/plugins/plugin-servers/src/main/kotlin/com/redmadrobot/debug/plugin/servers/ui/ServersViewState.kt @@ -1,13 +1,17 @@ package com.redmadrobot.debug.plugin.servers.ui +import androidx.compose.runtime.Immutable import com.redmadrobot.debug.plugin.servers.data.model.DebugServer +@Immutable internal data class ServersViewState( val preInstalledServers: List = emptyList(), val addedServers: List = emptyList(), - val serverDialogState: ServerDialogState = ServerDialogState() + val serverDialogState: ServerDialogState = ServerDialogState(), + val isEditMode: Boolean = false ) +@Immutable internal data class ServerDialogState( val show: Boolean = false, val serverName: String = "", diff --git a/plugins/plugin-servers/src/main/res/values/strings.xml b/plugins/plugin-servers/src/main/res/values/strings.xml index 2f66365a..25125147 100644 --- a/plugins/plugin-servers/src/main/res/values/strings.xml +++ b/plugins/plugin-servers/src/main/res/values/strings.xml @@ -4,8 +4,11 @@ https://google.com Wrong host format. Must be filled - save + Save Default server Added servers Name + Add server + Edit Server + %s selected