feat: redesign UI with dark glassmorphism theme and Lucide icons
Replace the default Material Design theme with a futuristic dark glassmorphism aesthetic featuring ambient gradient backgrounds, translucent glass-effect surfaces, neon cyan/purple accents, and Lucide icon pack for consistent thin-line iconography. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
cc383eb668
commit
d7dddb3cf9
14 changed files with 474 additions and 162 deletions
|
|
@ -1,4 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.PicoClaw" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
<style name="Theme.PicoClaw" parent="android:Theme.Material.NoActionBar">
|
||||
<item name="android:windowBackground">#FF050510</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -2,16 +2,37 @@ package io.picoclaw.android.core.ui.theme
|
|||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
// ─── Base Palette ─────────────────────────────────────────────
|
||||
val DeepBlack = Color(0xFF050510)
|
||||
val DarkSurface = Color(0xFF0F0F23)
|
||||
val DarkCard = Color(0xFF141428)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
// ─── Accent Colors ────────────────────────────────────────────
|
||||
val NeonCyan = Color(0xFF00D4FF)
|
||||
val ElectricPurple = Color(0xFFA855F7)
|
||||
val AccentPink = Color(0xFFEC4899)
|
||||
|
||||
val UserBubble = Color(0xFF2196F3)
|
||||
val AgentBubble = Color(0xFF424242)
|
||||
val ConnectedGreen = Color(0xFF4CAF50)
|
||||
val ReconnectingYellow = Color(0xFFFFC107)
|
||||
val DisconnectedRed = Color(0xFFF44336)
|
||||
// ─── Glassmorphism ────────────────────────────────────────────
|
||||
val GlassWhite = Color(0x14FFFFFF)
|
||||
val GlassBorder = Color(0x1FFFFFFF)
|
||||
|
||||
// ─── Message Bubbles ──────────────────────────────────────────
|
||||
val UserBubble = Color(0x1A00D4FF)
|
||||
val UserBubbleBorder = Color(0x3300D4FF)
|
||||
val AgentBubble = Color(0x14A855F7)
|
||||
val AgentBubbleBorder = Color(0x26A855F7)
|
||||
|
||||
// ─── Text Colors ──────────────────────────────────────────────
|
||||
val TextPrimary = Color(0xFFF0F0FF)
|
||||
val TextSecondary = Color(0xB3F0F0FF)
|
||||
val TextTertiary = Color(0x66F0F0FF)
|
||||
|
||||
// ─── Status Colors ────────────────────────────────────────────
|
||||
val ConnectedGreen = Color(0xFF22C55E)
|
||||
val ReconnectingYellow = Color(0xFFFBBF24)
|
||||
val DisconnectedRed = Color(0xFFEF4444)
|
||||
|
||||
// ─── Ambient Gradient Orbs ────────────────────────────────────
|
||||
val GradientCyan = Color(0xFF06B6D4)
|
||||
val GradientPurple = Color(0xFF7C3AED)
|
||||
val GradientPink = Color(0xFFDB2777)
|
||||
|
|
|
|||
|
|
@ -1,45 +1,50 @@
|
|||
package io.picoclaw.android.core.ui.theme
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Shapes
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
private val FuturisticDarkScheme = darkColorScheme(
|
||||
primary = NeonCyan,
|
||||
onPrimary = DeepBlack,
|
||||
primaryContainer = NeonCyan.copy(alpha = 0.15f),
|
||||
onPrimaryContainer = NeonCyan,
|
||||
secondary = ElectricPurple,
|
||||
onSecondary = DeepBlack,
|
||||
secondaryContainer = ElectricPurple.copy(alpha = 0.15f),
|
||||
onSecondaryContainer = ElectricPurple,
|
||||
tertiary = AccentPink,
|
||||
onTertiary = DeepBlack,
|
||||
background = DeepBlack,
|
||||
onBackground = TextPrimary,
|
||||
surface = DarkSurface,
|
||||
onSurface = TextPrimary,
|
||||
surfaceVariant = DarkCard,
|
||||
onSurfaceVariant = TextSecondary,
|
||||
outline = GlassBorder,
|
||||
outlineVariant = GlassWhite,
|
||||
error = DisconnectedRed,
|
||||
onError = TextPrimary,
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
private val FuturisticShapes = Shapes(
|
||||
small = RoundedCornerShape(8.dp),
|
||||
medium = RoundedCornerShape(16.dp),
|
||||
large = RoundedCornerShape(24.dp),
|
||||
extraLarge = RoundedCornerShape(28.dp),
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun PicoClawTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
colorScheme = FuturisticDarkScheme,
|
||||
typography = Typography,
|
||||
shapes = FuturisticShapes,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,11 +7,60 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.unit.sp
|
||||
|
||||
val Typography = Typography(
|
||||
displayLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Light,
|
||||
fontSize = 57.sp,
|
||||
lineHeight = 64.sp,
|
||||
letterSpacing = (-0.25).sp
|
||||
),
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 20.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 1.sp
|
||||
),
|
||||
titleMedium = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.8.sp
|
||||
),
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.3.sp
|
||||
),
|
||||
bodyMedium = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.25.sp
|
||||
),
|
||||
bodySmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.4.sp
|
||||
),
|
||||
labelLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ dependencies {
|
|||
implementation(libs.compose.ui.tooling.preview)
|
||||
implementation(libs.compose.material3)
|
||||
implementation(libs.compose.icons.extended)
|
||||
implementation("com.composables:icons-lucide-android:2.2.1")
|
||||
implementation(libs.activity.compose)
|
||||
implementation(libs.lifecycle.runtime.compose)
|
||||
implementation(libs.lifecycle.viewmodel.compose)
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ package io.picoclaw.android.feature.chat.component
|
|||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -22,7 +24,7 @@ fun ConnectionBanner(
|
|||
modifier: Modifier = Modifier
|
||||
) {
|
||||
AnimatedVisibility(visible = connectionState != ConnectionState.CONNECTED) {
|
||||
val (color, text) = when (connectionState) {
|
||||
val (accentColor, text) = when (connectionState) {
|
||||
ConnectionState.CONNECTING -> ReconnectingYellow to "Connecting..."
|
||||
ConnectionState.RECONNECTING -> ReconnectingYellow to "Reconnecting..."
|
||||
ConnectionState.DISCONNECTED -> DisconnectedRed to "Disconnected"
|
||||
|
|
@ -31,14 +33,24 @@ fun ConnectionBanner(
|
|||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(color)
|
||||
.padding(vertical = 4.dp),
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||
.background(
|
||||
color = accentColor.copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
.border(
|
||||
width = 0.5.dp,
|
||||
color = accentColor.copy(alpha = 0.3f),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
.padding(vertical = 8.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
color = Color.White,
|
||||
fontSize = 12.sp
|
||||
color = accentColor,
|
||||
fontSize = 12.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package io.picoclaw.android.feature.chat.component
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
|
|
@ -8,8 +9,6 @@ import androidx.compose.foundation.layout.size
|
|||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -17,9 +16,13 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import io.picoclaw.android.core.domain.model.ImageAttachment
|
||||
import io.picoclaw.android.core.ui.theme.GlassBorder
|
||||
import io.picoclaw.android.core.ui.theme.TextSecondary
|
||||
import com.composables.icons.lucide.R as LucideR
|
||||
|
||||
@Composable
|
||||
fun ImagePreviewRow(
|
||||
|
|
@ -30,7 +33,7 @@ fun ImagePreviewRow(
|
|||
if (images.isEmpty()) return
|
||||
|
||||
LazyRow(
|
||||
modifier = modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
modifier = modifier.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
itemsIndexed(images) { index, attachment ->
|
||||
|
|
@ -40,7 +43,12 @@ fun ImagePreviewRow(
|
|||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(64.dp)
|
||||
.clip(RoundedCornerShape(8.dp)),
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.border(
|
||||
width = 0.5.dp,
|
||||
color = GlassBorder,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
IconButton(
|
||||
|
|
@ -50,9 +58,10 @@ fun ImagePreviewRow(
|
|||
.align(Alignment.TopEnd)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Close,
|
||||
painter = painterResource(LucideR.drawable.lucide_ic_x),
|
||||
contentDescription = "Remove",
|
||||
modifier = Modifier.size(14.dp)
|
||||
modifier = Modifier.size(14.dp),
|
||||
tint = TextSecondary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package io.picoclaw.android.feature.chat.component
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
|
|
@ -13,7 +14,6 @@ import androidx.compose.material3.Surface
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
|
|
@ -23,7 +23,10 @@ import com.mikepenz.markdown.m3.markdownTypography
|
|||
import io.picoclaw.android.core.domain.model.ChatMessage
|
||||
import io.picoclaw.android.core.domain.model.MessageSender
|
||||
import io.picoclaw.android.core.ui.theme.AgentBubble
|
||||
import io.picoclaw.android.core.ui.theme.AgentBubbleBorder
|
||||
import io.picoclaw.android.core.ui.theme.TextPrimary
|
||||
import io.picoclaw.android.core.ui.theme.UserBubble
|
||||
import io.picoclaw.android.core.ui.theme.UserBubbleBorder
|
||||
import java.io.File
|
||||
|
||||
@Composable
|
||||
|
|
@ -34,22 +37,24 @@ fun MessageBubble(
|
|||
val isUser = message.sender == MessageSender.USER
|
||||
val alignment = if (isUser) Alignment.CenterEnd else Alignment.CenterStart
|
||||
val bubbleColor = if (isUser) UserBubble else AgentBubble
|
||||
val borderColor = if (isUser) UserBubbleBorder else AgentBubbleBorder
|
||||
val shape = RoundedCornerShape(
|
||||
topStart = 16.dp,
|
||||
topEnd = 16.dp,
|
||||
bottomStart = if (isUser) 16.dp else 4.dp,
|
||||
bottomEnd = if (isUser) 4.dp else 16.dp
|
||||
topStart = 20.dp,
|
||||
topEnd = 20.dp,
|
||||
bottomStart = if (isUser) 20.dp else 4.dp,
|
||||
bottomEnd = if (isUser) 4.dp else 20.dp
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 2.dp),
|
||||
.padding(horizontal = 12.dp, vertical = 3.dp),
|
||||
contentAlignment = alignment
|
||||
) {
|
||||
Surface(
|
||||
shape = shape,
|
||||
color = bubbleColor,
|
||||
border = BorderStroke(0.5.dp, borderColor),
|
||||
modifier = Modifier.widthIn(max = 300.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
|
|
@ -72,9 +77,9 @@ fun MessageBubble(
|
|||
if (message.content.isNotEmpty()) {
|
||||
Markdown(
|
||||
content = message.content,
|
||||
colors = markdownColor(text = Color.White),
|
||||
colors = markdownColor(text = TextPrimary),
|
||||
typography = markdownTypography(
|
||||
paragraph = MaterialTheme.typography.bodyLarge.copy(color = Color.White),
|
||||
paragraph = MaterialTheme.typography.bodyLarge.copy(color = TextPrimary),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,35 @@
|
|||
package io.picoclaw.android.feature.chat.component
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Send
|
||||
import androidx.compose.material.icons.filled.CameraAlt
|
||||
import androidx.compose.material.icons.filled.Image
|
||||
import androidx.compose.material.icons.filled.Mic
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.FilledIconButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.picoclaw.android.core.ui.theme.DeepBlack
|
||||
import io.picoclaw.android.core.ui.theme.GlassBorder
|
||||
import io.picoclaw.android.core.ui.theme.GlassWhite
|
||||
import io.picoclaw.android.core.ui.theme.NeonCyan
|
||||
import io.picoclaw.android.core.ui.theme.TextPrimary
|
||||
import io.picoclaw.android.core.ui.theme.TextSecondary
|
||||
import io.picoclaw.android.core.ui.theme.TextTertiary
|
||||
import com.composables.icons.lucide.R as LucideR
|
||||
|
||||
@Composable
|
||||
fun MessageInput(
|
||||
|
|
@ -35,32 +44,70 @@ fun MessageInput(
|
|||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 8.dp),
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||
.background(
|
||||
color = GlassWhite,
|
||||
shape = RoundedCornerShape(28.dp)
|
||||
)
|
||||
.border(
|
||||
width = 0.5.dp,
|
||||
color = GlassBorder,
|
||||
shape = RoundedCornerShape(28.dp)
|
||||
)
|
||||
.padding(start = 4.dp, end = 4.dp, top = 4.dp, bottom = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton(onClick = onCameraClick) {
|
||||
Icon(Icons.Default.CameraAlt, contentDescription = "Camera")
|
||||
Icon(
|
||||
painter = painterResource(LucideR.drawable.lucide_ic_camera),
|
||||
contentDescription = "Camera",
|
||||
tint = TextSecondary
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onGalleryClick) {
|
||||
Icon(Icons.Default.Image, contentDescription = "Gallery")
|
||||
Icon(
|
||||
painter = painterResource(LucideR.drawable.lucide_ic_image),
|
||||
contentDescription = "Gallery",
|
||||
tint = TextSecondary
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onMicClick) {
|
||||
Icon(Icons.Default.Mic, contentDescription = "Voice")
|
||||
Icon(
|
||||
painter = painterResource(LucideR.drawable.lucide_ic_mic),
|
||||
contentDescription = "Voice",
|
||||
tint = TextSecondary
|
||||
)
|
||||
}
|
||||
OutlinedTextField(
|
||||
value = text,
|
||||
onValueChange = onTextChanged,
|
||||
modifier = Modifier.weight(1f),
|
||||
placeholder = { Text("Message...") },
|
||||
placeholder = { Text("Message...", color = TextTertiary) },
|
||||
maxLines = 4,
|
||||
shape = MaterialTheme.shapes.extraLarge
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = Color.Transparent,
|
||||
unfocusedBorderColor = Color.Transparent,
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
cursorColor = NeonCyan,
|
||||
focusedTextColor = TextPrimary,
|
||||
unfocusedTextColor = TextPrimary
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
FilledIconButton(
|
||||
onClick = onSendClick,
|
||||
shape = CircleShape
|
||||
shape = CircleShape,
|
||||
colors = IconButtonDefaults.filledIconButtonColors(
|
||||
containerColor = NeonCyan,
|
||||
contentColor = DeepBlack
|
||||
)
|
||||
) {
|
||||
Icon(Icons.AutoMirrored.Filled.Send, contentDescription = "Send")
|
||||
Icon(
|
||||
painter = painterResource(LucideR.drawable.lucide_ic_send_horizontal),
|
||||
contentDescription = "Send"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.picoclaw.android.core.ui.theme.NeonCyan
|
||||
import io.picoclaw.android.core.ui.theme.TextSecondary
|
||||
|
||||
@Composable
|
||||
fun StatusIndicator(
|
||||
|
|
@ -36,13 +38,13 @@ fun StatusIndicator(
|
|||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(14.dp),
|
||||
strokeWidth = 2.dp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
color = NeonCyan
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = label ?: "",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
color = TextSecondary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,19 +7,19 @@ import androidx.activity.compose.BackHandler
|
|||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
|
|
@ -29,11 +29,22 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.core.content.FileProvider
|
||||
import io.picoclaw.android.core.domain.model.ImageAttachment
|
||||
import io.picoclaw.android.core.ui.theme.DeepBlack
|
||||
import io.picoclaw.android.core.ui.theme.GradientCyan
|
||||
import io.picoclaw.android.core.ui.theme.GradientPink
|
||||
import io.picoclaw.android.core.ui.theme.GradientPurple
|
||||
import io.picoclaw.android.core.ui.theme.TextSecondary
|
||||
import io.picoclaw.android.feature.chat.ChatEvent
|
||||
import io.picoclaw.android.feature.chat.ChatViewModel
|
||||
import com.composables.icons.lucide.R as LucideR
|
||||
import io.picoclaw.android.feature.chat.component.ConnectionBanner
|
||||
import io.picoclaw.android.feature.chat.component.ImagePreviewRow
|
||||
import io.picoclaw.android.feature.chat.component.MessageInput
|
||||
|
|
@ -96,7 +107,6 @@ fun ChatScreen(
|
|||
}
|
||||
}
|
||||
|
||||
// RECORD_AUDIO permission
|
||||
var pendingVoiceStart by remember { mutableStateOf(false) }
|
||||
|
||||
val micPermissionLauncher = rememberLauncherForActivityResult(
|
||||
|
|
@ -119,7 +129,6 @@ fun ChatScreen(
|
|||
}
|
||||
}
|
||||
|
||||
// Back handler for voice mode
|
||||
BackHandler(enabled = uiState.voiceModeState.isActive) {
|
||||
viewModel.onEvent(ChatEvent.OnVoiceModeStop)
|
||||
}
|
||||
|
|
@ -152,14 +161,58 @@ fun ChatScreen(
|
|||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(DeepBlack)
|
||||
.drawBehind {
|
||||
drawCircle(
|
||||
brush = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
GradientCyan.copy(alpha = 0.07f),
|
||||
Color.Transparent
|
||||
),
|
||||
center = Offset(size.width * 0.15f, size.height * 0.1f),
|
||||
radius = size.width * 0.8f
|
||||
)
|
||||
)
|
||||
drawCircle(
|
||||
brush = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
GradientPurple.copy(alpha = 0.07f),
|
||||
Color.Transparent
|
||||
),
|
||||
center = Offset(size.width * 0.85f, size.height * 0.9f),
|
||||
radius = size.width * 0.7f
|
||||
)
|
||||
)
|
||||
drawCircle(
|
||||
brush = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
GradientPink.copy(alpha = 0.03f),
|
||||
Color.Transparent
|
||||
),
|
||||
center = Offset(size.width * 0.5f, size.height * 0.5f),
|
||||
radius = size.width * 0.5f
|
||||
)
|
||||
)
|
||||
}
|
||||
) {
|
||||
Scaffold(
|
||||
containerColor = Color.Transparent,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("PicoClaw") },
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color.Transparent
|
||||
),
|
||||
actions = {
|
||||
IconButton(onClick = onNavigateToSettings) {
|
||||
Icon(Icons.Default.Settings, contentDescription = "Settings")
|
||||
Icon(
|
||||
painter = painterResource(LucideR.drawable.lucide_ic_settings),
|
||||
contentDescription = "Settings",
|
||||
tint = TextSecondary
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
// TTS settings are currently used only within the chat feature, so this screen
|
||||
// is placed under feature/chat. If TTS settings become shared across multiple
|
||||
// features in the future, consider extracting them into a dedicated feature/settings module.
|
||||
package io.picoclaw.android.feature.chat.screen
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuAnchorType
|
||||
|
|
@ -21,10 +19,13 @@ import androidx.compose.material3.Icon
|
|||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SliderDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -34,9 +35,23 @@ 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.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.picoclaw.android.core.domain.model.TtsEngineInfo
|
||||
import io.picoclaw.android.core.domain.model.TtsVoiceInfo
|
||||
import io.picoclaw.android.core.ui.theme.DeepBlack
|
||||
import io.picoclaw.android.core.ui.theme.GlassBorder
|
||||
import io.picoclaw.android.core.ui.theme.GlassWhite
|
||||
import io.picoclaw.android.core.ui.theme.GradientCyan
|
||||
import io.picoclaw.android.core.ui.theme.GradientPurple
|
||||
import io.picoclaw.android.core.ui.theme.NeonCyan
|
||||
import io.picoclaw.android.core.ui.theme.TextPrimary
|
||||
import io.picoclaw.android.core.ui.theme.TextSecondary
|
||||
import com.composables.icons.lucide.R as LucideR
|
||||
import io.picoclaw.android.feature.chat.SettingsViewModel
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
|
|
@ -48,13 +63,48 @@ fun SettingsScreen(
|
|||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(DeepBlack)
|
||||
.drawBehind {
|
||||
drawCircle(
|
||||
brush = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
GradientCyan.copy(alpha = 0.07f),
|
||||
Color.Transparent
|
||||
),
|
||||
center = Offset(size.width * 0.15f, size.height * 0.1f),
|
||||
radius = size.width * 0.8f
|
||||
)
|
||||
)
|
||||
drawCircle(
|
||||
brush = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
GradientPurple.copy(alpha = 0.07f),
|
||||
Color.Transparent
|
||||
),
|
||||
center = Offset(size.width * 0.85f, size.height * 0.9f),
|
||||
radius = size.width * 0.7f
|
||||
)
|
||||
)
|
||||
}
|
||||
) {
|
||||
Scaffold(
|
||||
containerColor = Color.Transparent,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Settings") },
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color.Transparent
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||
Icon(
|
||||
painter = painterResource(LucideR.drawable.lucide_ic_arrow_left),
|
||||
contentDescription = "Back",
|
||||
tint = TextSecondary
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -67,7 +117,11 @@ fun SettingsScreen(
|
|||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp)
|
||||
) {
|
||||
Text("Text-to-Speech", style = MaterialTheme.typography.titleMedium)
|
||||
Text(
|
||||
"Text-to-Speech",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = NeonCyan
|
||||
)
|
||||
|
||||
EngineSelector(
|
||||
selectedEngine = uiState.ttsConfig.enginePackageName,
|
||||
|
|
@ -97,12 +151,17 @@ fun SettingsScreen(
|
|||
|
||||
Button(
|
||||
onClick = viewModel::onTestSpeak,
|
||||
enabled = !uiState.isTesting
|
||||
enabled = !uiState.isTesting,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = NeonCyan,
|
||||
contentColor = DeepBlack
|
||||
)
|
||||
) {
|
||||
Text(if (uiState.isTesting) "Speaking..." else "Test Voice")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
|
|
@ -127,8 +186,16 @@ private fun EngineSelector(
|
|||
value = displayText,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text("Engine") },
|
||||
label = { Text("Engine", color = TextSecondary) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) },
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = NeonCyan.copy(alpha = 0.5f),
|
||||
unfocusedBorderColor = GlassBorder,
|
||||
focusedContainerColor = GlassWhite,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
focusedTextColor = TextPrimary,
|
||||
unfocusedTextColor = TextPrimary
|
||||
),
|
||||
modifier = Modifier
|
||||
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
|
||||
.fillMaxWidth()
|
||||
|
|
@ -179,8 +246,16 @@ private fun VoiceSelector(
|
|||
value = displayText,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text("Voice") },
|
||||
label = { Text("Voice", color = TextSecondary) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) },
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = NeonCyan.copy(alpha = 0.5f),
|
||||
unfocusedBorderColor = GlassBorder,
|
||||
focusedContainerColor = GlassWhite,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
focusedTextColor = TextPrimary,
|
||||
unfocusedTextColor = TextPrimary
|
||||
),
|
||||
modifier = Modifier
|
||||
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
|
||||
.fillMaxWidth()
|
||||
|
|
@ -224,11 +299,11 @@ private fun SliderSetting(
|
|||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(label, style = MaterialTheme.typography.bodyLarge)
|
||||
Text(label, style = MaterialTheme.typography.bodyLarge, color = TextPrimary)
|
||||
Text(
|
||||
"%.1f".format(localValue),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
||||
color = TextSecondary
|
||||
)
|
||||
}
|
||||
Slider(
|
||||
|
|
@ -236,7 +311,12 @@ private fun SliderSetting(
|
|||
onValueChange = { localValue = it },
|
||||
onValueChangeFinished = { onValueChangeFinished(localValue) },
|
||||
valueRange = valueRange,
|
||||
steps = 14
|
||||
steps = 14,
|
||||
colors = SliderDefaults.colors(
|
||||
thumbColor = NeonCyan,
|
||||
activeTrackColor = NeonCyan,
|
||||
inactiveTrackColor = GlassWhite
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
|
@ -26,9 +24,20 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.picoclaw.android.core.domain.model.VoicePhase
|
||||
import io.picoclaw.android.core.ui.theme.DeepBlack
|
||||
import io.picoclaw.android.core.ui.theme.GradientCyan
|
||||
import io.picoclaw.android.core.ui.theme.GradientPurple
|
||||
import io.picoclaw.android.core.ui.theme.TextPrimary
|
||||
import io.picoclaw.android.core.ui.theme.TextSecondary
|
||||
import com.composables.icons.lucide.R as LucideR
|
||||
|
||||
@Composable
|
||||
fun VoiceModeOverlay(
|
||||
|
|
@ -45,9 +54,30 @@ fun VoiceModeOverlay(
|
|||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.background(DeepBlack)
|
||||
.drawBehind {
|
||||
drawCircle(
|
||||
brush = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
GradientCyan.copy(alpha = 0.1f),
|
||||
Color.Transparent
|
||||
),
|
||||
center = Offset(size.width * 0.3f, size.height * 0.3f),
|
||||
radius = size.width * 0.6f
|
||||
)
|
||||
)
|
||||
drawCircle(
|
||||
brush = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
GradientPurple.copy(alpha = 0.1f),
|
||||
Color.Transparent
|
||||
),
|
||||
center = Offset(size.width * 0.7f, size.height * 0.7f),
|
||||
radius = size.width * 0.5f
|
||||
)
|
||||
)
|
||||
}
|
||||
) {
|
||||
// Close button
|
||||
IconButton(
|
||||
onClick = onClose,
|
||||
modifier = Modifier
|
||||
|
|
@ -55,13 +85,13 @@ fun VoiceModeOverlay(
|
|||
.padding(16.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "閉じる",
|
||||
modifier = Modifier.size(28.dp)
|
||||
painter = painterResource(LucideR.drawable.lucide_ic_x),
|
||||
contentDescription = "Close",
|
||||
modifier = Modifier.size(28.dp),
|
||||
tint = TextSecondary
|
||||
)
|
||||
}
|
||||
|
||||
// Center content
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
|
@ -87,38 +117,34 @@ fun VoiceModeOverlay(
|
|||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
// Phase label
|
||||
Text(
|
||||
text = state.statusText.takeIf { state.phase == VoicePhase.THINKING }
|
||||
?: phaseLabel(state.phase),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
||||
color = TextSecondary
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Recognized text
|
||||
if (state.recognizedText.isNotEmpty()) {
|
||||
Text(
|
||||
text = state.recognizedText,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
color = TextPrimary,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
// Response text
|
||||
if (state.responseText.isNotEmpty() && state.phase == VoicePhase.SPEAKING) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = state.responseText,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f),
|
||||
color = TextSecondary,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
// Error message
|
||||
if (state.errorMessage != null) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
|
|
@ -135,9 +161,9 @@ fun VoiceModeOverlay(
|
|||
|
||||
private fun phaseLabel(phase: VoicePhase): String = when (phase) {
|
||||
VoicePhase.IDLE -> ""
|
||||
VoicePhase.LISTENING -> "聞き取り中..."
|
||||
VoicePhase.SENDING -> "送信中..."
|
||||
VoicePhase.THINKING -> "考え中..."
|
||||
VoicePhase.SPEAKING -> "話しています..."
|
||||
VoicePhase.ERROR -> "エラー"
|
||||
VoicePhase.LISTENING -> "Listening..."
|
||||
VoicePhase.SENDING -> "Sending..."
|
||||
VoicePhase.THINKING -> "Thinking..."
|
||||
VoicePhase.SPEAKING -> "Speaking..."
|
||||
VoicePhase.ERROR -> "Error"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,12 +22,12 @@ import kotlin.math.PI
|
|||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
|
||||
private val ListeningColor = Color(0xFF4A9EFF)
|
||||
private val ListeningColor = Color(0xFF00D4FF)
|
||||
private val SendingColor = Color(0xFFFF8C42)
|
||||
private val ThinkingColor = Color(0xFFA855F7)
|
||||
private val SpeakingColor = Color(0xFF22C55E)
|
||||
private val ErrorColor = Color(0xFFEF4444)
|
||||
private val IdleColor = Color(0xFF6B7280)
|
||||
private val IdleColor = Color(0xFF4A5568)
|
||||
|
||||
@Composable
|
||||
fun VoiceOrb(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue