AddServerContent.kt 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. package org.sirekanyan.outline.ui
  2. import androidx.compose.foundation.layout.Column
  3. import androidx.compose.foundation.layout.Row
  4. import androidx.compose.foundation.layout.fillMaxWidth
  5. import androidx.compose.foundation.layout.padding
  6. import androidx.compose.foundation.text.KeyboardActions
  7. import androidx.compose.foundation.text.KeyboardOptions
  8. import androidx.compose.material3.Checkbox
  9. import androidx.compose.material3.LocalContentColor
  10. import androidx.compose.material3.MaterialTheme
  11. import androidx.compose.material3.OutlinedTextField
  12. import androidx.compose.material3.Text
  13. import androidx.compose.runtime.Composable
  14. import androidx.compose.runtime.LaunchedEffect
  15. import androidx.compose.runtime.MutableState
  16. import androidx.compose.runtime.getValue
  17. import androidx.compose.runtime.mutableStateOf
  18. import androidx.compose.runtime.remember
  19. import androidx.compose.runtime.saveable.rememberSaveable
  20. import androidx.compose.runtime.setValue
  21. import androidx.compose.ui.Alignment
  22. import androidx.compose.ui.Modifier
  23. import androidx.compose.ui.draw.alpha
  24. import androidx.compose.ui.focus.FocusRequester
  25. import androidx.compose.ui.focus.focusRequester
  26. import androidx.compose.ui.platform.LocalContext
  27. import androidx.compose.ui.text.input.ImeAction
  28. import androidx.compose.ui.unit.dp
  29. import kotlinx.coroutines.CoroutineScope
  30. import kotlinx.coroutines.launch
  31. import org.sirekanyan.outline.NotSupportedContent
  32. import org.sirekanyan.outline.Router
  33. import org.sirekanyan.outline.SelectedPage
  34. import org.sirekanyan.outline.api.model.createServerEntity
  35. import org.sirekanyan.outline.app
  36. import org.sirekanyan.outline.ext.rememberStateScope
  37. import org.sirekanyan.outline.repository.ServerRepository
  38. import javax.net.ssl.SSLException
  39. @Composable
  40. private fun rememberAddServerState(router: Router): AddServerState {
  41. val context = LocalContext.current
  42. val scope = rememberStateScope()
  43. val servers = remember { context.app().serverRepository }
  44. val draft = rememberSaveable { mutableStateOf("") }
  45. val insecure = rememberSaveable { mutableStateOf(false) }
  46. return remember { AddServerState(scope, router, servers, draft, insecure) }
  47. }
  48. private class AddServerState(
  49. private val scope: CoroutineScope,
  50. private val router: Router,
  51. private val servers: ServerRepository,
  52. draftState: MutableState<String>,
  53. insecureState: MutableState<Boolean>,
  54. ) {
  55. var draft by draftState
  56. var insecure by insecureState
  57. var error by mutableStateOf("")
  58. var isLoading by mutableStateOf(false)
  59. private set
  60. var isDialogVisible by mutableStateOf(false)
  61. fun onAddClicked() {
  62. if (draft.startsWith("ss://")) {
  63. isDialogVisible = true
  64. return
  65. }
  66. scope.launch {
  67. updateServer()
  68. }
  69. }
  70. private suspend fun updateServer() {
  71. try {
  72. isLoading = true
  73. val server = servers.updateServer(createServerEntity(draft, insecure))
  74. router.dialog = null
  75. router.page = SelectedPage(server)
  76. router.closeDrawer(animated = false)
  77. } catch (exception: SSLException) {
  78. exception.printStackTrace()
  79. error = "Cannot establish a secure connection"
  80. } catch (exception: Exception) {
  81. exception.printStackTrace()
  82. error = "Check URL or try again"
  83. } finally {
  84. isLoading = false
  85. }
  86. }
  87. }
  88. @Composable
  89. fun AddServerContent(router: Router) {
  90. val state = rememberAddServerState(router)
  91. Column {
  92. DialogToolbar(
  93. title = "Add server",
  94. onCloseClick = { router.dialog = null },
  95. action = "Add" to { state.onAddClicked() },
  96. isLoading = state.isLoading,
  97. )
  98. val focusRequester = remember { FocusRequester() }
  99. OutlinedTextField(
  100. value = state.draft,
  101. onValueChange = {
  102. state.draft = it.trim()
  103. state.error = ""
  104. },
  105. modifier = Modifier
  106. .fillMaxWidth()
  107. .padding(16.dp, 24.dp, 16.dp, 8.dp)
  108. .focusRequester(focusRequester),
  109. label = { Text("Management API URL") },
  110. placeholder = { Text("https://xx.xx.xx.xx:xxx/xxxxx", Modifier.alpha(0.38f)) },
  111. isError = state.error.isNotEmpty(),
  112. supportingText = { Text(state.error) },
  113. maxLines = 4,
  114. keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
  115. keyboardActions = KeyboardActions(onDone = { state.onAddClicked() }),
  116. )
  117. LaunchedEffect(Unit) {
  118. focusRequester.requestFocus()
  119. }
  120. Row(verticalAlignment = Alignment.CenterVertically) {
  121. Checkbox(
  122. checked = state.insecure,
  123. onCheckedChange = { state.insecure = it },
  124. )
  125. Text(
  126. text = "Allow insecure connection",
  127. modifier = Modifier.padding(end = 16.dp),
  128. style = MaterialTheme.typography.bodySmall,
  129. color = LocalContentColor.current.copy(alpha = 0.66f),
  130. )
  131. }
  132. }
  133. if (state.isDialogVisible) {
  134. NotSupportedContent(onDismissRequest = { state.isDialogVisible = false })
  135. }
  136. }