瀏覽代碼

several functional enhancements

master
gituser 2 週之前
父節點
當前提交
9dc9a6b95f

+ 6
- 0
.idea/vcs.xml 查看文件

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

+ 106
- 11
app/src/main/java/ch/spherIC/ledlampcontrol/data/model/LedModels.kt 查看文件

@@ -11,6 +11,7 @@ import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.intOrNull

@Serializable
data class LedColor(
@@ -20,16 +21,47 @@ data class LedColor(
val w: Int = 0
)

@Serializable
@Serializable(with = LedModeTypeSerializer::class)
enum class LedModeType {
SINGLE_COLOR,
RAINBOW,
SPECTRAL,
FADING,
FLASH,
SEGMENTS
SINGLE_COLOR, // 0
RAINBOW, // 1
SPECTRAL, // 2
FADING, // 3
FLASH, // 4
SEGMENTS // 5
}

object LedModeTypeSerializer : KSerializer<LedModeType> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LedModeType", PrimitiveKind.INT)

override fun deserialize(decoder: Decoder): LedModeType {
val input = decoder as? JsonDecoder ?: return LedModeType.SINGLE_COLOR
val element = input.decodeJsonElement()
val modeInt = if (element is JsonPrimitive) {
element.intOrNull ?: element.content.toIntOrNull() ?: 0
} else 0
return LedModeType.entries.getOrNull(modeInt) ?: LedModeType.SINGLE_COLOR
}

override fun serialize(encoder: Encoder, value: LedModeType) {
encoder.encodeInt(value.ordinal)
}
}

@Serializable
data class TimeInterval(
val start: Long = 0,
val end: Long = 0
)

@Serializable
data class LedTimer(
val on: Boolean = false,
val timeInterval1: TimeInterval = TimeInterval(),
val timeInterval2: TimeInterval = TimeInterval()
)

@Serializable
data class LedDevice(
val id: String,
@@ -38,6 +70,27 @@ data class LedDevice(
val port: Int = 81
)

@Serializable
data class LedConfig(
val version: Int = 0,
val ssid: String = "",
val pass: String = "",
val wifiValid: Boolean = false,
val mode: LedModeType = LedModeType.SINGLE_COLOR,
val brightness: Int = 0,
val singleColor: LedColor = LedColor(),
val segmentColor1: LedColor = LedColor(),
val segmentColor2: LedColor = LedColor(),
val segmentColor3: LedColor = LedColor(),
val rainbowDelay: Long = 0,
val spectralStepTime: Long = 0,
val fadingHoldTime: Long = 0,
val fadingFadeTime: Long = 0,
val flashOnTime: Long = 0,
val flashOffTime: Long = 0,
val timer: LedTimer = LedTimer()
)

@Serializable
data class LedState(
@Serializable(with = PowerBooleanSerializer::class)
@@ -80,17 +133,59 @@ sealed class LedCommand {
) : LedCommand()
@Serializable
data class SetColor(val color: LedColor) : LedCommand()
data class ChangeMode(
val cmd: String = "CHG_MODE",
val `val`: String // LM_SINGLE_COLOR, etc.
) : LedCommand()
@Serializable
data class SetBrightness(val brightness: Int) : LedCommand()
data class ChangeColor(
val cmd: String = "CHG_COLOR",
val `val`: String, // singleColor, segmentColor1, etc.
val clr: String // r,g,b,w
) : LedCommand()
@Serializable
data class SetMode(val mode: LedModeType) : LedCommand()
data class ChangeBrightness(
val cmd: String = "CHG_BRIGHTNESS",
val `val`: String // "255"
) : LedCommand()
@Serializable
data class SetSpeed(val speed: Int) : LedCommand()
data class ChangeConfigParam(
val cmd: String = "CHG_CFG_PARAM",
val `val`: String, // flashOnTime, etc.
val amt: String // "2000"
) : LedCommand()
@Serializable
data class ChangeTimer(
val cmd: String = "CHG_TIMER",
val status: String // on/off
) : LedCommand()
@Serializable
data class ChangeTimerInterval(
val cmd: String = "CHG_TIMER_INTERVAL",
val interval: String, // timeInterval1, etc.
val timerange: String // "12:47:00 - 12:48:10"
) : LedCommand()
@Serializable
data class GetConfig(
val cmd: String = "GET_CONFIG"
) : LedCommand()

@Serializable
data object RequestState : LedCommand()
// Legacy support
@Serializable
data class SetColor(val color: LedColor) : LedCommand()
@Serializable
data class SetBrightness(val brightness: Int) : LedCommand()
@Serializable
data class SetMode(val mode: LedModeType) : LedCommand()
@Serializable
data class SetSpeed(val speed: Int) : LedCommand()
}

+ 38
- 8
app/src/main/java/ch/spherIC/ledlampcontrol/data/network/LedWebSocketManager.kt 查看文件

@@ -1,13 +1,13 @@
package ch.spherIC.ledlampcontrol.data.network

import ch.spherIC.ledlampcontrol.data.model.LedCommand
import ch.spherIC.ledlampcontrol.data.model.LedConfig
import ch.spherIC.ledlampcontrol.data.model.LedState
import kotlinx.coroutines.flow.*
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.encodeToJsonElement
import okhttp3.*
import okio.ByteString

class LedWebSocketManager(private val client: OkHttpClient) {

@@ -20,6 +20,9 @@ class LedWebSocketManager(private val client: OkHttpClient) {
private val _ledState = MutableStateFlow(LedState())
val ledState: StateFlow<LedState> = _ledState.asStateFlow()

private val _ledConfig = MutableStateFlow(LedConfig())
val ledConfig: StateFlow<LedConfig> = _ledConfig.asStateFlow()

private val json = Json {
ignoreUnknownKeys = true
encodeDefaults = true
@@ -41,15 +44,15 @@ class LedWebSocketManager(private val client: OkHttpClient) {
webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
if (currentUrl != url) return // Ignore if URL changed during connection
if (currentUrl != url) return
_connectionState.value = ConnectionState.Connected
sendCommand(LedCommand.RequestState)
// After opening, get full config
sendCommand(LedCommand.GetConfig())
}

override fun onMessage(webSocket: WebSocket, text: String) {
if (currentUrl != url) return
// Try to handle raw status strings first
val rawText = text.trim().uppercase()
if (rawText == "LED ON" || rawText == "LED OFF") {
val isPowerOn = rawText == "LED ON"
@@ -58,8 +61,22 @@ class LedWebSocketManager(private val client: OkHttpClient) {
}

try {
val state = json.decodeFromString<LedState>(text)
_ledState.value = state
// Try parsing as Config first if it contains "version" or many keys
if (text.contains("\"version\"")) {
val config = json.decodeFromString<LedConfig>(text)
_ledConfig.value = config
// Sync basic state from config
_ledState.update {
it.copy(
brightness = config.brightness,
mode = config.mode,
color = config.singleColor
)
}
} else {
val state = json.decodeFromString<LedState>(text)
_ledState.value = state
}
} catch (e: Exception) {
// Handle parsing error
}
@@ -89,13 +106,26 @@ class LedWebSocketManager(private val client: OkHttpClient) {
fun sendCommand(command: LedCommand) {
val jsonElement = when (command) {
is LedCommand.SetPower -> json.encodeToJsonElement(command)
is LedCommand.SetColor -> json.encodeToJsonElement(command)
is LedCommand.ChangeMode -> json.encodeToJsonElement(command)
is LedCommand.ChangeColor -> json.encodeToJsonElement(command)
is LedCommand.ChangeBrightness -> json.encodeToJsonElement(command)
is LedCommand.ChangeConfigParam -> json.encodeToJsonElement(command)
is LedCommand.ChangeTimer -> json.encodeToJsonElement(command)
is LedCommand.ChangeTimerInterval -> json.encodeToJsonElement(command)
is LedCommand.GetConfig -> json.encodeToJsonElement(command)
is LedCommand.RequestState -> json.encodeToJsonElement(command)
is LedCommand.SetBrightness -> json.encodeToJsonElement(command)
is LedCommand.SetColor -> json.encodeToJsonElement(command)
is LedCommand.SetMode -> json.encodeToJsonElement(command)
is LedCommand.SetSpeed -> json.encodeToJsonElement(command)
is LedCommand.RequestState -> json.encodeToJsonElement(command)
}
webSocket?.send(jsonElement.toString())
// Follow up with GET_CONFIG to stay in sync if it wasn't a GET_CONFIG itself
if (command !is LedCommand.GetConfig) {
val getConfigMessage = json.encodeToJsonElement(LedCommand.GetConfig()).toString()
webSocket?.send(getConfigMessage)
}
}

sealed class ConnectionState {

+ 21
- 11
app/src/main/java/ch/spherIC/ledlampcontrol/data/repository/LedRepository.kt 查看文件

@@ -2,16 +2,17 @@ package ch.spherIC.ledlampcontrol.data.repository

import ch.spherIC.ledlampcontrol.data.model.LedColor
import ch.spherIC.ledlampcontrol.data.model.LedCommand
import ch.spherIC.ledlampcontrol.data.model.LedConfig
import ch.spherIC.ledlampcontrol.data.model.LedModeType
import ch.spherIC.ledlampcontrol.data.model.LedState
import ch.spherIC.ledlampcontrol.data.network.LedWebSocketManager
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow

class LedRepository(private val webSocketManager: LedWebSocketManager) {

val connectionState: StateFlow<LedWebSocketManager.ConnectionState> = webSocketManager.connectionState
val ledState: StateFlow<LedState> = webSocketManager.ledState
val ledConfig: StateFlow<LedConfig> = webSocketManager.ledConfig

fun connect(url: String) {
webSocketManager.connect(url)
@@ -25,23 +26,32 @@ class LedRepository(private val webSocketManager: LedWebSocketManager) {
webSocketManager.sendCommand(LedCommand.SetPower(status = if (power) "on" else "off"))
}

fun setColor(color: LedColor) {
webSocketManager.sendCommand(LedCommand.SetColor(color))
fun updateMode(mode: LedModeType) {
webSocketManager.sendCommand(LedCommand.ChangeMode(`val` = "LM_${mode.name}"))
}

fun setBrightness(brightness: Int) {
webSocketManager.sendCommand(LedCommand.SetBrightness(brightness))
fun updateColor(key: String, color: LedColor) {
val clrString = "${color.r},${color.g},${color.b},${color.w}"
webSocketManager.sendCommand(LedCommand.ChangeColor(`val` = key, clr = clrString))
}

fun setMode(mode: LedModeType) {
webSocketManager.sendCommand(LedCommand.SetMode(mode))
fun updateBrightness(brightness: Int) {
webSocketManager.sendCommand(LedCommand.ChangeBrightness(`val` = brightness.toString()))
}

fun setSpeed(speed: Int) {
webSocketManager.sendCommand(LedCommand.SetSpeed(speed))
fun updateConfigParam(param: String, value: String) {
webSocketManager.sendCommand(LedCommand.ChangeConfigParam(`val` = param, amt = value))
}

fun updateTimer(enabled: Boolean) {
webSocketManager.sendCommand(LedCommand.ChangeTimer(status = if (enabled) "on" else "off"))
}

fun updateTimerInterval(intervalKey: String, timeRange: String) {
webSocketManager.sendCommand(LedCommand.ChangeTimerInterval(interval = intervalKey, timerange = timeRange))
}
fun requestState() {
webSocketManager.sendCommand(LedCommand.RequestState)
fun getConfig() {
webSocketManager.sendCommand(LedCommand.GetConfig())
}
}

+ 15
- 7
app/src/main/java/ch/spherIC/ledlampcontrol/ui/components/ColorWheel.kt 查看文件

@@ -7,16 +7,13 @@ import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
import kotlin.math.atan2
@@ -29,9 +26,20 @@ import kotlin.math.sqrt
@Composable
fun ColorWheel(
modifier: Modifier = Modifier,
selectedColor: Color,
onColorChanged: (r: Int, g: Int, b: Int) -> Unit
) {
var hsv by remember { mutableStateOf(floatArrayOf(0f, 1f, 1f)) }
var hsv by remember { mutableStateOf(floatArrayOf(0f, 0f, 1f)) }
// Update local hsv when selectedColor changes externally
LaunchedEffect(selectedColor) {
val hsvOut = FloatArray(3)
android.graphics.Color.colorToHSV(selectedColor.toArgb(), hsvOut)
// Only update if it's not a pure white/black color to avoid jumping to 0,0
if (hsvOut[1] > 0 || selectedColor != Color.Black) {
hsv = hsvOut
}
}

BoxWithConstraints(modifier = modifier.aspectRatio(1f)) {
val radius = min(constraints.maxWidth, constraints.maxHeight) / 2f
@@ -71,7 +79,7 @@ fun ColorWheel(
) {
val sweepGradient = Brush.sweepGradient(
colors = listOf(
Color.Red, Color.Magenta, Color.Blue, Color.Cyan, Color.Green, Color.Yellow, Color.Red
Color.Red, Color.Yellow, Color.Green, Color.Cyan, Color.Blue, Color.Magenta, Color.Red
),
center = center
)

+ 278
- 30
app/src/main/java/ch/spherIC/ledlampcontrol/ui/dashboard/DashboardScreen.kt 查看文件

@@ -17,9 +17,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import ch.spherIC.ledlampcontrol.data.model.LedDevice
import ch.spherIC.ledlampcontrol.data.model.LedModeType
import ch.spherIC.ledlampcontrol.data.model.LedState
import ch.spherIC.ledlampcontrol.data.model.*
import ch.spherIC.ledlampcontrol.data.network.LedWebSocketManager
import ch.spherIC.ledlampcontrol.ui.components.ColorWheel
import ch.spherIC.ledlampcontrol.ui.theme.LEDLampControlTheme
@@ -30,6 +28,7 @@ import kotlinx.coroutines.launch
@Composable
fun DashboardScreen(viewModel: LedViewModel) {
val ledState by viewModel.ledState.collectAsState()
val ledConfig by viewModel.ledConfig.collectAsState()
var selectedTab by remember { mutableIntStateOf(0) }
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
@@ -136,12 +135,18 @@ fun DashboardScreen(viewModel: LedViewModel) {
NavigationBarItem(
selected = selectedTab == 2,
onClick = { selectedTab = 2 },
icon = { Icon(Icons.Default.Router, contentDescription = "Devices") },
label = { Text("Devices") }
icon = { Icon(Icons.Default.Timer, contentDescription = "Timer") },
label = { Text("Timer") }
)
NavigationBarItem(
selected = selectedTab == 3,
onClick = { selectedTab = 3 },
icon = { Icon(Icons.Default.Router, contentDescription = "Devices") },
label = { Text("Devices") }
)
NavigationBarItem(
selected = selectedTab == 4,
onClick = { selectedTab = 4 },
icon = { Icon(Icons.Default.Palette, contentDescription = "Appearance") },
label = { Text("Appearance") }
)
@@ -151,27 +156,46 @@ fun DashboardScreen(viewModel: LedViewModel) {
Box(
modifier = Modifier
.padding(paddingValues)
.imePadding() // Moves the content up when the keyboard appears
.imePadding()
.fillMaxSize()
) {
when (selectedTab) {
0 -> {
AdjustPane(
ledConfig = ledConfig,
ledState = ledState,
onPowerToggle = { viewModel.togglePower() },
onColorChanged = { r, g, b -> viewModel.updateColor(r, g, b, ledState.color.w) },
onWhiteChanged = { w -> viewModel.updateColor(ledState.color.r, ledState.color.g, ledState.color.b, w) },
onColorChanged = { r, g, b ->
viewModel.updateColor("singleColor", r, g, b, 0)
},
onWhiteChanged = { w ->
if (w > 0) {
viewModel.updateColor("singleColor", 0, 0, 0, w)
} else {
// If W is set to 0, just send 0,0,0,0 or stay as is?
// Based on requirements, if W > 0, RGB must be 0.
viewModel.updateColor("singleColor", 0, 0, 0, 0)
}
},
onBrightnessChanged = { b -> viewModel.updateBrightness(b) }
)
}
1 -> {
StylePane(
ledState = ledState,
ledConfig = ledConfig,
onModeSelected = { viewModel.updateMode(it) },
onSpeedChanged = { viewModel.updateSpeed(it) }
onParamChanged = { param, value -> viewModel.updateConfigParam(param, value) },
onSegmentColorChanged = { key, r, g, b, w -> viewModel.updateColor(key, r, g, b, w) }
)
}
2 -> {
TimerPane(
timer = ledConfig.timer,
onToggle = { viewModel.toggleTimer() },
onIntervalChange = { key, start, end -> viewModel.updateTimerInterval(key, start, end) }
)
}
3 -> {
val discoveredDevices by viewModel.discoveredDevices.collectAsState()
val savedDevices by viewModel.devices.collectAsState()
val scanStatus by viewModel.scanStatus.collectAsState()
@@ -188,7 +212,7 @@ fun DashboardScreen(viewModel: LedViewModel) {
onStopDiscovery = { viewModel.stopDiscovery() }
)
}
3 -> {
4 -> {
val themeMode by viewModel.themeMode.collectAsState()
val dynamicColor by viewModel.dynamicColor.collectAsState()
@@ -207,12 +231,22 @@ fun DashboardScreen(viewModel: LedViewModel) {

@Composable
fun AdjustPane(
ledConfig: LedConfig,
ledState: LedState,
onPowerToggle: () -> Unit,
onColorChanged: (Int, Int, Int) -> Unit,
onWhiteChanged: (Int) -> Unit,
onBrightnessChanged: (Int) -> Unit
) {
var localBrightness by remember(ledConfig.brightness) { mutableFloatStateOf(ledConfig.brightness.toFloat()) }
var localWhite by remember(ledConfig.singleColor.w) { mutableFloatStateOf(ledConfig.singleColor.w.toFloat()) }

// When an RGB color is picked (wheel or presets), we must reset the local white slider
val handleRgbChange: (Int, Int, Int) -> Unit = { r, g, b ->
localWhite = 0f
onColorChanged(r, g, b)
}

Column(
modifier = Modifier
.fillMaxSize()
@@ -238,16 +272,20 @@ fun AdjustPane(
modifier = Modifier
.size(320.dp)
.padding(8.dp),
onColorChanged = onColorChanged
selectedColor = Color(ledConfig.singleColor.r, ledConfig.singleColor.g, ledConfig.singleColor.b),
onColorChanged = handleRgbChange
)

Spacer(modifier = Modifier.height(24.dp))

// Brightness Slider
Text("Brightness", style = MaterialTheme.typography.titleMedium)
Text("Brightness: ${localBrightness.toInt()}", style = MaterialTheme.typography.titleMedium)
Slider(
value = ledState.brightness.toFloat(),
onValueChange = { onBrightnessChanged(it.toInt()) },
value = localBrightness,
onValueChange = {
localBrightness = it
onBrightnessChanged(it.toInt())
},
valueRange = 0f..255f,
modifier = Modifier.fillMaxWidth()
)
@@ -255,10 +293,13 @@ fun AdjustPane(
Spacer(modifier = Modifier.height(8.dp))

// White Channel Slider
Text("White Channel", style = MaterialTheme.typography.titleMedium)
Text("White Channel: ${localWhite.toInt()}", style = MaterialTheme.typography.titleMedium)
Slider(
value = ledState.color.w.toFloat(),
onValueChange = { onWhiteChanged(it.toInt()) },
value = localWhite,
onValueChange = {
localWhite = it
onWhiteChanged(it.toInt())
},
valueRange = 0f..255f,
modifier = Modifier.fillMaxWidth()
)
@@ -275,9 +316,9 @@ fun AdjustPane(
ColorItem(Color(0xFFFFEB3B), "CCT"),
ColorItem(Color(0xFFBDBDBD), "W")
),
currentColor = Color(ledState.color.r, ledState.color.g, ledState.color.b),
currentColor = Color(ledConfig.singleColor.r, ledConfig.singleColor.g, ledConfig.singleColor.b),
onColorSelected = { color ->
onColorChanged(
handleRgbChange(
(color.red * 255).toInt(),
(color.green * 255).toInt(),
(color.blue * 255).toInt()
@@ -297,9 +338,9 @@ fun AdjustPane(
ColorItem(Color.Green),
ColorItem(Color.Blue)
),
currentColor = Color(ledState.color.r, ledState.color.g, ledState.color.b),
currentColor = Color(ledConfig.singleColor.r, ledConfig.singleColor.g, ledConfig.singleColor.b),
onColorSelected = { color ->
onColorChanged(
handleRgbChange(
(color.red * 255).toInt(),
(color.green * 255).toInt(),
(color.blue * 255).toInt()
@@ -311,9 +352,10 @@ fun AdjustPane(

@Composable
fun StylePane(
ledState: LedState,
ledConfig: LedConfig,
onModeSelected: (LedModeType) -> Unit,
onSpeedChanged: (Int) -> Unit
onParamChanged: (String, String) -> Unit,
onSegmentColorChanged: (String, Int, Int, Int, Int) -> Unit
) {
Column(
modifier = Modifier
@@ -329,7 +371,7 @@ fun StylePane(
val displayName = mode.name.replace("_", " ").lowercase().replaceFirstChar { it.uppercase() }
NavigationDrawerItem(
label = { Text(displayName) },
selected = ledState.mode == mode,
selected = ledConfig.mode == mode,
onClick = { onModeSelected(mode) },
modifier = Modifier.padding(vertical = 4.dp),
colors = NavigationDrawerItemDefaults.colors(
@@ -339,17 +381,222 @@ fun StylePane(
}
Spacer(modifier = Modifier.height(32.dp))
Text("Animation Speed", style = MaterialTheme.typography.titleMedium)

// Mode specific parameters
when (ledConfig.mode) {
LedModeType.RAINBOW -> {
ParamSlider(
label = "Rainbow Delay",
value = ledConfig.rainbowDelay,
range = 0f..500f,
onValueChange = { onParamChanged("rainbowDelay", it.toString()) }
)
}
LedModeType.SPECTRAL -> {
ParamSlider(
label = "Spectral Step Time",
value = ledConfig.spectralStepTime,
range = 100f..30000f,
onValueChange = { onParamChanged("spectralStepTime", it.toString()) }
)
}
LedModeType.FADING -> {
ParamSlider(
label = "Hold Time",
value = ledConfig.fadingHoldTime,
range = 100f..10000f,
onValueChange = { onParamChanged("fadingHoldTime", it.toString()) }
)
ParamSlider(
label = "Fade Time",
value = ledConfig.fadingFadeTime,
range = 100f..5000f,
onValueChange = { onParamChanged("fadingFadeTime", it.toString()) }
)
}
LedModeType.FLASH -> {
ParamSlider(
label = "On Time",
value = ledConfig.flashOnTime,
range = 100f..5000f,
onValueChange = { onParamChanged("flashOnTime", it.toString()) }
)
ParamSlider(
label = "Off Time",
value = ledConfig.flashOffTime,
range = 100f..5000f,
onValueChange = { onParamChanged("flashOffTime", it.toString()) }
)
}
LedModeType.SEGMENTS -> {
SegmentColorPicker("Segment 1", "segmentColor1", ledConfig.segmentColor1, onSegmentColorChanged)
Spacer(modifier = Modifier.height(16.dp))
SegmentColorPicker("Segment 2", "segmentColor2", ledConfig.segmentColor2, onSegmentColorChanged)
Spacer(modifier = Modifier.height(16.dp))
SegmentColorPicker("Segment 3", "segmentColor3", ledConfig.segmentColor3, onSegmentColorChanged)
}
else -> {}
}
}
}

@Composable
fun ParamSlider(label: String, value: Long, range: ClosedFloatingPointRange<Float>, onValueChange: (Int) -> Unit) {
var localValue by remember(value) { mutableFloatStateOf(value.toFloat()) }
Column {
Text("$label: ${localValue.toInt()}ms", style = MaterialTheme.typography.titleMedium)
Slider(
value = ledState.speed.toFloat(),
onValueChange = { onSpeedChanged(it.toInt()) },
valueRange = 0f..255f,
value = localValue,
onValueChange = {
localValue = it
onValueChange(it.toInt())
},
valueRange = range,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
}
}

@Composable
fun SegmentColorPicker(label: String, key: String, color: LedColor, onColorChanged: (String, Int, Int, Int, Int) -> Unit) {
var showDialog by remember { mutableStateOf(false) }
Card(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(label, style = MaterialTheme.typography.titleMedium)
Surface(
modifier = Modifier.size(40.dp),
color = Color(color.r, color.g, color.b),
shape = CircleShape,
border = BorderStroke(1.dp, Color.Gray),
onClick = { showDialog = true }
) {}
}
}
if (showDialog) {
AlertDialog(
onDismissRequest = { showDialog = false },
title = { Text("Select Color for $label") },
text = {
Column {
// Simple color wheel or grid here. For now, a few presets.
val presets = listOf(Color.Red, Color.Green, Color.Blue, Color.Yellow, Color.Magenta, Color.Cyan, Color.White)
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
presets.forEach { preset ->
Surface(
modifier = Modifier.size(32.dp),
color = preset,
shape = CircleShape,
onClick = {
onColorChanged(
key,
(preset.red * 255).toInt(),
(preset.green * 255).toInt(),
(preset.blue * 255).toInt(),
0
)
showDialog = false
}
) {}
}
}
}
},
confirmButton = { TextButton(onClick = { showDialog = false }) { Text("Close") } }
)
}
}

@Composable
fun TimerPane(
timer: LedTimer,
onToggle: () -> Unit,
onIntervalChange: (String, Long, Long) -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Timer Settings", style = MaterialTheme.typography.headlineMedium)
Spacer(modifier = Modifier.height(24.dp))

Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text("Enable Timer", style = MaterialTheme.typography.titleLarge)
Switch(checked = timer.on, onCheckedChange = { onToggle() })
}

Spacer(modifier = Modifier.height(32.dp))

TimerIntervalItem("Interval 1", "timeInterval1", timer.timeInterval1, onIntervalChange)
Spacer(modifier = Modifier.height(24.dp))

TimerIntervalItem("Interval 2", "timeInterval2", timer.timeInterval2, onIntervalChange)
}
}

@Composable
fun TimerIntervalItem(label: String, key: String, interval: TimeInterval, onUpdate: (String, Long, Long) -> Unit) {
var showDialog by remember { mutableStateOf(false) }
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Text(label, style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(8.dp))
Text("From: ${formatSeconds(interval.start)} To: ${formatSeconds(interval.end)}")
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { showDialog = true }) {
Text("Edit Range")
}
}
}
if (showDialog) {
// Very simplified Time Range picker. In production you'd use a real range picker.
AlertDialog(
onDismissRequest = { showDialog = false },
title = { Text("Edit $label") },
text = {
Column {
Text("Implementation placeholder for Time Range Picker")
Text("Currently: ${formatSeconds(interval.start)} - ${formatSeconds(interval.end)}")
}
},
confirmButton = {
TextButton(onClick = {
// Simulate update for testing
onUpdate(key, interval.start + 60, interval.end + 60)
showDialog = false
}) { Text("Test +1m") }
},
dismissButton = {
TextButton(onClick = { showDialog = false }) { Text("Cancel") }
}
)
}
}

private fun formatSeconds(seconds: Long): String {
val h = seconds / 3600
val m = (seconds % 3600) / 60
val s = seconds % 60
return "%02d:%02d:%02d".format(h, m, s)
}

@Composable
fun DevicesPane(
savedDevices: List<LedDevice>,
@@ -421,6 +668,7 @@ fun DevicesPane(
value = ipText,
onValueChange = { ipText = it },
label = { Text("IP Address") },
placeholder = { Text("e.g. 192.168.1.100 (Port 81 is default)") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))

+ 46
- 13
app/src/main/java/ch/spherIC/ledlampcontrol/ui/viewmodel/LedViewModel.kt 查看文件

@@ -2,14 +2,13 @@ package ch.spherIC.ledlampcontrol.ui.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import ch.spherIC.ledlampcontrol.data.model.LedColor
import ch.spherIC.ledlampcontrol.data.model.LedDevice
import ch.spherIC.ledlampcontrol.data.model.LedModeType
import ch.spherIC.ledlampcontrol.data.model.LedState
import ch.spherIC.ledlampcontrol.data.model.*
import ch.spherIC.ledlampcontrol.data.network.LedDiscoveryManager
import ch.spherIC.ledlampcontrol.data.network.LedWebSocketManager
import ch.spherIC.ledlampcontrol.data.repository.LedRepository
import ch.spherIC.ledlampcontrol.data.repository.SettingsRepository
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import java.util.UUID
@@ -21,6 +20,7 @@ class LedViewModel(
) : ViewModel() {

private val _ignoredIps = MutableStateFlow<Set<String>>(emptySet())
private val updateJobs = mutableMapOf<String, Job>()

val devices: StateFlow<List<LedDevice>> = settingsRepository.devicesFlow
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
@@ -48,8 +48,8 @@ class LedViewModel(
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), true)

val connectionState: StateFlow<LedWebSocketManager.ConnectionState> = repository.connectionState
val ledState: StateFlow<LedState> = repository.ledState
val ledConfig: StateFlow<LedConfig> = repository.ledConfig

init {
viewModelScope.launch {
@@ -93,7 +93,6 @@ class LedViewModel(
fun selectDevice(deviceId: String?) {
viewModelScope.launch {
if (deviceId != null && deviceId == selectedDeviceId.value) {
// If the device is already selected, force a reconnection attempt
devices.value.find { it.id == deviceId }?.let { device ->
repository.connect("ws://${device.ip}:${device.port}")
}
@@ -134,19 +133,53 @@ class LedViewModel(
repository.setPower(!currentPower)
}

fun updateColor(r: Int, g: Int, b: Int, w: Int) {
repository.setColor(LedColor(r, g, b, w))
fun updateMode(mode: LedModeType) {
repository.updateMode(mode)
}

fun updateColor(key: String, r: Int, g: Int, b: Int, w: Int) {
sendDebounced("color_$key") {
if (key == "singleColor" && ledConfig.value.mode != LedModeType.SINGLE_COLOR) {
repository.updateMode(LedModeType.SINGLE_COLOR)
}
repository.updateColor(key, LedColor(r, g, b, w))
}
}

fun updateBrightness(brightness: Int) {
repository.setBrightness(brightness)
sendDebounced("brightness") {
repository.updateBrightness(brightness)
}
}

fun updateMode(mode: LedModeType) {
repository.setMode(mode)
fun updateConfigParam(param: String, value: String) {
sendDebounced("param_$param") {
repository.updateConfigParam(param, value)
}
}

private fun sendDebounced(key: String, delayMillis: Long = 200, block: () -> Unit) {
updateJobs[key]?.cancel()
updateJobs[key] = viewModelScope.launch {
delay(delayMillis)
block()
updateJobs.remove(key)
}
}

fun toggleTimer() {
repository.updateTimer(!ledConfig.value.timer.on)
}

fun updateTimerInterval(key: String, startTime: Long, endTime: Long) {
val range = "${formatSeconds(startTime)} - ${formatSeconds(endTime)}"
repository.updateTimerInterval(key, range)
}

fun updateSpeed(speed: Int) {
repository.setSpeed(speed)
private fun formatSeconds(seconds: Long): String {
val h = seconds / 3600
val m = (seconds % 3600) / 60
val s = seconds % 60
return "%02d:%02d:%02d".format(h, m, s)
}
}

Loading…
取消
儲存