package com.vgmlr.shim import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Clear import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.colorResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Popup import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner import kotlinx.coroutines.delay import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.ui.text.input.TextFieldValue @OptIn(ExperimentalMaterial3Api::class) @Composable fun InputBar( shimText: String, themeManager: ShimThemeManager, onShimTextChange: (String) -> Unit, hashtagText: TextFieldValue, allTags: List, tagAssociations: Map>, onHashtagTextChange: (TextFieldValue) -> Unit, onAdd: (String, TextFieldValue) -> Unit ) { val focusRequester = remember { FocusRequester() } val focusManager = LocalFocusManager.current val lifecycleOwner = LocalLifecycleOwner.current var showSuggestions by remember { mutableStateOf(false) } var lastSubmittedText by remember { mutableStateOf("") } val words = hashtagText.text.split(" ") val currentWord = words.lastOrNull() ?: "" val previousWord = if (words.size >= 2) words[words.size - 2] else "" DisposableEffect(lifecycleOwner) { val observer = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_RESUME) { focusRequester.requestFocus() } } lifecycleOwner.lifecycle.addObserver(observer) onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } } LaunchedEffect(Unit) { delay(100) focusRequester.requestFocus() } Column( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(topStart = 14.dp, topEnd = 14.dp)) .background(colorResource(id = R.color.input_bg_color)) ) { Row(verticalAlignment = Alignment.CenterVertically) { TextField( value = shimText, onValueChange = onShimTextChange, placeholder = { Text("Logic", color = colorResource(id = R.color.input_color).copy(alpha = 0.5f)) }, textStyle = TextStyle( fontSize = 16.sp, lineHeight = (themeManager.fontSize.floatValue * (themeManager.lineHeight.floatValue + themeManager.lineSpacing.floatValue)).sp, color = colorResource(id = R.color.input_color)), modifier = Modifier .weight(1f) .focusRequester(focusRequester) .padding(horizontal = 8.dp, vertical = 6.dp), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Down) }), colors = TextFieldDefaults.colors( focusedContainerColor = Color.Transparent, unfocusedContainerColor = Color.Transparent, focusedTextColor = colorResource(id = R.color.input_color), unfocusedTextColor = colorResource(id = R.color.input_color), cursorColor = colorResource(id = R.color.input_color), focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent ) ) } HorizontalDivider(modifier = Modifier.fillMaxWidth(), thickness = 2.dp, color = colorResource(id = R.color.input_spacer_color)) Row(verticalAlignment = Alignment.CenterVertically) { Box(modifier = Modifier.weight(1f)) { TextField( value = hashtagText, onValueChange = { if (lastSubmittedText.isNotEmpty() && it.text == lastSubmittedText) { return@TextField } if (it.text != lastSubmittedText && it.text.isNotEmpty()) { lastSubmittedText = "" } if (it.text.length <= 70) { val sanitized = it.copy(text = it.text.replace("#", "")) onHashtagTextChange(sanitized) val newWords = sanitized.text.split(" ") val ongoingWord = newWords.lastOrNull() ?: "" val prevWord = if (newWords.size >= 2) newWords[newWords.size - 2] else "" val isTyping = ongoingWord.isNotBlank() val hasAssociations = ongoingWord.isEmpty() && prevWord.isNotBlank() && !tagAssociations[prevWord].isNullOrEmpty() showSuggestions = isTyping || hasAssociations } }, placeholder = { Text("Tags", color = colorResource(id = R.color.input_color).copy(alpha = 0.5f)) }, modifier = Modifier .fillMaxWidth() .padding(horizontal = 8.dp, vertical = 4.dp), textStyle = TextStyle( color = colorResource(id = R.color.input_color), lineHeight = (themeManager.fontSize.floatValue * (themeManager.lineHeight.floatValue + themeManager.lineSpacing.floatValue)).sp, fontSize = 16.sp), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { if (shimText.isNotBlank()) { lastSubmittedText = hashtagText.text onAdd(shimText, hashtagText) focusRequester.requestFocus() } }), colors = TextFieldDefaults.colors( focusedContainerColor = Color.Transparent, unfocusedContainerColor = Color.Transparent, focusedTextColor = colorResource(id = R.color.input_color), unfocusedTextColor = colorResource(id = R.color.input_color), cursorColor = colorResource(id = R.color.input_color), focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent ), singleLine = true ) if (showSuggestions) { val suggestions = if (currentWord.isNotBlank()) { allTags.filter { it.startsWith(currentWord, ignoreCase = true) && it != currentWord } } else if (previousWord.isNotBlank()) { val alreadyTypedTags = words.filter { it.isNotBlank() }.toSet() tagAssociations[previousWord]?.filter { it !in alreadyTypedTags } ?: emptyList() } else { emptyList() } if (suggestions.isNotEmpty()) { Popup(alignment = Alignment.TopStart, offset = androidx.compose.ui.unit.IntOffset(120, -70)) { Surface( color = colorResource(id = R.color.suggestion_bg_color), shape = RoundedCornerShape(7.dp), shadowElevation = 4.dp ) { Row( modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), horizontalArrangement = Arrangement.spacedBy(16.dp) ) { val interactionSource = remember { MutableInteractionSource() } suggestions.take(3).forEach { tag -> Text( text = tag, color = colorResource(id = R.color.input_color), modifier = Modifier.clickable(interactionSource = interactionSource, indication = null) { val textWithoutLastWord = hashtagText.text.substringBeforeLast(" ", missingDelimiterValue = "") val newText = if (textWithoutLastWord.isEmpty()) "$tag " else "$textWithoutLastWord $tag " onHashtagTextChange( TextFieldValue( text = newText, selection = androidx.compose.ui.text.TextRange(newText.length) ) ) val currentTags = newText.split(" ").filter { it.isNotBlank() }.toSet() val availableAssoc = tagAssociations[tag]?.filter { it !in currentTags } showSuggestions = !availableAssoc.isNullOrEmpty() } ) } } } } } } } IconButton( onClick = { if (shimText.isNotBlank()) { lastSubmittedText = hashtagText.text onAdd(shimText, hashtagText) focusRequester.requestFocus() } }, modifier = Modifier .padding(end = 20.dp) .background(Color.White.copy(alpha = 0.1f), shape = androidx.compose.foundation.shape.CircleShape) ) { Icon(Icons.Default.Add, contentDescription = "Add", tint = colorResource(id = R.color.input_icon_color), modifier = Modifier.size(26.dp)) } } HorizontalDivider(modifier = Modifier.fillMaxWidth(), thickness = 2.dp, color = colorResource(id = R.color.input_spacer_color)) } } @OptIn(ExperimentalMaterial3Api::class) @Composable fun SearchField(query: String, onQueryChange: (String) -> Unit, onClose: () -> Unit) { Row( modifier = Modifier .fillMaxWidth() .height(54.dp) .background(colorResource(id = R.color.search_bg_color)), verticalAlignment = Alignment.CenterVertically ) { IconButton(onClick = { if (query.isEmpty()) onClose() else onQueryChange("") }) { Icon(Icons.Default.Clear, contentDescription = "Clear", tint = Color.White.copy(alpha = 0.6f)) } TextField( value = query, onValueChange = onQueryChange, placeholder = { Text("Search", color = Color.White.copy(alpha = 0.5f), fontSize = 18.sp) }, textStyle = TextStyle(fontSize = 18.sp), modifier = Modifier.weight(1f), colors = TextFieldDefaults.colors( focusedContainerColor = Color.Transparent, unfocusedContainerColor = Color.Transparent, focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, cursorColor = Color.White, focusedTextColor = Color.White, unfocusedTextColor = Color.White ), singleLine = true ) } } @OptIn(ExperimentalFoundationApi::class) @Composable fun ShimItem( shim: ShimClass, themeManager: ShimThemeManager, isSelected: Boolean = false, onHashtagClick: (String) -> Unit, onLongClick: () -> Unit = {}, onClick: () -> Unit = {} ) { Column( modifier = Modifier .fillMaxWidth() .background(if (isSelected) colorResource(id = R.color.select_bg_color) else Color.Transparent) .combinedClickable( onClick = onClick, onLongClick = onLongClick ) .padding(horizontal = 18.dp, vertical = 18.dp) ) { Row(modifier = Modifier.padding(vertical = 0.dp)) { shim.shimhash.split(" ").filter { it.isNotBlank() }.forEach { tag -> Text( text = tag, style = TextStyle( fontSize = themeManager.fontSize.floatValue.sp, lineHeight = (themeManager.fontSize.floatValue * (themeManager.lineHeight.floatValue + themeManager.lineSpacing.floatValue)).sp, color = themeManager.hashtagColor.value, fontFamily = if (themeManager.isMonospace.value) FontFamily.Monospace else FontFamily.Default ), modifier = Modifier .clickable { onHashtagClick(tag) } .padding(end = 8.dp) ) } } Spacer(modifier = Modifier.height(6.dp)) Text( text = shim.shimtext, style = TextStyle( fontSize = themeManager.fontSize.floatValue.sp, lineHeight = (themeManager.fontSize.floatValue * (themeManager.lineHeight.floatValue + themeManager.lineSpacing.floatValue)).sp, color = themeManager.textColor.value, fontFamily = if (themeManager.isMonospace.value) FontFamily.Monospace else FontFamily.Default, letterSpacing = 0.6.sp ) ) Spacer(modifier = Modifier.height(3.dp)) // Spacer(modifier = Modifier.height(10.dp)) // Text( // text = shim.shimtime, // style = TextStyle( // fontSize = (themeManager.fontSize.floatValue * 0.85).sp, // color = colorResource(id = R.color.timestamp_color), // fontFamily = if (themeManager.isMonospace.value) FontFamily.Monospace else FontFamily.Default // ) // ) } HorizontalDivider( modifier = Modifier .fillMaxWidth() .padding(top = 0.dp), thickness = 1.dp, color = colorResource(id = R.color.text_color).copy(alpha = 0.1f) ) }