Skip to content

Commit c64cde4

Browse files
committed
refactor(settings): rebuild settings information architecture
1 parent ffdb9b1 commit c64cde4

15 files changed

Lines changed: 1631 additions & 1092 deletions
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
package com.driezy.medlog.ui.screen.settings
2+
3+
import androidx.compose.foundation.layout.Arrangement
4+
import androidx.compose.foundation.layout.Column
5+
import androidx.compose.foundation.layout.Row
6+
import androidx.compose.foundation.layout.Spacer
7+
import androidx.compose.foundation.layout.fillMaxWidth
8+
import androidx.compose.foundation.layout.padding
9+
import androidx.compose.foundation.layout.size
10+
import androidx.compose.foundation.layout.width
11+
import androidx.compose.foundation.text.KeyboardOptions
12+
import androidx.compose.material3.FilledTonalButton
13+
import androidx.compose.material3.MaterialTheme
14+
import androidx.compose.material3.OutlinedButton
15+
import androidx.compose.material3.OutlinedTextField
16+
import androidx.compose.material3.SecondaryTabRow
17+
import androidx.compose.material3.Surface
18+
import androidx.compose.material3.Tab
19+
import androidx.compose.material3.Text
20+
import androidx.compose.runtime.Composable
21+
import androidx.compose.runtime.getValue
22+
import androidx.compose.runtime.mutableIntStateOf
23+
import androidx.compose.runtime.mutableStateOf
24+
import androidx.compose.runtime.setValue
25+
import androidx.compose.runtime.saveable.rememberSaveable
26+
import androidx.compose.ui.Alignment
27+
import androidx.compose.ui.Modifier
28+
import androidx.compose.ui.res.stringResource
29+
import androidx.compose.ui.text.font.FontWeight
30+
import androidx.compose.ui.text.input.KeyboardType
31+
import androidx.compose.ui.text.input.PasswordVisualTransformation
32+
import androidx.compose.ui.unit.dp
33+
import com.driezy.medlog.R
34+
import com.driezy.medlog.ui.icons.MedLogIcon
35+
import com.driezy.medlog.ui.icons.MedLogIcons
36+
import com.driezy.medlog.ui.theme.MedLogSpacing
37+
38+
@Composable
39+
internal fun ApiKeyManagementSection(
40+
uiState: SettingsUiState,
41+
onApiKeySave: (String) -> Unit,
42+
onApiKeyImport: (String) -> Unit,
43+
onApiKeyScan: () -> Unit,
44+
onApiKeyClear: () -> Unit,
45+
) {
46+
var apiKeyDraft by rememberSaveable(uiState.cloudAiProvider) { mutableStateOf("") }
47+
var apiKeyImportDraft by rememberSaveable { mutableStateOf("") }
48+
val apiKeyImportPresentation = CloudAiApiKeyImportPresentation.from(apiKeyImportDraft)
49+
var selectedTabIndex by rememberSaveable { mutableIntStateOf(0) }
50+
51+
Surface(
52+
modifier = Modifier.fillMaxWidth(),
53+
shape = MaterialTheme.shapes.extraLarge,
54+
color = MaterialTheme.colorScheme.surfaceContainerHigh,
55+
) {
56+
Column(
57+
modifier = Modifier.padding(MedLogSpacing.Medium),
58+
verticalArrangement = Arrangement.spacedBy(MedLogSpacing.Small),
59+
) {
60+
ApiKeyStatusHeader(
61+
uiState = uiState,
62+
onApiKeyClear = onApiKeyClear,
63+
)
64+
SecondaryTabRow(selectedTabIndex = selectedTabIndex) {
65+
Tab(
66+
selected = selectedTabIndex == 0,
67+
onClick = { selectedTabIndex = 0 },
68+
text = { Text(stringResource(R.string.settings_ai_api_key_tab_manual)) },
69+
)
70+
Tab(
71+
selected = selectedTabIndex == 1,
72+
onClick = { selectedTabIndex = 1 },
73+
text = { Text(stringResource(R.string.settings_ai_api_key_tab_import)) },
74+
)
75+
}
76+
when (selectedTabIndex) {
77+
0 -> ManualApiKeyPane(
78+
draft = apiKeyDraft,
79+
onDraftChange = { apiKeyDraft = it },
80+
onSave = {
81+
onApiKeySave(apiKeyDraft)
82+
apiKeyDraft = ""
83+
},
84+
)
85+
1 -> ApiKeyImportPane(
86+
draft = apiKeyImportDraft,
87+
presentation = apiKeyImportPresentation,
88+
onDraftChange = { apiKeyImportDraft = it },
89+
onScan = onApiKeyScan,
90+
onImport = {
91+
onApiKeyImport(apiKeyImportDraft)
92+
apiKeyImportDraft = ""
93+
},
94+
)
95+
}
96+
}
97+
}
98+
}
99+
100+
@Composable
101+
private fun ApiKeyStatusHeader(
102+
uiState: SettingsUiState,
103+
onApiKeyClear: () -> Unit,
104+
) {
105+
Row(
106+
modifier = Modifier.fillMaxWidth(),
107+
horizontalArrangement = Arrangement.SpaceBetween,
108+
verticalAlignment = Alignment.CenterVertically,
109+
) {
110+
Column(modifier = Modifier.weight(1f)) {
111+
Text(
112+
text = stringResource(R.string.settings_ai_api_key_title),
113+
style = MaterialTheme.typography.titleSmall,
114+
fontWeight = FontWeight.SemiBold,
115+
)
116+
Text(
117+
text = if (uiState.cloudAiProviderHasApiKey) {
118+
stringResource(R.string.settings_ai_api_key_configured, uiState.cloudAiProvider.providerName)
119+
} else {
120+
stringResource(R.string.settings_ai_api_key_missing, uiState.cloudAiProvider.providerName)
121+
},
122+
style = MaterialTheme.typography.bodySmall,
123+
color = MaterialTheme.colorScheme.onSurfaceVariant,
124+
)
125+
}
126+
if (uiState.cloudAiProviderHasApiKey) {
127+
OutlinedButton(onClick = onApiKeyClear) {
128+
Text(stringResource(R.string.settings_ai_api_key_clear))
129+
}
130+
}
131+
}
132+
}
133+
134+
@Composable
135+
private fun ManualApiKeyPane(
136+
draft: String,
137+
onDraftChange: (String) -> Unit,
138+
onSave: () -> Unit,
139+
) {
140+
Column(verticalArrangement = Arrangement.spacedBy(MedLogSpacing.Small)) {
141+
OutlinedTextField(
142+
value = draft,
143+
onValueChange = onDraftChange,
144+
modifier = Modifier.fillMaxWidth(),
145+
singleLine = true,
146+
label = { Text(stringResource(R.string.settings_ai_api_key_label)) },
147+
visualTransformation = PasswordVisualTransformation(),
148+
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
149+
supportingText = { Text(stringResource(R.string.settings_ai_api_key_storage_hint)) },
150+
)
151+
FilledTonalButton(
152+
onClick = onSave,
153+
enabled = draft.isNotBlank(),
154+
modifier = Modifier.align(Alignment.End),
155+
) {
156+
Text(stringResource(R.string.settings_ai_api_key_save))
157+
}
158+
}
159+
}
160+
161+
@Composable
162+
private fun ApiKeyImportPane(
163+
draft: String,
164+
presentation: CloudAiApiKeyImportPresentation,
165+
onDraftChange: (String) -> Unit,
166+
onScan: () -> Unit,
167+
onImport: () -> Unit,
168+
) {
169+
Column(verticalArrangement = Arrangement.spacedBy(MedLogSpacing.Small)) {
170+
OutlinedTextField(
171+
value = draft,
172+
onValueChange = onDraftChange,
173+
modifier = Modifier.fillMaxWidth(),
174+
minLines = 2,
175+
maxLines = 5,
176+
isError = presentation.visualState == CloudAiApiKeyImportVisualState.UNSUPPORTED,
177+
label = { Text(stringResource(R.string.settings_ai_api_key_import_label)) },
178+
supportingText = { ApiKeyImportSupportingText(presentation) },
179+
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
180+
)
181+
Row(
182+
modifier = Modifier.fillMaxWidth(),
183+
horizontalArrangement = Arrangement.spacedBy(MedLogSpacing.Small, Alignment.End),
184+
verticalAlignment = Alignment.CenterVertically,
185+
) {
186+
OutlinedButton(onClick = onScan) {
187+
MedLogIcon(
188+
MedLogIcons.QrCodeScanner,
189+
contentDescription = null,
190+
modifier = Modifier.size(16.dp),
191+
)
192+
Spacer(Modifier.width(MedLogSpacing.Tiny))
193+
Text(stringResource(R.string.settings_ai_api_key_import_scan))
194+
}
195+
FilledTonalButton(
196+
onClick = onImport,
197+
enabled = presentation.canImport,
198+
) {
199+
Text(stringResource(R.string.settings_ai_api_key_import_save))
200+
}
201+
}
202+
}
203+
}
204+
205+
@Composable
206+
private fun ApiKeyImportSupportingText(
207+
presentation: CloudAiApiKeyImportPresentation,
208+
) {
209+
when (presentation.visualState) {
210+
CloudAiApiKeyImportVisualState.EMPTY ->
211+
Text(stringResource(R.string.settings_ai_api_key_import_hint))
212+
CloudAiApiKeyImportVisualState.UNSUPPORTED ->
213+
Text(stringResource(R.string.settings_ai_api_key_import_unsupported))
214+
CloudAiApiKeyImportVisualState.READY -> {
215+
val model = presentation.model
216+
Text(
217+
text = if (model.isNullOrBlank()) {
218+
stringResource(
219+
R.string.settings_ai_api_key_import_preview,
220+
presentation.providerName.orEmpty(),
221+
)
222+
} else {
223+
stringResource(
224+
R.string.settings_ai_api_key_import_preview_with_model,
225+
presentation.providerName.orEmpty(),
226+
model,
227+
)
228+
},
229+
)
230+
}
231+
}
232+
}

0 commit comments

Comments
 (0)