on 6e7976c help sign in
6 files changed+80−350
since splitsnapshots
unifiedsplit
app/build.gradle.kts+7−9 app/src/main/java/soy/iko/crush/MainActivity.kt−2 app/src/main/java/soy/iko/crush/data/CrushRepository.kt−10 app/src/main/java/soy/iko/crush/ui/ChatViewModel.kt−16 app/src/main/java/soy/iko/crush/ui/screens/ChatScreen.kt+63−246 app/src/main/java/soy/iko/crush/ui/screens/ConnectScreen.kt+10−67
app/build.gradle.kts+7−9
@@ -44,16 +44,14 @@ android {
4444 // (so CI can publish an installable APK). When the env vars are absent, the
4545 // release build falls back to unsigned — same behavior as before — so local
4646 // builds keep working without any keystore setup.
47− val releaseSigning = providers.environmentVariable("CRUSH_STORE_FILE").orNull
48− ?.takeIf { it.isNotBlank() }
49− ?.let { path ->
50− signingConfigs.create("release") {
51− storeFile = file(path)
52− storePassword = providers.environmentVariable("CRUSH_STORE_PASSWORD").orNull
53− keyAlias = providers.environmentVariable("CRUSH_KEY_ALIAS").orNull
54− keyPassword = providers.environmentVariable("CRUSH_KEY_PASSWORD").orNull
55− }
47+ val releaseSigning = providers.environmentVariable("CRUSH_STORE_FILE").orNull?.let { path ->
48+ signingConfigs.create("release") {
49+ storeFile = file(path)
50+ storePassword = providers.environmentVariable("CRUSH_STORE_PASSWORD").orNull
51+ keyAlias = providers.environmentVariable("CRUSH_KEY_ALIAS").orNull
52+ keyPassword = providers.environmentVariable("CRUSH_KEY_PASSWORD").orNull
5653 }
54+ }
5755
5856 buildTypes {
5957 debug {
app/src/main/java/soy/iko/crush/MainActivity.kt−2
@@ -31,14 +31,12 @@ class MainActivity : ComponentActivity() {
3131 onNewSession = vm::newSession,
3232 onPickSession = vm::openSession,
3333 onGrantPermission = vm::grantPermission,
34− onDisconnect = vm::disconnect,
3534 )
3635 } else {
3736 ConnectScreen(
3837 defaultHost = state.host,
3938 defaultWorkspace = state.workspacePath,
4039 error = state.error,
41− isConnecting = state.isConnecting,
4240 onConnect = vm::connect,
4341 )
4442 }
app/src/main/java/soy/iko/crush/data/CrushRepository.kt−10
@@ -70,16 +70,6 @@ class CrushRepository(
7070 return true
7171 }
7272
73− fun disconnect() {
74− _connection.value = ConnectionState.Disconnected
75− workspaceId = null
76− sessionId = null
77− _messages.value = emptyList()
78− _sessions.value = emptyList()
79− _permissions.value = emptyList()
80− _workspaces.value = emptyList()
81− }
82−
8373 suspend fun refreshWorkspaces() {
8474 _workspaces.value = runCatching { api.listWorkspaces() }.getOrDefault(emptyList())
8575 }
app/src/main/java/soy/iko/crush/ui/ChatViewModel.kt−16
@@ -31,7 +31,6 @@ data class ChatUiState(
3131 val messages: List<Message> = emptyList(),
3232 val pendingPermissions: List<PermissionRequest> = emptyList(),
3333 val isBusy: Boolean = false,
34− val isConnecting: Boolean = false,
3534 val error: String? = null,
3635)
3736
@@ -42,7 +41,6 @@ class ChatViewModel(
4241
4342 private val _error = MutableStateFlow<String?>(null)
4443 private val _busy = MutableStateFlow(false)
45− private val _connecting = MutableStateFlow(false)
4644
4745 private var sseJob: Job? = null
4846
@@ -57,7 +55,6 @@ class ChatViewModel(
5755 repo.permissions as Flow<Any>,
5856 _busy as Flow<Any>,
5957 _error as Flow<Any>,
60− _connecting as Flow<Any>,
6158 ) { values ->
6259 ChatUiState(
6360 connection = values[0] as ConnectionState,
@@ -70,7 +67,6 @@ class ChatViewModel(
7067 pendingPermissions = values[5] as List<PermissionRequest>,
7168 isBusy = values[6] as Boolean,
7269 error = values[7] as String?,
73− isConnecting = values[8] as Boolean,
7470 )
7571 }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), ChatUiState())
7672
@@ -79,34 +75,22 @@ class ChatViewModel(
7975 fun connect(host: String, workspacePath: String) {
8076 viewModelScope.launch(Dispatchers.IO) {
8177 _error.value = null
82− _connecting.value = true
8378 settings.setHost(host)
8479 settings.setWorkspace(workspacePath)
8580 val ok = repo.connect(host)
8681 if (!ok) {
8782 _error.value = "Could not reach crush server at $host"
88− _connecting.value = false
8983 return@launch
9084 }
9185 val ws = repo.openWorkspace(workspacePath)
9286 if (ws == null) {
9387 _error.value = "Could not open workspace '$workspacePath'"
94− _connecting.value = false
9588 return@launch
9689 }
97− _connecting.value = false
9890 startEventStream()
9991 }
10092 }
10193
102− fun disconnect() {
103− sseJob?.cancel()
104− repo.disconnect()
105− _busy.value = false
106− _connecting.value = false
107− _error.value = null
108− }
109−
11094 fun newSession() {
11195 viewModelScope.launch(Dispatchers.IO) {
11296 _error.value = null
app/src/main/java/soy/iko/crush/ui/screens/ChatScreen.kt+63−246
@@ -1,7 +1,6 @@
11package soy.iko.crush.ui.screens
22
33import androidx.compose.foundation.background
4−import androidx.compose.foundation.clickable
54import androidx.compose.foundation.layout.Arrangement
65import androidx.compose.foundation.layout.Box
76import androidx.compose.foundation.layout.Column
@@ -16,33 +15,28 @@ import androidx.compose.foundation.layout.widthIn
1615import androidx.compose.foundation.lazy.LazyColumn
1716import androidx.compose.foundation.lazy.items
1817import androidx.compose.foundation.lazy.rememberLazyListState
19−import androidx.compose.foundation.shape.CircleShape
2018import androidx.compose.foundation.shape.RoundedCornerShape
21−import androidx.compose.foundation.text.KeyboardActions
22−import androidx.compose.foundation.text.KeyboardOptions
2319import androidx.compose.material.icons.Icons
2420import androidx.compose.material.icons.automirrored.filled.Send
2521import androidx.compose.material.icons.filled.Add
26−import androidx.compose.material.icons.filled.MoreVert
2722import androidx.compose.material.icons.filled.Stop
2823import androidx.compose.material3.AlertDialog
2924import androidx.compose.material3.Button
25+import androidx.compose.material3.Card
26+import androidx.compose.material3.CardDefaults
3027import androidx.compose.material3.CircularProgressIndicator
31−import androidx.compose.material3.DropdownMenu
32−import androidx.compose.material3.DropdownMenuItem
3328import androidx.compose.material3.ExperimentalMaterial3Api
29+import androidx.compose.material3.FloatingActionButton
3430import androidx.compose.material3.HorizontalDivider
3531import androidx.compose.material3.Icon
3632import androidx.compose.material3.IconButton
3733import androidx.compose.material3.MaterialTheme
38−import androidx.compose.material3.ModalBottomSheet
3934import androidx.compose.material3.OutlinedTextField
4035import androidx.compose.material3.Scaffold
4136import androidx.compose.material3.Surface
4237import androidx.compose.material3.Text
4338import androidx.compose.material3.TextButton
4439import androidx.compose.material3.TopAppBar
45−import androidx.compose.material3.rememberModalBottomSheetState
4640import androidx.compose.runtime.Composable
4741import androidx.compose.runtime.LaunchedEffect
4842import androidx.compose.runtime.getValue
@@ -52,19 +46,12 @@ import androidx.compose.runtime.setValue
5246import androidx.compose.ui.Alignment
5347import androidx.compose.ui.Modifier
5448import androidx.compose.ui.draw.clip
55−import androidx.compose.ui.platform.LocalContext
5649import androidx.compose.ui.text.font.FontFamily
5750import androidx.compose.ui.text.font.FontWeight
58−import androidx.compose.ui.text.input.ImeAction
5951import androidx.compose.ui.unit.dp
60−import soy.iko.crush.proto.Message
6152import soy.iko.crush.proto.MessageRole
62−import soy.iko.crush.proto.PermissionRequest
63−import soy.iko.crush.proto.Session
6453import soy.iko.crush.ui.ChatUiState
6554import soy.iko.crush.ui.MessageRenderer
66−import java.text.DateFormat
67−import java.util.Date
6855
6956@OptIn(ExperimentalMaterial3Api::class)
7057@Composable
@@ -74,14 +61,13 @@ fun ChatScreen(
7461 onCancel: () -> Unit,
7562 onNewSession: () -> Unit,
7663 onPickSession: (String) -> Unit,
77− onGrantPermission: (PermissionRequest, Boolean) -> Unit,
78− onDisconnect: () -> Unit,
64+ onGrantPermission: (soy.iko.crush.proto.PermissionRequest, Boolean) -> Unit,
7965) {
8066 var input by remember { mutableStateOf("") }
8167 var showSessionPicker by remember { mutableStateOf(false) }
82− var showOverflow by remember { mutableStateOf(false) }
8368 val listState = rememberLazyListState()
8469
70+ // Auto-scroll to bottom when new messages arrive.
8571 LaunchedEffect(state.messages.size) {
8672 if (state.messages.isNotEmpty()) {
8773 listState.animateScrollToItem(state.messages.lastIndex)
@@ -93,12 +79,8 @@ fun ChatScreen(
9379 TopAppBar(
9480 title = {
9581 Column {
96− Row(verticalAlignment = Alignment.CenterVertically) {
97− Text("Crush", fontWeight = FontWeight.Bold)
98− Spacer(Modifier.size(6.dp))
99− StatusDot(state)
100− }
101− val subtitle = state.currentSessionId?.let { "session ${it.take(8)}" } ?: "no session"
82+ Text("Crush", fontWeight = FontWeight.Bold)
83+ val subtitle = state.currentSessionId?.let { "session $it" } ?: "no session"
10284 Text(
10385 subtitle,
10486 style = MaterialTheme.typography.labelSmall,
@@ -119,30 +101,6 @@ fun ChatScreen(
119101 IconButton(onClick = { showSessionPicker = true }) {
120102 Icon(Icons.Filled.Add, contentDescription = "Sessions")
121103 }
122− Box {
123− IconButton(onClick = { showOverflow = true }) {
124− Icon(Icons.Filled.MoreVert, contentDescription = "More")
125− }
126− DropdownMenu(
127− expanded = showOverflow,
128− onDismissRequest = { showOverflow = false },
129− ) {
130− DropdownMenuItem(
131− text = { Text("Switch session") },
132− onClick = {
133− showOverflow = false
134− showSessionPicker = true
135− },
136− )
137− DropdownMenuItem(
138− text = { Text("Disconnect") },
139− onClick = {
140− showOverflow = false
141− onDisconnect()
142− },
143− )
144− }
145− }
146104 },
147105 )
148106 },
@@ -168,28 +126,20 @@ fun ChatScreen(
168126 if (state.error != null) {
169127 ErrorBanner(state.error)
170128 }
171− if (state.messages.isEmpty() && !state.isBusy) {
172− EmptyConversation(
173− modifier = Modifier
174− .weight(1f)
175− .fillMaxWidth(),
176− )
177− } else {
178− LazyColumn(
179− state = listState,
180− modifier = Modifier
181− .weight(1f)
182− .fillMaxWidth()
183− .padding(horizontal = 12.dp),
184− verticalArrangement = Arrangement.spacedBy(8.dp),
185− contentPadding = androidx.compose.foundation.layout.PaddingValues(vertical = 12.dp),
186− ) {
187− items(state.messages, key = { it.id }) { msg ->
188− MessageBubble(msg)
189− }
190− if (state.isBusy && state.messages.isEmpty()) {
191− item { TypingIndicator() }
192− }
129+ LazyColumn(
130+ state = listState,
131+ modifier = Modifier
132+ .weight(1f)
133+ .fillMaxWidth()
134+ .padding(horizontal = 12.dp),
135+ verticalArrangement = Arrangement.spacedBy(8.dp),
136+ contentPadding = androidx.compose.foundation.layout.PaddingValues(vertical = 12.dp),
137+ ) {
138+ items(state.messages, key = { it.id }) { msg ->
139+ MessageBubble(msg)
140+ }
141+ if (state.isBusy && state.messages.isEmpty()) {
142+ item { TypingIndicator() }
193143 }
194144 }
195145 PermissionQueue(
@@ -217,74 +167,27 @@ fun ChatScreen(
217167}
218168
219169@Composable
220−private fun StatusDot(state: ChatUiState) {
221− val color = when {
222− state.isBusy -> MaterialTheme.colorScheme.tertiary
223− state.currentSessionId != null -> MaterialTheme.colorScheme.primary
224− else -> MaterialTheme.colorScheme.outline
225− }
226− Box(
227− modifier = Modifier
228− .size(8.dp)
229− .clip(CircleShape)
230− .background(color),
231− )
232−}
233−
234−@Composable
235−private fun MessageBubble(message: Message) {
170+private fun MessageBubble(message: soy.iko.crush.proto.Message) {
236171 val isUser = message.role == MessageRole.user
237172 val bg = if (isUser) MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)
238173 else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
239174 val alignment = if (isUser) Alignment.End else Alignment.Start
240− val context = LocalContext.current
241175
242176 Column(
243177 modifier = Modifier.fillMaxWidth(),
244178 horizontalAlignment = alignment,
245179 ) {
246− Row(
247− verticalAlignment = Alignment.CenterVertically,
248− horizontalArrangement = Arrangement.spacedBy(6.dp),
180+ Text(
181+ MessageRenderer.roleLabel(message.role),
182+ style = MaterialTheme.typography.labelSmall,
183+ color = MaterialTheme.colorScheme.onSurfaceVariant,
249184 modifier = Modifier.padding(bottom = 2.dp, start = 4.dp, end = 4.dp),
250− ) {
251− Text(
252− MessageRenderer.roleLabel(message.role),
253− style = MaterialTheme.typography.labelSmall,
254− color = MaterialTheme.colorScheme.onSurfaceVariant,
255− )
256− if (message.createdAt > 0) {
257− Text(
258− "·",
259− style = MaterialTheme.typography.labelSmall,
260− color = MaterialTheme.colorScheme.onSurfaceVariant,
261− )
262− Text(
263− formatTimestamp(message.createdAt),
264− style = MaterialTheme.typography.labelSmall,
265− color = MaterialTheme.colorScheme.onSurfaceVariant,
266− )
267− }
268− }
185+ )
269186 Box(
270187 modifier = Modifier
271188 .widthIn(max = 340.dp)
272189 .clip(RoundedCornerShape(16.dp))
273190 .background(bg)
274− .clickable(
275− enabled = message.parts.any { it.type == soy.iko.crush.proto.PartType.Text },
276− onClick = {
277− val text = MessageRenderer.renderText(message)
278− if (text.isNotEmpty()) {
279− val clipboard =
280− context.getSystemService(android.content.Context.CLIPBOARD_SERVICE)
281− as android.content.ClipboardManager
282− clipboard.setPrimaryClip(
283− android.content.ClipData.newPlainText("Crush", text),
284− )
285− }
286− },
287− )
288191 .padding(12.dp),
289192 ) {
290193 val text = MessageRenderer.renderText(message)
@@ -297,6 +200,7 @@ private fun MessageBubble(message: Message) {
297200 color = MaterialTheme.colorScheme.onSurface,
298201 )
299202 } else if (message.parts.isNotEmpty()) {
203+ // Non-text parts (tool calls etc.) — render compactly.
300204 Text(
301205 message.parts.joinToString("\n") { MessageRenderer.renderPart(it) },
302206 style = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace),
@@ -328,14 +232,6 @@ private fun InputBar(
328232 placeholder = { Text("Message Crush…") },
329233 maxLines = 5,
330234 enabled = enabled,
331− keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send),
332− keyboardActions = KeyboardActions(
333− onSend = {
334− if (enabled && text.isNotBlank()) {
335− onSend()
336− }
337− },
338− ),
339235 )
340236 Spacer(Modifier.size(8.dp))
341237 IconButton(onClick = onSend, enabled = enabled && text.isNotBlank()) {
@@ -347,14 +243,16 @@ private fun InputBar(
347243
348244@Composable
349245private fun ErrorBanner(message: String) {
350− Surface(color = MaterialTheme.colorScheme.errorContainer, tonalElevation = 2.dp) {
246+ Box(
247+ modifier = Modifier
248+ .fillMaxWidth()
249+ .background(MaterialTheme.colorScheme.error.copy(alpha = 0.12f))
250+ .padding(8.dp),
251+ ) {
351252 Text(
352253 message,
353− color = MaterialTheme.colorScheme.onErrorContainer,
254+ color = MaterialTheme.colorScheme.error,
354255 style = MaterialTheme.typography.bodySmall,
355− modifier = Modifier
356− .fillMaxWidth()
357− .padding(12.dp),
358256 )
359257 }
360258}
@@ -371,36 +269,10 @@ private fun TypingIndicator() {
371269 }
372270}
373271
374−@Composable
375−private fun EmptyConversation(modifier: Modifier = Modifier) {
376− Column(
377− modifier = modifier,
378− horizontalAlignment = Alignment.CenterHorizontally,
379− verticalArrangement = Arrangement.Center,
380− ) {
381− Text(
382− "💬",
383− style = MaterialTheme.typography.displayMedium,
384− )
385− Spacer(Modifier.height(8.dp))
386− Text(
387− "Say hello to Crush",
388− style = MaterialTheme.typography.titleMedium,
389− color = MaterialTheme.colorScheme.onSurface,
390− )
391− Spacer(Modifier.height(4.dp))
392− Text(
393− "Send a message below to start the conversation.",
394− style = MaterialTheme.typography.bodySmall,
395− color = MaterialTheme.colorScheme.onSurfaceVariant,
396− )
397− }
398−}
399−
400272@Composable
401273private fun PermissionQueue(
402− requests: List<PermissionRequest>,
403− onGrant: (PermissionRequest, Boolean) -> Unit,
274+ requests: List<soy.iko.crush.proto.PermissionRequest>,
275+ onGrant: (soy.iko.crush.proto.PermissionRequest, Boolean) -> Unit,
404276) {
405277 if (requests.isEmpty()) return
406278 val req = requests.first()
@@ -419,96 +291,41 @@ private fun PermissionQueue(
419291 )
420292}
421293
422−@OptIn(ExperimentalMaterial3Api::class)
423294@Composable
424295private fun SessionPickerSheet(
425− sessions: List<Session>,
296+ sessions: List<soy.iko.crush.proto.Session>,
426297 currentId: String?,
427298 onPick: (String?) -> Unit,
428299 onNew: () -> Unit,
429300 onDismiss: () -> Unit,
430301) {
431− val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
432− ModalBottomSheet(
302+ AlertDialog(
433303 onDismissRequest = onDismiss,
434− sheetState = sheetState,
435− ) {
436− Column(
437− modifier = Modifier
438− .fillMaxWidth()
439− .padding(horizontal = 16.dp, vertical = 8.dp),
440− ) {
441− Row(
442− modifier = Modifier.fillMaxWidth(),
443− horizontalArrangement = Arrangement.SpaceBetween,
444− verticalAlignment = Alignment.CenterVertically,
445− ) {
446− Text(
447− "Sessions",
448− style = MaterialTheme.typography.titleLarge,
449− fontWeight = FontWeight.Bold,
450− )
451− TextButton(onClick = onNew) { Text("New session") }
452− }
453− Spacer(Modifier.height(8.dp))
454− if (sessions.isEmpty()) {
455− Text(
456− "No sessions yet",
457− color = MaterialTheme.colorScheme.onSurfaceVariant,
458− modifier = Modifier.padding(vertical = 16.dp),
459− )
460− } else {
304+ title = { Text("Sessions") },
305+ text = {
306+ Column {
461307 sessions.forEach { s ->
462− SessionRow(s, isActive = s.id == currentId, onClick = { onPick(s.id) })
463− HorizontalDivider()
308+ val active = s.id == currentId
309+ Text(
310+ s.title.ifBlank { s.id.take(8) },
311+ fontWeight = if (active) FontWeight.Bold else FontWeight.Normal,
312+ color = if (active) MaterialTheme.colorScheme.primary
313+ else MaterialTheme.colorScheme.onSurface,
314+ modifier = Modifier
315+ .fillMaxWidth()
316+ .padding(8.dp)
317+ .clip(RoundedCornerShape(8.dp))
318+ .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f))
319+ .padding(8.dp),
320+ )
321+ Spacer(Modifier.height(4.dp))
322+ }
323+ if (sessions.isEmpty()) {
324+ Text("No sessions yet", color = MaterialTheme.colorScheme.onSurfaceVariant)
464325 }
465326 }
466− Spacer(Modifier.height(16.dp))
467− }
468− }
469−}
470−
471−@Composable
472−private fun SessionRow(session: Session, isActive: Boolean, onClick: () -> Unit) {
473− val title = session.title.ifBlank { session.id.take(8) }
474− Column(
475− modifier = Modifier
476− .fillMaxWidth()
477− .clickable(onClick = onClick)
478− .padding(vertical = 12.dp, horizontal = 8.dp),
479− ) {
480− Row(
481− modifier = Modifier.fillMaxWidth(),
482− verticalAlignment = Alignment.CenterVertically,
483− ) {
484− Text(
485− title,
486− fontWeight = if (isActive) FontWeight.Bold else FontWeight.Normal,
487− color = if (isActive) MaterialTheme.colorScheme.primary
488− else MaterialTheme.colorScheme.onSurface,
489− style = MaterialTheme.typography.bodyLarge,
490− modifier = Modifier.weight(1f),
491− )
492− if (isActive) {
493− Box(
494− modifier = Modifier
495− .size(8.dp)
496− .clip(CircleShape)
497− .background(MaterialTheme.colorScheme.primary),
498− )
499− }
500− }
501− if (session.messageCount > 0) {
502− Text(
503− "${session.messageCount} messages",
504− style = MaterialTheme.typography.labelSmall,
505− color = MaterialTheme.colorScheme.onSurfaceVariant,
506− )
507− }
508− }
509−}
510−
511−private fun formatTimestamp(epochSeconds: Long): String {
512− val timeMs = epochSeconds * 1000L
513− return DateFormat.getTimeInstance(DateFormat.SHORT).format(Date(timeMs))
327+ },
328+ confirmButton = { Button(onNew) { Text("New session") } },
329+ dismissButton = { TextButton(onDismiss) { Text("Close") } },
330+ )
514331}
app/src/main/java/soy/iko/crush/ui/screens/ConnectScreen.kt+10−67
@@ -1,21 +1,13 @@
11package soy.iko.crush.ui.screens
22
3−import androidx.compose.foundation.background
43import androidx.compose.foundation.layout.Arrangement
5−import androidx.compose.foundation.layout.Box
64import androidx.compose.foundation.layout.Column
75import androidx.compose.foundation.layout.Spacer
86import androidx.compose.foundation.layout.fillMaxSize
97import androidx.compose.foundation.layout.fillMaxWidth
108import androidx.compose.foundation.layout.height
119import androidx.compose.foundation.layout.padding
12−import androidx.compose.foundation.layout.size
1310import androidx.compose.foundation.layout.widthIn
14−import androidx.compose.foundation.shape.RoundedCornerShape
15−import androidx.compose.foundation.text.KeyboardActions
16−import androidx.compose.foundation.text.KeyboardOptions
17−import androidx.compose.material.icons.Icons
18−import androidx.compose.material.icons.filled.RocketLaunch
1911import androidx.compose.material3.Button
2012import androidx.compose.material3.CircularProgressIndicator
2113import androidx.compose.material3.Icon
@@ -23,6 +15,8 @@ import androidx.compose.material3.MaterialTheme
2315import androidx.compose.material3.OutlinedTextField
2416import androidx.compose.material3.Surface
2517import androidx.compose.material3.Text
18+import androidx.compose.material.icons.Icons
19+import androidx.compose.material.icons.filled.Favorite
2620import androidx.compose.runtime.Composable
2721import androidx.compose.runtime.getValue
2822import androidx.compose.runtime.mutableStateOf
@@ -30,12 +24,11 @@ import androidx.compose.runtime.remember
3024import androidx.compose.runtime.setValue
3125import androidx.compose.ui.Alignment
3226import androidx.compose.ui.Modifier
33−import androidx.compose.ui.draw.clip
34−import androidx.compose.ui.graphics.Brush
3527import androidx.compose.ui.text.font.FontWeight
3628import androidx.compose.ui.text.input.ImeAction
3729import androidx.compose.ui.text.input.KeyboardCapitalization
3830import androidx.compose.ui.text.input.KeyboardType
31+import androidx.compose.ui.text.input.TextFieldValue
3932import androidx.compose.ui.unit.dp
4033import androidx.compose.ui.unit.sp
4134
@@ -44,7 +37,6 @@ fun ConnectScreen(
4437 defaultHost: String,
4538 defaultWorkspace: String,
4639 error: String?,
47− isConnecting: Boolean,
4840 onConnect: (host: String, workspace: String) -> Unit,
4941) {
5042 var host by remember { mutableStateOf(defaultHost.ifBlank { "http://192.168.1.100:8080" }) }
@@ -61,27 +53,12 @@ fun ConnectScreen(
6153 horizontalAlignment = Alignment.CenterHorizontally,
6254 verticalArrangement = Arrangement.Center,
6355 ) {
64− Box(
65− modifier = Modifier
66− .size(88.dp)
67− .clip(RoundedCornerShape(24.dp))
68− .background(
69− Brush.linearGradient(
70− listOf(
71− MaterialTheme.colorScheme.primary,
72− MaterialTheme.colorScheme.secondary,
73− ),
74− ),
75− ),
76− contentAlignment = Alignment.Center,
77− ) {
78− Icon(
79− imageVector = Icons.Filled.RocketLaunch,
80− contentDescription = null,
81− tint = MaterialTheme.colorScheme.onPrimary,
82− modifier = Modifier.size(44.dp),
83− )
84− }
56+ Icon(
57+ imageVector = Icons.Filled.Favorite,
58+ contentDescription = null,
59+ tint = MaterialTheme.colorScheme.primary,
60+ modifier = Modifier.height(72.dp),
61+ )
8562 Spacer(Modifier.height(12.dp))
8663 Text(
8764 "Crush",
@@ -106,37 +83,14 @@ fun ConnectScreen(
10683 value = host,
10784 onValueChange = { host = it },
10885 label = { Text("Server host") },
109− placeholder = { Text("http://192.168.1.100:8080") },
11086 singleLine = true,
111− enabled = !isConnecting,
112− keyboardOptions = KeyboardOptions(
113− keyboardType = KeyboardType.Uri,
114− capitalization = KeyboardCapitalization.None,
115− autoCorrectEnabled = false,
116− imeAction = ImeAction.Next,
117− ),
11887 modifier = Modifier.fillMaxWidth(),
11988 )
12089 OutlinedTextField(
12190 value = workspace,
12291 onValueChange = { workspace = it },
12392 label = { Text("Workspace path (on server)") },
124− placeholder = { Text("/home/iko/project") },
12593 singleLine = true,
126− enabled = !isConnecting,
127− keyboardOptions = KeyboardOptions(
128− keyboardType = KeyboardType.Ascii,
129− capitalization = KeyboardCapitalization.None,
130− autoCorrectEnabled = false,
131− imeAction = ImeAction.Done,
132− ),
133− keyboardActions = KeyboardActions(
134− onDone = {
135− if (!isConnecting && host.isNotBlank() && workspace.isNotBlank()) {
136− onConnect(host, workspace)
137− }
138− },
139− ),
14094 modifier = Modifier.fillMaxWidth(),
14195 )
14296 if (error != null) {
@@ -148,20 +102,9 @@ fun ConnectScreen(
148102 }
149103 Button(
150104 onClick = { onConnect(host, workspace) },
151− enabled = !isConnecting && host.isNotBlank() && workspace.isNotBlank(),
152105 modifier = Modifier.fillMaxWidth(),
153106 ) {
154− if (isConnecting) {
155− CircularProgressIndicator(
156− modifier = Modifier.size(18.dp),
157− strokeWidth = 2.dp,
158− color = MaterialTheme.colorScheme.onPrimary,
159− )
160− Spacer(Modifier.size(8.dp))
161− Text("Connecting…")
162− } else {
163− Text("Connect")
164− }
107+ Text("Connect")
165108 }
166109 }
167110 }