git.iko.soy/opencode-android
on a40d3a2 help sign in
175 lines 8.3 kB history raw
1package soy.iko.opencode.ui.server
3import androidx.compose.animation.AnimatedVisibility
4import androidx.compose.foundation.layout.Arrangement
5import androidx.compose.foundation.layout.Box
6import androidx.compose.foundation.layout.Column
7import androidx.compose.foundation.layout.fillMaxSize
8import androidx.compose.foundation.layout.fillMaxWidth
9import androidx.compose.foundation.layout.imePadding
10import androidx.compose.foundation.layout.padding
11import androidx.compose.foundation.layout.size
12import androidx.compose.foundation.layout.widthIn
13import androidx.compose.foundation.rememberScrollState
14import androidx.compose.foundation.text.KeyboardActions
15import androidx.compose.foundation.text.KeyboardOptions
16import androidx.compose.foundation.verticalScroll
17import androidx.compose.material.icons.Icons
18import androidx.compose.material.icons.automirrored.filled.ArrowBack
19import androidx.compose.material3.Button
20import androidx.compose.material3.CircularProgressIndicator
21import androidx.compose.material3.ExperimentalMaterial3Api
22import androidx.compose.material3.Icon
23import androidx.compose.material3.IconButton
24import androidx.compose.material3.MaterialTheme
25import androidx.compose.material3.OutlinedButton
26import androidx.compose.material3.OutlinedTextField
27import androidx.compose.material3.Scaffold
28import androidx.compose.material3.Text
29import androidx.compose.material3.TopAppBar
30import androidx.compose.runtime.Composable
31import androidx.compose.runtime.getValue
32import androidx.compose.ui.Alignment
33import androidx.compose.ui.Modifier
34import androidx.compose.ui.platform.testTag
35import androidx.compose.ui.semantics.contentDescription
36import androidx.compose.ui.semantics.semantics
37import androidx.compose.ui.text.input.ImeAction
38import androidx.compose.ui.text.input.KeyboardType
39import androidx.compose.ui.text.input.PasswordVisualTransformation
40import androidx.compose.ui.res.stringResource
41import androidx.compose.ui.unit.dp
42import androidx.lifecycle.compose.collectAsStateWithLifecycle
43import androidx.lifecycle.viewmodel.compose.viewModel
44import soy.iko.opencode.di.AppContainer
45import soy.iko.opencode.R
46import soy.iko.opencode.ui.vmFactory
48@OptIn(ExperimentalMaterial3Api::class)
49@Composable
50fun ServerEditScreen(
51 container: AppContainer,
52 profileId: String?,
53 onDone: () -> Unit,
54) {
55 val vm: ServerEditViewModel =
56 viewModel(factory = vmFactory { ServerEditViewModel(container, profileId) })
57 val state by vm.state.collectAsStateWithLifecycle()
59 Scaffold(
60 topBar = {
61 TopAppBar(
62 title = { Text(if (state.isNew) stringResource(R.string.add_server) else stringResource(R.string.edit_server)) },
63 navigationIcon = {
64 IconButton(onClick = onDone) {
65 Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back))
66 }
67 },
68 )
69 },
70 ) { padding ->
71 if (!state.loaded) {
72 val loadingLabel = stringResource(R.string.loading)
73 Box(modifier = Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.Center) {
74 CircularProgressIndicator(Modifier.semantics { contentDescription = loadingLabel })
75 }
76 } else {
77 Column(
78 modifier = Modifier
79 .padding(padding)
80 .imePadding()
81 .widthIn(max = 600.dp)
82 .padding(16.dp)
83 .verticalScroll(rememberScrollState()),
84 verticalArrangement = Arrangement.spacedBy(12.dp),
85 ) {
86 OutlinedTextField(
87 value = state.label,
88 onValueChange = { v -> vm.update { it.copy(label = v) } },
89 label = { Text(stringResource(R.string.label_optional)) },
90 singleLine = true,
91 keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
92 modifier = Modifier.fillMaxWidth().testTag("server_label"),
93 )
94 OutlinedTextField(
95 value = state.baseUrl,
96 onValueChange = { v -> vm.update { it.copy(baseUrl = v, authFieldsVisible = false, error = null) } },
97 label = { Text(stringResource(R.string.base_url)) },
98 placeholder = { Text(stringResource(R.string.base_url_hint)) },
99 singleLine = true,
100 keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri, imeAction = ImeAction.Next),
101 modifier = Modifier.fillMaxWidth().testTag("server_url"),
102 )
103 // Connectivity check — probes the server without credentials to detect
104 // whether auth is required. Auth fields are only revealed afterwards (or
105 // when editing a profile that already has saved credentials).
106 OutlinedButton(
107 onClick = { vm.probe() },
108 enabled = state.canSave && !state.probing && !state.saving,
109 modifier = Modifier.fillMaxWidth().testTag("server_probe"),
110 ) {
111 if (state.probing) {
112 CircularProgressIndicator(
113 modifier = Modifier.size(20.dp),
114 strokeWidth = 2.dp,
115 )
116 Text(stringResource(R.string.checking), modifier = Modifier.padding(start = 8.dp))
117 } else {
118 Text(stringResource(R.string.check_connectivity))
119 }
120 }
121 AnimatedVisibility(visible = state.authFieldsVisible) {
122 Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
123 Text(
124 stringResource(R.string.auth_required),
125 style = MaterialTheme.typography.bodySmall,
126 color = MaterialTheme.colorScheme.error,
127 )
128 OutlinedTextField(
129 value = state.username,
130 onValueChange = { v -> vm.update { it.copy(username = v) } },
131 label = { Text(stringResource(R.string.username_optional)) },
132 singleLine = true,
133 keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
134 modifier = Modifier.fillMaxWidth().testTag("server_username"),
135 )
136 OutlinedTextField(
137 value = state.password,
138 onValueChange = { v -> vm.update { it.copy(password = v) } },
139 label = { Text(stringResource(R.string.password_optional)) },
140 singleLine = true,
141 visualTransformation = PasswordVisualTransformation(),
142 keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Done),
143 keyboardActions = KeyboardActions(onDone = { if (state.canSave && !state.saving) vm.save(onDone) }),
144 modifier = Modifier.fillMaxWidth().testTag("server_password"),
145 )
146 }
147 }
148 Button(
149 onClick = { vm.save(onDone) },
150 enabled = state.canSave && !state.saving,
151 modifier = Modifier.fillMaxWidth(),
152 ) {
153 if (state.saving) {
154 CircularProgressIndicator(
155 modifier = Modifier.size(20.dp),
156 color = MaterialTheme.colorScheme.onPrimary,
157 strokeWidth = 2.dp,
158 )
159 Text(stringResource(R.string.saving), modifier = Modifier.padding(start = 8.dp))
160 } else {
161 Text(stringResource(R.string.save))
162 }
163 }
164 state.error?.let { message ->
165 Text(
166 message,
167 color = androidx.compose.material3.MaterialTheme.colorScheme.error,
168 style = androidx.compose.material3.MaterialTheme.typography.bodySmall,
169 modifier = Modifier.fillMaxWidth(),
170 )
171 }
172 }
173 }
174 }