1package soy.iko.opencode.ui.server
3import androidx.activity.compose.BackHandler
4import androidx.compose.animation.AnimatedVisibility
5import androidx.compose.foundation.layout.Arrangement
6import androidx.compose.foundation.layout.Box
7import androidx.compose.foundation.layout.Column
8import androidx.compose.foundation.layout.Row
9import androidx.compose.foundation.layout.fillMaxSize
10import androidx.compose.foundation.layout.fillMaxWidth
11import androidx.compose.foundation.layout.imePadding
12import androidx.compose.foundation.layout.padding
13import androidx.compose.foundation.layout.size
14import androidx.compose.foundation.layout.widthIn
15import androidx.compose.foundation.rememberScrollState
16import androidx.compose.foundation.text.KeyboardActions
17import androidx.compose.foundation.text.KeyboardOptions
18import androidx.compose.foundation.verticalScroll
19import androidx.compose.material.icons.Icons
20import androidx.compose.material.icons.automirrored.filled.ArrowBack
21import androidx.compose.material.icons.filled.Visibility
22import androidx.compose.material.icons.filled.VisibilityOff
23import androidx.compose.material3.AlertDialog
24import androidx.compose.material3.Button
25import androidx.compose.material3.CircularProgressIndicator
26import androidx.compose.material3.ExperimentalMaterial3Api
27import androidx.compose.material3.Icon
28import androidx.compose.material3.IconButton
29import androidx.compose.material3.MaterialTheme
30import androidx.compose.material3.OutlinedButton
31import androidx.compose.material3.OutlinedTextField
32import androidx.compose.material3.Scaffold
33import androidx.compose.material3.Text
34import androidx.compose.material3.TextButton
35import androidx.compose.material3.TopAppBar
36import androidx.compose.runtime.Composable
37import androidx.compose.runtime.getValue
38import androidx.compose.runtime.mutableStateOf
39import androidx.compose.runtime.remember
40import androidx.compose.runtime.saveable.rememberSaveable
41import androidx.compose.runtime.setValue
42import androidx.compose.ui.Alignment
43import androidx.compose.ui.Modifier
44import androidx.compose.ui.platform.testTag
45import androidx.compose.ui.semantics.contentDescription
46import androidx.compose.ui.semantics.semantics
47import androidx.compose.ui.text.input.ImeAction
48import androidx.compose.ui.text.input.KeyboardType
49import androidx.compose.ui.text.input.PasswordVisualTransformation
50import androidx.compose.ui.text.input.VisualTransformation
51import androidx.compose.ui.res.stringResource
52import androidx.compose.ui.unit.dp
53import androidx.lifecycle.compose.collectAsStateWithLifecycle
54import androidx.lifecycle.viewmodel.compose.viewModel
55import soy.iko.opencode.di.AppContainer
56import soy.iko.opencode.R
57import soy.iko.opencode.ui.vmFactory
59@OptIn(ExperimentalMaterial3Api::class)
62 container: AppContainer,
66 val vm: ServerEditViewModel =
67 viewModel(factory = vmFactory { ServerEditViewModel(container, profileId) })
68 val state by vm.state.collectAsStateWithLifecycle()
69 var showDiscardConfirm by rememberSaveable { mutableStateOf(false) }
70 var passwordVisible by rememberSaveable { mutableStateOf(false) }
73 if (state.isDirty && !state.saving) {
74 showDiscardConfirm = true
80 // Intercept the system back gesture when the form has unsaved changes so the user
81 // gets a chance to keep editing instead of losing their work silently.
82 BackHandler(enabled = state.isDirty && !state.saving && !showDiscardConfirm) {
83 showDiscardConfirm = true
89 title = { Text(if (state.isNew) stringResource(R.string.add_server) else stringResource(R.string.edit_server)) },
91 IconButton(onClick = ::safeExit) {
92 Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back))
99 val loadingLabel = stringResource(R.string.loading)
100 Box(modifier = Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.Center) {
101 CircularProgressIndicator(Modifier.semantics { contentDescription = loadingLabel })
106 passwordVisible = passwordVisible,
107 onTogglePassword = { passwordVisible = !passwordVisible },
115 if (showDiscardConfirm) {
117 onDismissRequest = { showDiscardConfirm = false },
118 title = { Text(stringResource(R.string.discard_title)) },
119 text = { Text(stringResource(R.string.discard_text)) },
121 TextButton(onClick = {
122 showDiscardConfirm = false
124 }) { Text(stringResource(R.string.discard), color = MaterialTheme.colorScheme.error) }
127 TextButton(onClick = { showDiscardConfirm = false }) { Text(stringResource(R.string.keep_editing)) }
134private fun ServerEditForm(
135 state: ServerEditState,
136 passwordVisible: Boolean,
137 onTogglePassword: () -> Unit,
138 padding: androidx.compose.foundation.layout.PaddingValues,
139 vm: ServerEditViewModel,
146 .widthIn(max = 600.dp)
148 .verticalScroll(rememberScrollState()),
149 verticalArrangement = Arrangement.spacedBy(12.dp),
153 onValueChange = { v -> vm.update { it.copy(label = v) } },
154 label = { Text(stringResource(R.string.label_optional)) },
156 keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
157 modifier = Modifier.fillMaxWidth().testTag("server_label"),
160 value = state.baseUrl,
161 onValueChange = { v -> vm.update { it.copy(baseUrl = v, error = null) } },
162 label = { Text(stringResource(R.string.base_url)) },
163 placeholder = { Text(stringResource(R.string.base_url_hint)) },
165 isError = state.baseUrl.isNotBlank() && !isValidUrl(state.baseUrl),
167 if (state.baseUrl.isNotBlank() && !isValidUrl(state.baseUrl)) {
168 Text(stringResource(R.string.invalid_url))
171 keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri, imeAction = ImeAction.Next),
172 modifier = Modifier.fillMaxWidth().testTag("server_url"),
175 onClick = { vm.probe() },
176 enabled = state.canSave && !state.probing && !state.saving,
177 modifier = Modifier.fillMaxWidth().testTag("server_probe"),
180 CircularProgressIndicator(
181 modifier = Modifier.size(20.dp),
184 Text(stringResource(R.string.checking), modifier = Modifier.padding(start = 8.dp))
186 Text(stringResource(R.string.check_connectivity))
189 if (!state.authFieldsVisible) {
191 stringResource(R.string.auth_help),
192 style = MaterialTheme.typography.bodySmall,
193 color = MaterialTheme.colorScheme.onSurfaceVariant,
194 modifier = Modifier.fillMaxWidth(),
197 AnimatedVisibility(visible = state.authFieldsVisible) {
200 passwordVisible = passwordVisible,
201 onTogglePassword = onTogglePassword,
202 onUpdate = vm::update,
203 onTestCredentials = vm::testCredentials,
204 onImeDone = { if (state.canSave && !state.saving) vm.saveAndConnect(onDone) },
208 modifier = Modifier.fillMaxWidth(),
209 horizontalArrangement = Arrangement.spacedBy(12.dp),
212 onClick = { vm.save(onDone) },
213 enabled = state.canSave && !state.saving,
214 modifier = Modifier.weight(1f),
217 CircularProgressIndicator(
218 modifier = Modifier.size(20.dp),
219 color = MaterialTheme.colorScheme.onPrimary,
222 Text(stringResource(R.string.saving), modifier = Modifier.padding(start = 8.dp))
224 Text(stringResource(R.string.save))
228 onClick = { vm.saveAndConnect(onDone) },
229 enabled = state.canSave && !state.saving,
230 modifier = Modifier.weight(1f),
232 Text(stringResource(R.string.save_and_connect))
235 state.error?.let { message ->
238 color = MaterialTheme.colorScheme.error,
239 style = MaterialTheme.typography.bodySmall,
240 modifier = Modifier.fillMaxWidth(),
247private fun AuthFields(
248 state: ServerEditState,
249 passwordVisible: Boolean,
250 onTogglePassword: () -> Unit,
251 onUpdate: ((ServerEditState) -> ServerEditState) -> Unit,
252 onTestCredentials: () -> Unit,
253 onImeDone: () -> Unit,
255 val showPasswordLabel = stringResource(R.string.show_password)
256 val hidePasswordLabel = stringResource(R.string.hide_password)
257 Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
259 stringResource(R.string.auth_required),
260 style = MaterialTheme.typography.bodySmall,
261 color = MaterialTheme.colorScheme.error,
264 value = state.username,
265 onValueChange = { v -> onUpdate { it.copy(username = v) } },
266 label = { Text(stringResource(R.string.username_optional)) },
268 keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
269 modifier = Modifier.fillMaxWidth().testTag("server_username"),
272 value = state.password,
273 onValueChange = { v -> onUpdate { it.copy(password = v) } },
274 label = { Text(stringResource(R.string.password_optional)) },
276 visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
278 IconButton(onClick = onTogglePassword) {
280 if (passwordVisible) Icons.Filled.VisibilityOff else Icons.Filled.Visibility,
281 contentDescription = if (passwordVisible) hidePasswordLabel else showPasswordLabel,
285 keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Done),
286 keyboardActions = KeyboardActions(onDone = { onImeDone() }),
287 modifier = Modifier.fillMaxWidth().testTag("server_password"),
290 onClick = onTestCredentials,
291 enabled = state.canSave && !state.testingCredentials && !state.saving,
292 modifier = Modifier.fillMaxWidth().testTag("server_test_creds"),
294 if (state.testingCredentials) {
295 CircularProgressIndicator(
296 modifier = Modifier.size(20.dp),
299 Text(stringResource(R.string.testing_credentials), modifier = Modifier.padding(start = 8.dp))
301 Text(stringResource(R.string.test_credentials))
304 state.credentialsResult?.let { ok ->
306 stringResource(if (ok) R.string.credentials_ok else R.string.credentials_rejected),
307 style = MaterialTheme.typography.bodySmall,
308 color = if (ok) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error,
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list