소스 검색

ai page improvements & selectable text

milan jurkulák 2 년 전
부모
커밋
ec74f43bbc

+ 6 - 6
src/main/java/eu/mjdev/desktop/helpers/gif/GifDecoder.java

@@ -418,11 +418,11 @@ public class GifDecoder {
                             break;
                         case 0xff:
                             readBlock();
-                            String app = "";
+                            StringBuilder app = new StringBuilder();
                             for (int i = 0; i < 11; i++) {
-                                app += (char) block[i];
+                                app.append((char) block[i]);
                             }
-                            if (app.equals("NETSCAPE2.0")) {
+                            if (app.toString().equals("NETSCAPE2.0")) {
                                 readNetscapeExt();
                             } else
                                 skip();
@@ -456,11 +456,11 @@ public class GifDecoder {
     }
 
     protected void readHeader() {
-        String id = "";
+        StringBuilder id = new StringBuilder();
         for (int i = 0; i < 6; i++) {
-            id += (char) read();
+            id.append((char) read());
         }
-        if (!id.startsWith("GIF")) {
+        if (!id.toString().startsWith("GIF")) {
             status = STATUS_FORMAT_ERROR;
             return;
         }

+ 120 - 34
src/main/kotlin/eu/mjdev/desktop/components/controlcenter/pages/AIPage.kt

@@ -1,43 +1,61 @@
 package eu.mjdev.desktop.components.controlcenter.pages
 
 import androidx.compose.desktop.ui.tooling.preview.Preview
+import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.*
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.onClick
 import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.OutlinedTextField
-import androidx.compose.material.Text
+import androidx.compose.material.Icon
 import androidx.compose.material.TextFieldDefaults
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Campaign
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalClipboardManager
+import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.unit.dp
 import eu.mjdev.desktop.components.controlcenter.base.ControlCenterPage
 import eu.mjdev.desktop.components.controlcenter.base.ControlCenterPageScope.Companion.remember
+import eu.mjdev.desktop.components.input.SelectableOutlineEditText
+import eu.mjdev.desktop.components.text.TextAny
 import eu.mjdev.desktop.extensions.Compose.rememberState
-import kotlinx.coroutines.launch
+import eu.mjdev.desktop.icons.Icons
 
+// todo copy only selected text to clipboard
+@OptIn(ExperimentalFoundationApi::class)
 @Suppress("FunctionName")
 fun AIPage() = ControlCenterPage(
-    icon = Icons.Filled.Campaign,
+    icon = Icons.Chat,
     name = "AI",
     condition = { ai.isAvailable() }
 ) {
+    val clipboardManager = LocalClipboardManager.current
+    val iconSize = 24.dp
     val questionsList = remember { mutableStateListOf<Pair<String, String>>() }
     val request = rememberState("")
-    val onDone: (String) -> Unit = { what ->
+    val iconSend = androidx.compose.runtime.remember {
+        derivedStateOf {
+            if (request.value.isNotEmpty()) Icons.SendIcon else null
+        }
+    }
+    val onDone: () -> Unit = {
+        val what = request.value.replace("\n", "")
         if (what.isNotEmpty()) {
             questionsList.add(Pair(what, ""))
+            request.value = ""
             api.ai.ask(what) { _, res ->
-                res.replace("* **", "").replace("**", "").let { resx ->
-                    questionsList.removeIf { it.first == what }
-                    questionsList.add(Pair(what, resx))
-                    talk(resx)
-                }
+                res.replace("* **", "")
+                    .replace("**", "")
+                    .let { resx ->
+                        questionsList.removeIf { it.first == what }
+                        questionsList.add(Pair(what, resx))
+                        talk(resx)
+                    }
             }
         }
     }
@@ -53,44 +71,84 @@ fun AIPage() = ControlCenterPage(
                 LazyColumn(
                     modifier = Modifier.fillMaxWidth()
                         .weight(1f)
-                        .background(Color.Black.copy(alpha = 0.3f), RoundedCornerShape(8.dp)),
+                        .background(
+                            Color.Black.copy(alpha = 0.3f),
+                            RoundedCornerShape(8.dp)
+                        )
                 ) {
                     itemsIndexed(questionsList) { idx, textData ->
                         Column(
                             modifier = Modifier.padding(4.dp)
                                 .fillMaxWidth()
-                                .background(Color.White.copy(alpha = 0.1f), RoundedCornerShape(8.dp))
+                                .background(
+                                    Color.White.copy(alpha = 0.1f),
+                                    RoundedCornerShape(8.dp)
+                                )
                                 .padding(4.dp),
                         ) {
-                            Text(
-                                modifier = Modifier.padding(4.dp)
+                            Box(
+                                Modifier.padding(4.dp)
                                     .fillMaxWidth()
-                                    .background(Color.White.copy(alpha = 0.1f), RoundedCornerShape(8.dp))
-                                    .padding(8.dp),
-                                text = "${idx + 1}. ${textData.first}",
-                                color = Color.White
-                            )
-                            Text(
+                                    .background(
+                                        Color.White.copy(alpha = 0.1f),
+                                        RoundedCornerShape(8.dp)
+                                    )
+                                    .padding(8.dp)
+                            ) {
+                                TextAny(
+                                    modifier = Modifier.fillMaxWidth().padding(end = 24.dp),
+                                    text = "${idx + 1}. ${textData.first}",
+                                    color = Color.White
+                                )
+                                Icon(
+                                    modifier = Modifier.padding(4.dp)
+                                        .size(iconSize)
+                                        .align(Alignment.TopEnd)
+                                        .onClick {
+                                            clipboardManager.setText(AnnotatedString(textData.first))
+                                        },
+                                    imageVector = Icons.CopyToClipboard,
+                                    tint = Color.White.copy(alpha = 0.9f),
+                                    contentDescription = ""
+                                )
+                            }
+                            Box(
                                 modifier = Modifier.padding(4.dp)
                                     .fillMaxWidth()
-                                    .background(Color.White.copy(alpha = 0.1f), RoundedCornerShape(8.dp))
+                                    .background(
+                                        Color.White.copy(alpha = 0.1f),
+                                        RoundedCornerShape(8.dp)
+                                    )
                                     .padding(8.dp),
-                                text = textData.second,
-                                color = Color.White.copy(alpha = 0.9f)
-                            )
+                            ) {
+                                TextAny(
+                                    modifier = Modifier.fillMaxWidth().padding(end = 24.dp),
+                                    text = textData.second,
+                                    color = Color.White.copy(alpha = 0.9f)
+                                )
+                                Icon(
+                                    modifier = Modifier.padding(4.dp)
+                                        .size(iconSize)
+                                        .align(Alignment.TopEnd)
+                                        .onClick {
+                                            clipboardManager.setText(AnnotatedString(textData.second))
+                                        },
+                                    imageVector = Icons.CopyToClipboard,
+                                    tint = Color.White.copy(alpha = 0.9f),
+                                    contentDescription = ""
+                                )
+                            }
                         }
                     }
                 }
-                OutlinedTextField(
-                    modifier = Modifier.padding(top = 8.dp).fillMaxWidth(),
+                SelectableOutlineEditText(
+                    modifier = Modifier.padding(top = 8.dp)
+                        .fillMaxWidth(),
                     value = request.value,
                     onValueChange = { t: String ->
                         request.value = t
                         if (t.contains("\n")) {
-                            scope.launch {
-                                onDone(t.replace("\n", ""))
-                            }
-                            request.value = ""
+                            onDone()
                         }
                     },
                     maxLines = 4,
@@ -102,7 +160,35 @@ fun AIPage() = ControlCenterPage(
                         cursorColor = Color.White,
                         focusedBorderColor = Color.White,
                         unfocusedBorderColor = Color.Black
-                    )
+                    ),
+                    trailingIcon = {
+                        Row(
+                            verticalAlignment = Alignment.CenterVertically,
+                        ) {
+                            if (request.value.isNotEmpty()) {
+                                Icon(
+                                    modifier = Modifier.padding(4.dp)
+                                        .size(iconSize)
+                                        .onClick {
+                                            clipboardManager.setText(AnnotatedString(request.value))
+                                        },
+                                    imageVector = Icons.CopyToClipboard,
+                                    tint = Color.White.copy(alpha = 0.9f),
+                                    contentDescription = ""
+                                )
+                            }
+                            if (iconSend.value != null) {
+                                Icon(
+                                    modifier = Modifier.padding(4.dp)
+                                        .size(iconSize)
+                                        .onClick { onDone() },
+                                    imageVector = iconSend.value!!,
+                                    tint = Color.White.copy(alpha = 0.9f),
+                                    contentDescription = ""
+                                )
+                            }
+                        }
+                    }
                 )
             }
         }
@@ -111,4 +197,4 @@ fun AIPage() = ControlCenterPage(
 
 @Preview
 @Composable
-fun AIPagePreview() =  AIPage().render()
+fun AIPagePreview() = AIPage().render()

+ 4 - 6
src/main/kotlin/eu/mjdev/desktop/components/input/PasswordTextView.kt

@@ -12,10 +12,6 @@ import androidx.compose.material.Icon
 import androidx.compose.material.OutlinedTextField
 import androidx.compose.material.TextFieldColors
 import androidx.compose.material.TextFieldDefaults
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.Send
-import androidx.compose.material.icons.filled.Visibility
-import androidx.compose.material.icons.filled.VisibilityOff
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.derivedStateOf
@@ -30,6 +26,7 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
 import androidx.compose.ui.text.input.VisualTransformation
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
+import eu.mjdev.desktop.icons.Icons
 
 @Suppress("FunctionName")
 @OptIn(ExperimentalFoundationApi::class)
@@ -50,12 +47,13 @@ fun PasswordTextView(
 ) {
     val iconVisible = remember {
         derivedStateOf {
-            if (passwordVisible.value) Icons.Filled.Visibility else Icons.Filled.VisibilityOff
+            if (passwordVisible.value) Icons.TextFieldVisiblePassword
+            else Icons.TextFieldInVisiblePassword
         }
     }
     val iconSend = remember {
         derivedStateOf {
-            if (password.value.isNotEmpty()) Icons.AutoMirrored.Filled.Send else null
+            if (password.value.isNotEmpty()) Icons.SendIcon else null
         }
     }
     val description = remember {

+ 111 - 0
src/main/kotlin/eu/mjdev/desktop/components/input/SelectableOutlineEditText.kt

@@ -0,0 +1,111 @@
+package eu.mjdev.desktop.components.input
+
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.material.LocalTextStyle
+import androidx.compose.material.OutlinedTextField
+import androidx.compose.material.TextFieldColors
+import androidx.compose.material.TextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.input.VisualTransformation
+
+@Composable
+fun SelectableOutlineEditText(
+    value: String,
+    onValueChange: (String) -> Unit,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    readOnly: Boolean = false,
+    textStyle: TextStyle = LocalTextStyle.current,
+    label: @Composable (() -> Unit)? = null,
+    placeholder: @Composable (() -> Unit)? = null,
+    leadingIcon: @Composable (() -> Unit)? = null,
+    trailingIcon: @Composable (() -> Unit)? = null,
+    isError: Boolean = false,
+    visualTransformation: VisualTransformation = VisualTransformation.None,
+    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
+    keyboardActions: KeyboardActions = KeyboardActions.Default,
+    singleLine: Boolean = false,
+    maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
+    minLines: Int = 1,
+    interactionSource: MutableInteractionSource? = null,
+    shape: Shape = TextFieldDefaults.OutlinedTextFieldShape,
+    colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors()
+) = SelectionContainer(
+    modifier = modifier
+) {
+    OutlinedTextField(
+        value = value,
+        onValueChange = onValueChange,
+        enabled = enabled,
+        readOnly = readOnly,
+        textStyle = textStyle,
+        label = label,
+        placeholder = placeholder,
+        leadingIcon = leadingIcon,
+        trailingIcon = trailingIcon,
+        isError = isError,
+        visualTransformation = visualTransformation,
+        keyboardOptions = keyboardOptions,
+        keyboardActions = keyboardActions,
+        maxLines = maxLines,
+        minLines = minLines,
+        interactionSource = interactionSource,
+        shape = shape,
+        colors = colors
+    )
+}
+
+@Suppress("unused")
+@Composable
+fun SelectableOutlineEditText(
+    value: TextFieldValue,
+    onValueChange: (TextFieldValue) -> Unit,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    readOnly: Boolean = false,
+    textStyle: TextStyle = LocalTextStyle.current,
+    label: @Composable (() -> Unit)? = null,
+    placeholder: @Composable (() -> Unit)? = null,
+    leadingIcon: @Composable (() -> Unit)? = null,
+    trailingIcon: @Composable (() -> Unit)? = null,
+    isError: Boolean = false,
+    visualTransformation: VisualTransformation = VisualTransformation.None,
+    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
+    keyboardActions: KeyboardActions = KeyboardActions.Default,
+    singleLine: Boolean = false,
+    maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
+    minLines: Int = 1,
+    interactionSource: MutableInteractionSource? = null,
+    shape: Shape = TextFieldDefaults.OutlinedTextFieldShape,
+    colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors()
+) = SelectionContainer(
+    modifier = modifier
+) {
+    OutlinedTextField(
+        value = value,
+        onValueChange = onValueChange,
+        enabled = enabled,
+        readOnly = readOnly,
+        textStyle = textStyle,
+        label = label,
+        placeholder = placeholder,
+        leadingIcon = leadingIcon,
+        trailingIcon = trailingIcon,
+        isError = isError,
+        visualTransformation = visualTransformation,
+        keyboardOptions = keyboardOptions,
+        keyboardActions = keyboardActions,
+        maxLines = maxLines,
+        minLines = minLines,
+        interactionSource = interactionSource,
+        shape = shape,
+        colors = colors
+    )
+}

+ 24 - 19
src/main/kotlin/eu/mjdev/desktop/components/text/TextAny.kt

@@ -1,6 +1,7 @@
 package eu.mjdev.desktop.components.text
 
 import androidx.compose.desktop.ui.tooling.preview.Preview
+import androidx.compose.foundation.text.selection.SelectionContainer
 import androidx.compose.material.LocalTextStyle
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
@@ -37,25 +38,29 @@ fun TextAny(
     maxLines: Int = Int.MAX_VALUE,
     onTextLayout: (TextLayoutResult) -> Unit = {},
     style: TextStyle = LocalTextStyle.current
-) = Text(
-    text = textFrom(text),
-    modifier = modifier,
-    color = color,
-    minLines = minLines,
-    fontSize = fontSize,
-    fontStyle = fontStyle,
-    fontWeight = fontWeight,
-    fontFamily = fontFamily,
-    letterSpacing = letterSpacing,
-    textDecoration = textDecoration,
-    textAlign = textAlign,
-    lineHeight = lineHeight,
-    overflow = overflow,
-    softWrap = softWrap,
-    maxLines = maxLines,
-    onTextLayout = onTextLayout,
-    style = style
-)
+) = SelectionContainer(
+    modifier = modifier
+) {
+    Text(
+        text = textFrom(text),
+        modifier = modifier,
+        color = color,
+        minLines = minLines,
+        fontSize = fontSize,
+        fontStyle = fontStyle,
+        fontWeight = fontWeight,
+        fontFamily = fontFamily,
+        letterSpacing = letterSpacing,
+        textDecoration = textDecoration,
+        textAlign = textAlign,
+        lineHeight = lineHeight,
+        overflow = overflow,
+        softWrap = softWrap,
+        maxLines = maxLines,
+        onTextLayout = onTextLayout,
+        style = style
+    )
+}
 
 @Preview
 @Composable

+ 21 - 0
src/main/kotlin/eu/mjdev/desktop/icons/Icons.kt

@@ -0,0 +1,21 @@
+package eu.mjdev.desktop.icons
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.Chat
+import androidx.compose.material.icons.automirrored.filled.Send
+import androidx.compose.material.icons.filled.ContentCopy
+import androidx.compose.material.icons.filled.Visibility
+import androidx.compose.material.icons.filled.VisibilityOff
+import androidx.compose.material.icons.automirrored.filled.TextSnippet
+import androidx.compose.material.icons.filled.Campaign
+
+@Suppress("unused")
+object Icons {
+    val SendIcon = Icons.AutoMirrored.Filled.Send
+    val TextFieldVisiblePassword = Icons.Filled.Visibility
+    val TextFieldInVisiblePassword = Icons.Filled.VisibilityOff
+    val CopyToClipboard = Icons.Filled.ContentCopy
+    val TextSnippet = Icons.AutoMirrored.Filled.TextSnippet
+    val Campaign = Icons.Filled.Campaign
+    val Chat = Icons.AutoMirrored.Filled.Chat
+}