11package org.bitcoinppl.cove.flows.SettingsFlow
22
3+ import androidx.compose.foundation.background
34import androidx.compose.foundation.layout.Box
45import androidx.compose.foundation.layout.Column
56import androidx.compose.foundation.layout.Row
67import androidx.compose.foundation.layout.WindowInsets
78import androidx.compose.foundation.layout.asPaddingValues
89import androidx.compose.foundation.layout.fillMaxSize
910import androidx.compose.foundation.layout.fillMaxWidth
10- import androidx.compose.foundation.layout.height
1111import androidx.compose.foundation.layout.padding
1212import androidx.compose.foundation.layout.safeDrawing
13- import androidx.compose.foundation.layout.width
1413import androidx.compose.foundation.rememberScrollState
1514import androidx.compose.foundation.text.KeyboardActions
1615import androidx.compose.foundation.text.KeyboardOptions
1716import androidx.compose.foundation.verticalScroll
18- import androidx.compose.material3.Button
19- import androidx.compose.material3.CircularProgressIndicator
17+ import androidx.compose.material.icons.Icons
18+ import androidx.compose.material.icons.filled.Delete
2019import androidx.compose.material3.ExperimentalMaterial3Api
20+ import androidx.compose.material3.Icon
2121import androidx.compose.material3.MaterialTheme
2222import androidx.compose.material3.OutlinedTextField
2323import androidx.compose.material3.Scaffold
2424import androidx.compose.material3.SnackbarHost
2525import androidx.compose.material3.SnackbarHostState
26+ import androidx.compose.material3.SwipeToDismissBox
27+ import androidx.compose.material3.SwipeToDismissBoxValue
2628import androidx.compose.material3.Text
2729import androidx.compose.material3.TextButton
30+ import androidx.compose.material3.rememberSwipeToDismissBoxState
2831import androidx.compose.runtime.Composable
2932import androidx.compose.runtime.getValue
3033import androidx.compose.runtime.mutableStateOf
@@ -43,6 +46,7 @@ import androidx.compose.ui.unit.dp
4346import kotlinx.coroutines.launch
4447import org.bitcoinppl.cove.R
4548import org.bitcoinppl.cove.TaggedItem
49+ import org.bitcoinppl.cove.views.MaterialDivider
4650import org.bitcoinppl.cove.views.MaterialSection
4751import org.bitcoinppl.cove.views.SectionHeader
4852import org.bitcoinppl.cove_core.AppAlertState
@@ -64,17 +68,18 @@ fun OhttpRelaySettingsScreen(
6468 modifier : Modifier = Modifier ,
6569) {
6670 val config = remember { Database ().globalConfig() }
67- val scope = rememberCoroutineScope()
6871 val snackbarHostState = remember { SnackbarHostState () }
72+ val scope = rememberCoroutineScope()
6973 val keyboardController = LocalSoftwareKeyboardController .current
70- val savedMessage = stringResource(R .string.ohttp_relay_saved)
7174 val invalidUrlTitle = stringResource(R .string.ohttp_relay_invalid_url_title)
7275 val invalidUrlMessage = stringResource(R .string.ohttp_relay_invalid_url_message)
7376 val updateFailedTitle = stringResource(R .string.ohttp_relay_update_failed_title)
7477 val updateFailedMessage = stringResource(R .string.ohttp_relay_update_failed_message)
78+ val savedMessage = stringResource(R .string.ohttp_relay_saved)
7579
76- var input by remember { mutableStateOf(config.ohttpRelayUrl() ? : " " ) }
77- var isSaving by remember { mutableStateOf(false ) }
80+ var relays by remember { mutableStateOf(config.ohttpRelayUrls()) }
81+ var newInput by remember { mutableStateOf(" " ) }
82+ var isAdding by remember { mutableStateOf(false ) }
7883
7984 fun showAlert (
8085 title : String ,
@@ -89,41 +94,36 @@ fun OhttpRelaySettingsScreen(
8994 )
9095 }
9196
92- fun save () {
93- if (isSaving) return
94-
95- val inputToSave = input
96- scope.launch {
97- isSaving = true
98- try {
99- val normalized = config.setOhttpRelayUrl(inputToSave)
100- input = normalized ? : " "
101- keyboardController?.hide()
102- isSaving = false
103-
104- launch {
105- snackbarHostState.showSnackbar(savedMessage)
106- }
107- } catch (e: Exception ) {
108- if (e is DatabaseException .GlobalConfig &&
109- e.v1 is GlobalConfigTableException .InvalidOhttpRelayUrl
110- ) {
111- showAlert(invalidUrlTitle, invalidUrlMessage)
112- } else {
113- showAlert(updateFailedTitle, updateFailedMessage)
114- }
115- isSaving = false
97+ fun save (newRelays : List <String >, showSuccess : Boolean = true): Boolean {
98+ return try {
99+ relays = config.setOhttpRelayUrls(newRelays)
100+ newInput = " "
101+ isAdding = false
102+ keyboardController?.hide()
103+ if (showSuccess) scope.launch { snackbarHostState.showSnackbar(savedMessage) }
104+ true
105+ } catch (e: Exception ) {
106+ if (e is DatabaseException .GlobalConfig &&
107+ e.v1 is GlobalConfigTableException .InvalidOhttpRelayUrl
108+ ) {
109+ showAlert(invalidUrlTitle, invalidUrlMessage)
110+ } else {
111+ showAlert(updateFailedTitle, updateFailedMessage)
116112 }
113+ false
117114 }
118115 }
119116
120- fun reset () {
121- try {
122- config.clearOhttpRelayUrl()
123- input = " "
124- } catch (e: Exception ) {
125- showAlert(updateFailedTitle, updateFailedMessage)
126- }
117+ fun addRelay () {
118+ val url = newInput.trim()
119+ if (url.isEmpty()) return
120+ save(relays + url)
121+ }
122+
123+ fun deleteRelay (index : Int ): Boolean {
124+ val updated = relays.toMutableList()
125+ updated.removeAt(index)
126+ return save(updated, showSuccess = false )
127127 }
128128
129129 Scaffold (
@@ -136,18 +136,6 @@ fun OhttpRelaySettingsScreen(
136136 SettingsTopAppBar (
137137 title = stringResource(R .string.title_settings_ohttp_relay),
138138 onBack = { app.popRoute() },
139- actions = {
140- if (isSaving) {
141- Box (
142- modifier = Modifier .padding(end = 16 .dp),
143- contentAlignment = Alignment .Center ,
144- ) {
145- CircularProgressIndicator (
146- modifier = Modifier .width(24 .dp).height(24 .dp),
147- )
148- }
149- }
150- },
151139 )
152140 },
153141 ) { paddingValues ->
@@ -184,42 +172,115 @@ fun OhttpRelaySettingsScreen(
184172
185173 SectionHeader (stringResource(R .string.ohttp_relay_custom_section))
186174 MaterialSection {
187- Column (modifier = Modifier .padding(horizontal = 16 .dp, vertical = 12 .dp)) {
188- OutlinedTextField (
189- value = input,
190- onValueChange = { input = it },
191- label = { Text (stringResource(R .string.ohttp_relay_url_placeholder)) },
192- keyboardOptions =
193- KeyboardOptions (
194- capitalization = KeyboardCapitalization .None ,
195- imeAction = ImeAction .Done ,
196- keyboardType = KeyboardType .Uri ,
197- ),
198- keyboardActions = KeyboardActions (onDone = { save() }),
199- singleLine = true ,
200- modifier = Modifier .fillMaxWidth(),
201- )
175+ Column {
176+ relays.forEachIndexed { index, relay ->
177+ RelayItem (
178+ relay = relay,
179+ onDelete = { deleteRelay(index) },
180+ )
181+ if (index < relays.lastIndex) {
182+ MaterialDivider (indent = 16 .dp)
183+ }
184+ }
202185
203- Row (
204- modifier =
205- Modifier
206- .fillMaxWidth()
207- .padding(top = 12 .dp),
208- verticalAlignment = Alignment .CenterVertically ,
209- ) {
210- Button (
211- onClick = ::save,
212- enabled = ! isSaving,
186+ if (isAdding) {
187+ Row (
188+ modifier =
189+ Modifier
190+ .fillMaxWidth()
191+ .padding(horizontal = 16 .dp, vertical = 8 .dp),
192+ verticalAlignment = Alignment .CenterVertically ,
213193 ) {
214- Text (stringResource(R .string.ohttp_relay_save))
215- }
194+ OutlinedTextField (
195+ value = newInput,
196+ onValueChange = { newInput = it },
197+ label = {
198+ Text (stringResource(R .string.ohttp_relay_url_placeholder))
199+ },
200+ keyboardOptions =
201+ KeyboardOptions (
202+ capitalization = KeyboardCapitalization .None ,
203+ imeAction = ImeAction .Done ,
204+ keyboardType = KeyboardType .Uri ,
205+ ),
206+ keyboardActions = KeyboardActions (onDone = { addRelay() }),
207+ singleLine = true ,
208+ modifier = Modifier .weight(1f ),
209+ )
216210
217- TextButton (onClick = ::reset) {
218- Text (stringResource(R .string.ohttp_relay_reset))
211+ TextButton (
212+ onClick = ::addRelay,
213+ enabled = newInput.trim().isNotEmpty(),
214+ ) {
215+ Text (stringResource(R .string.ohttp_relay_add))
216+ }
217+ }
218+ } else {
219+ TextButton (
220+ onClick = { isAdding = true },
221+ modifier = Modifier .padding(horizontal = 8 .dp),
222+ ) {
223+ Text (stringResource(R .string.ohttp_relay_add_relay))
219224 }
220225 }
221226 }
222227 }
223228 }
224229 }
225230}
231+
232+ @OptIn(ExperimentalMaterial3Api ::class )
233+ @Composable
234+ private fun RelayItem (
235+ relay : String ,
236+ onDelete : () -> Boolean ,
237+ ) {
238+ val dismissState =
239+ rememberSwipeToDismissBoxState(
240+ confirmValueChange = { value ->
241+ if (value == SwipeToDismissBoxValue .EndToStart ) {
242+ onDelete()
243+ } else {
244+ false
245+ }
246+ },
247+ positionalThreshold = { it * 0.4f },
248+ )
249+
250+ SwipeToDismissBox (
251+ state = dismissState,
252+ backgroundContent = {
253+ Box (
254+ modifier =
255+ Modifier
256+ .fillMaxSize()
257+ .background(MaterialTheme .colorScheme.error)
258+ .padding(end = 16 .dp),
259+ contentAlignment = Alignment .CenterEnd ,
260+ ) {
261+ Icon (
262+ imageVector = Icons .Default .Delete ,
263+ contentDescription = null ,
264+ tint = MaterialTheme .colorScheme.onError,
265+ )
266+ }
267+ },
268+ enableDismissFromStartToEnd = false ,
269+ ) {
270+ Row (
271+ modifier =
272+ Modifier
273+ .fillMaxWidth()
274+ .background(MaterialTheme .colorScheme.surface)
275+ .padding(horizontal = 16 .dp, vertical = 12 .dp),
276+ verticalAlignment = Alignment .CenterVertically ,
277+ ) {
278+ Text (
279+ text = relay,
280+ style = MaterialTheme .typography.bodySmall,
281+ fontFamily = FontFamily .Monospace ,
282+ modifier = Modifier .weight(1f ),
283+ )
284+ }
285+ }
286+ }
0 commit comments