diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/scripting/ScriptCatalog.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/scripting/ScriptCatalog.kt index e0d348e8..824344da 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/scripting/ScriptCatalog.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/scripting/ScriptCatalog.kt @@ -206,8 +206,45 @@ fun ScriptCatalog(root: ScriptingRootSection) { ) { item { if (isLoading) { - Box(modifier = Modifier.fillMaxWidth().padding(8.dp), contentAlignment = Alignment.Center) { - CircularProgressIndicator() + Box( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + contentAlignment = Alignment.Center + ) { + Surface( + shape = RoundedCornerShape(16.dp), + color = Color.White.copy(alpha = 0.06f), + tonalElevation = 0.dp, + shadowElevation = 0.dp, + border = BorderStroke( + 1.dp, + Brush.linearGradient( + listOf( + PurrfectPalette.glowPrimary.copy(alpha = 0.45f), + PurrfectPalette.glowSecondary.copy(alpha = 0.30f) + ) + ) + ) + ) { + Row( + modifier = Modifier.padding(horizontal = 14.dp, vertical = 10.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + CircularProgressIndicator( + modifier = Modifier.size(18.dp), + strokeWidth = 2.dp, + color = Color.White + ) + Text( + text = translation["loading"] ?: "Loading...", + color = Color.White, + fontWeight = FontWeight.SemiBold, + fontSize = 13.sp + ) + } + } } } else if (allScripts.isEmpty() && repositories.isNotEmpty()) { Box( diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json index 0c8ff5ba..093b1a7b 100644 --- a/common/src/main/assets/lang/en_US.json +++ b/common/src/main/assets/lang/en_US.json @@ -547,6 +547,8 @@ "no_settings_for_module": "This module does not have any settings", "open_module_failed": "Failed to open module file", "open_scripts_folder_button": "Open scripts folder", + "loaded_script": "Loaded script {name}", + "unloaded_script": "Unloaded script {name}", "script_already_installed": "Script already installed", "select_folder_button": "Choose Folder", "select_scripts_folder_toast": "Please select a scripts folder first", @@ -558,6 +560,7 @@ "no_repos_added": "No repositories added", "repo_list_info": "Find repositories here:", "link_text": "Repository list", + "loading": "Loading catalog...", "script_already_installed": "Script already installed", "script_downloaded": "Script downloaded", "could_not_create_file": "Could not create file", @@ -3530,6 +3533,11 @@ "toast_open_snap_failed": "Failed to open snap", "toast_mark_message_read_failed": "Failed to mark message as read. Check logs for more details", "toast_open_conversation_first": "You must open a conversation first", + "conversation_toolbox": { + "title": "Conversation Toolbox", + "loaded_script": "Loaded Script", + "failed_to_load": "Failed to load: {message}" + }, "toast_open_link_failed": "Failed to open link", "ai_response_style": { "casual": "Casual", diff --git a/common/src/main/kotlin/me/eternal/purrfectsnap/common/scripting/ui/InterfaceManager.kt b/common/src/main/kotlin/me/eternal/purrfectsnap/common/scripting/ui/InterfaceManager.kt index cebbd7e9..946a3c4b 100644 --- a/common/src/main/kotlin/me/eternal/purrfectsnap/common/scripting/ui/InterfaceManager.kt +++ b/common/src/main/kotlin/me/eternal/purrfectsnap/common/scripting/ui/InterfaceManager.kt @@ -2,7 +2,17 @@ package me.eternal.purrfectsnap.common.scripting.ui import android.app.Activity import android.app.AlertDialog +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Surface import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp import me.eternal.purrfectsnap.common.scripting.bindings.AbstractBinding import me.eternal.purrfectsnap.common.scripting.bindings.BindingSide import me.eternal.purrfectsnap.common.scripting.ktx.contextScope @@ -125,13 +135,46 @@ class InterfaceManager : AbstractBinding("interface-manager", BindingSide.COMMON @JSFunction fun createAlertDialog(activity: Activity, builder: (AlertDialog.Builder) -> Unit, callback: (interfaceBuilder: InterfaceBuilder, alertDialog: AlertDialog) -> Unit): AlertDialog { return createComposeAlertDialog(activity, builder = builder) { alertDialog -> - ScriptInterface(interfaceBuilder = remember { + val interfaceBuilder = remember { InterfaceBuilder().also { contextScope { callback(it, alertDialog) } } - }) + } + val shape = RoundedCornerShape(22.dp) + Surface( + modifier = Modifier + .fillMaxWidth() + .border( + width = 1.dp, + brush = Brush.linearGradient( + listOf( + Color(0xFF8C7BFF).copy(alpha = 0.52f), + Color(0xFF5FD8FF).copy(alpha = 0.30f) + ) + ), + shape = shape + ), + color = Color.Transparent, + shape = shape + ) { + androidx.compose.foundation.layout.Column( + modifier = Modifier + .background( + brush = Brush.linearGradient( + listOf( + Color(0xFF2A2452).copy(alpha = 0.97f), + Color(0xFF1A143A).copy(alpha = 0.94f), + ) + ), + shape = shape + ) + .padding(10.dp) + ) { + ScriptInterface(interfaceBuilder = interfaceBuilder) + } + } } } @@ -140,4 +183,4 @@ class InterfaceManager : AbstractBinding("interface-manager", BindingSide.COMMON } override fun getObject() = this -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/me/eternal/purrfectsnap/common/scripting/ui/ScriptInterface.kt b/common/src/main/kotlin/me/eternal/purrfectsnap/common/scripting/ui/ScriptInterface.kt index 37cf67ea..f6b62c42 100644 --- a/common/src/main/kotlin/me/eternal/purrfectsnap/common/scripting/ui/ScriptInterface.kt +++ b/common/src/main/kotlin/me/eternal/purrfectsnap/common/scripting/ui/ScriptInterface.kt @@ -1,18 +1,27 @@ package me.eternal.purrfectsnap.common.scripting.ui import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.material3.OutlinedButton +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField import androidx.compose.material3.Slider +import androidx.compose.material3.SliderDefaults import androidx.compose.material3.Switch import androidx.compose.material3.Text -import androidx.compose.material3.TextField import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.foundation.interaction.MutableInteractionSource import kotlinx.coroutines.launch import me.eternal.purrfectsnap.common.logger.AbstractLogger import me.eternal.purrfectsnap.common.scripting.ui.components.Node @@ -66,7 +75,7 @@ private fun DrawNode(node: Node) { Text( text = cachedAttributes["label"] as String, fontSize = (cachedAttributes["fontSize"]?.toString()?.toInt() ?: 14).sp, - color = (cachedAttributes["color"] as? Long)?.let { Color(it) } ?: Color.Unspecified + color = (cachedAttributes["color"] as? Long)?.let { Color(it) } ?: Color.White ) } @@ -130,7 +139,15 @@ private fun DrawNode(node: Node) { node.setAttribute("state", state) (cachedAttributes["callback"] as? (Boolean) -> Unit)?.let { it(state) } } - } + }, + colors = androidx.compose.material3.SwitchDefaults.colors( + checkedThumbColor = Color.White, + checkedTrackColor = Color(0xFF8C7BFF).copy(alpha = 0.66f), + uncheckedThumbColor = Color.White.copy(alpha = 0.9f), + uncheckedTrackColor = Color.White.copy(alpha = 0.20f), + checkedBorderColor = Color.Transparent, + uncheckedBorderColor = Color.Transparent + ) ) } NodeType.SLIDER -> { @@ -148,26 +165,55 @@ private fun DrawNode(node: Node) { }, valueRange = (cachedAttributes["min"] as Int).toFloat()..(cachedAttributes["max"] as Int).toFloat(), steps = cachedAttributes["step"] as Int, + colors = SliderDefaults.colors( + thumbColor = Color(0xFF8C7BFF), + activeTrackColor = Color(0xFF8C7BFF).copy(alpha = 0.72f), + inactiveTrackColor = Color.White.copy(alpha = 0.20f), + ) ) } NodeType.BUTTON -> { - OutlinedButton(onClick = { - runCallbackSafe { - (cachedAttributes["callback"] as? () -> Unit)?.let { it() } - } - }) { - NodeLabel() + val buttonShape = RoundedCornerShape(999.dp) + Box( + modifier = Modifier + .clip(buttonShape) + .background(Color.White.copy(alpha = 0.08f), buttonShape) + .border(1.dp, Color.White.copy(alpha = 0.20f), buttonShape) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = { + runCallbackSafe { + (cachedAttributes["callback"] as? () -> Unit)?.let { it() } + } + } + ) + .padding(horizontal = 16.dp, vertical = 9.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = cachedAttributes["label"] as String, + color = Color.White, + fontSize = (cachedAttributes["fontSize"]?.toString()?.toInt() ?: 14).sp, + fontWeight = FontWeight.SemiBold + ) } } NodeType.TEXT_INPUT -> { var textInputValue by remember { mutableStateOf(cachedAttributes["value"].toString()) } - TextField( + val inputShape = RoundedCornerShape(14.dp) + BasicTextField( value = textInputValue, readOnly = cachedAttributes["readonly"] as? Boolean ?: false, singleLine = cachedAttributes["singleLine"] as? Boolean ?: true, maxLines = cachedAttributes["maxLines"] as? Int ?: 1, + textStyle = TextStyle( + color = Color.White, + fontSize = 14.sp + ), + cursorBrush = SolidColor(Color(0xFF8C7BFF)), onValueChange = { value -> runCallbackSafe { textInputValue = value @@ -175,7 +221,24 @@ private fun DrawNode(node: Node) { (cachedAttributes["callback"] as? (String) -> Unit)?.let { it(value) } } }, - placeholder = { Text(cachedAttributes["placeholder"].toString()) } + decorationBox = { innerTextField -> + Box( + modifier = Modifier + .fillMaxWidth() + .background(Color.White.copy(alpha = 0.08f), inputShape) + .border(1.dp, Color.White.copy(alpha = 0.18f), inputShape) + .padding(horizontal = 12.dp, vertical = 10.dp) + ) { + if (textInputValue.isEmpty()) { + Text( + text = cachedAttributes["placeholder"].toString(), + color = Color.White.copy(alpha = 0.6f), + fontSize = 14.sp + ) + } + innerTextField() + } + } ) } else -> {} @@ -205,4 +268,4 @@ fun ScriptInterface(interfaceBuilder: InterfaceBuilder) { } } } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/ConversationToolbox.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/ConversationToolbox.kt index 2d791360..87ac9d9d 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/ConversationToolbox.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/ConversationToolbox.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -25,7 +26,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter @@ -99,7 +99,9 @@ class ConversationToolbox : Feature("Conversation Toolbox") { val interfaceManager = getBinding(InterfaceManager::class)?.takeIf { it.hasInterface(EnumScriptInterface.CONVERSATION_TOOLBOX) } ?: return@eachModule - addComposable("\uD83D\uDCDC ${moduleInfo.displayName}") { alertDialog, conversationId -> + addComposable( + "\uD83D\uDCDC ${context.translation["conversation_toolbox.loaded_script"]}: ${moduleInfo.displayName}" + ) { alertDialog, conversationId -> ScriptInterface(remember { interfaceManager.buildInterface(EnumScriptInterface.CONVERSATION_TOOLBOX, mapOf( "alertDialog" to alertDialog, @@ -143,7 +145,7 @@ class ConversationToolbox : Feature("Conversation Toolbox") { .verticalScroll(rememberScrollState()) ) { Text( - "Conversation Toolbox", + context.translation["conversation_toolbox.title"], fontSize = 18.sp, modifier = Modifier .fillMaxWidth() @@ -160,14 +162,16 @@ class ConversationToolbox : Feature("Conversation Toolbox") { Column( modifier = Modifier .fillMaxWidth() - .shadow(10.dp, itemShape, clip = true) .clip(itemShape) .background(Color.White.copy(alpha = 0.06f), itemShape) .border(1.dp, Color.White.copy(alpha = 0.10f), itemShape) ) { Row( modifier = Modifier - .clickable { expandedComposableCache[title] = !expanded } + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { expandedComposableCache[title] = !expanded } .fillMaxWidth() .padding(horizontal = 12.dp, vertical = 12.dp), horizontalArrangement = Arrangement.spacedBy(10.dp), @@ -196,7 +200,10 @@ class ConversationToolbox : Feature("Conversation Toolbox") { composable(alertDialog, openedConversationId) }.onFailure { throwable -> Text( - "Failed to load: ${throwable.message}", + context.translation.format( + "conversation_toolbox.failed_to_load", + "message" to (throwable.message ?: "unknown error") + ), color = PurrfectOverlayPalette.textSecondary, fontSize = 12.sp )