Przeglądaj źródła

several functional enhancements

master
gituser 2 tygodni temu
rodzic
commit
9dc9a6b95f

+ 6
- 0
.idea/vcs.xml Wyświetl plik

<?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 Wyświetl plik

import kotlinx.serialization.json.JsonDecoder import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.booleanOrNull import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.intOrNull


@Serializable @Serializable
data class LedColor( data class LedColor(
val w: Int = 0 val w: Int = 0
) )


@Serializable
@Serializable(with = LedModeTypeSerializer::class)
enum class LedModeType { 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 @Serializable
data class LedDevice( data class LedDevice(
val id: String, val id: String,
val port: Int = 81 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 @Serializable
data class LedState( data class LedState(
@Serializable(with = PowerBooleanSerializer::class) @Serializable(with = PowerBooleanSerializer::class)
) : LedCommand() ) : LedCommand()
@Serializable @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 @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 @Serializable
data class SetMode(val mode: LedModeType) : LedCommand()
data class ChangeBrightness(
val cmd: String = "CHG_BRIGHTNESS",
val `val`: String // "255"
) : LedCommand()
@Serializable @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 @Serializable
data object RequestState : LedCommand() 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 Wyświetl plik

package ch.spherIC.ledlampcontrol.data.network package ch.spherIC.ledlampcontrol.data.network


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


class LedWebSocketManager(private val client: OkHttpClient) { class LedWebSocketManager(private val client: OkHttpClient) {


private val _ledState = MutableStateFlow(LedState()) private val _ledState = MutableStateFlow(LedState())
val ledState: StateFlow<LedState> = _ledState.asStateFlow() val ledState: StateFlow<LedState> = _ledState.asStateFlow()


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

private val json = Json { private val json = Json {
ignoreUnknownKeys = true ignoreUnknownKeys = true
encodeDefaults = true encodeDefaults = true
webSocket = client.newWebSocket(request, object : WebSocketListener() { webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) { 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 _connectionState.value = ConnectionState.Connected
sendCommand(LedCommand.RequestState)
// After opening, get full config
sendCommand(LedCommand.GetConfig())
} }


override fun onMessage(webSocket: WebSocket, text: String) { override fun onMessage(webSocket: WebSocket, text: String) {
if (currentUrl != url) return if (currentUrl != url) return
// Try to handle raw status strings first
val rawText = text.trim().uppercase() val rawText = text.trim().uppercase()
if (rawText == "LED ON" || rawText == "LED OFF") { if (rawText == "LED ON" || rawText == "LED OFF") {
val isPowerOn = rawText == "LED ON" val isPowerOn = rawText == "LED ON"
} }


try { 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) { } catch (e: Exception) {
// Handle parsing error // Handle parsing error
} }
fun sendCommand(command: LedCommand) { fun sendCommand(command: LedCommand) {
val jsonElement = when (command) { val jsonElement = when (command) {
is LedCommand.SetPower -> json.encodeToJsonElement(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.SetBrightness -> json.encodeToJsonElement(command)
is LedCommand.SetColor -> json.encodeToJsonElement(command)
is LedCommand.SetMode -> json.encodeToJsonElement(command) is LedCommand.SetMode -> json.encodeToJsonElement(command)
is LedCommand.SetSpeed -> json.encodeToJsonElement(command) is LedCommand.SetSpeed -> json.encodeToJsonElement(command)
is LedCommand.RequestState -> json.encodeToJsonElement(command)
} }
webSocket?.send(jsonElement.toString()) 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 { sealed class ConnectionState {

+ 21
- 11
app/src/main/java/ch/spherIC/ledlampcontrol/data/repository/LedRepository.kt Wyświetl plik



import ch.spherIC.ledlampcontrol.data.model.LedColor import ch.spherIC.ledlampcontrol.data.model.LedColor
import ch.spherIC.ledlampcontrol.data.model.LedCommand 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.LedModeType
import ch.spherIC.ledlampcontrol.data.model.LedState import ch.spherIC.ledlampcontrol.data.model.LedState
import ch.spherIC.ledlampcontrol.data.network.LedWebSocketManager import ch.spherIC.ledlampcontrol.data.network.LedWebSocketManager
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow


class LedRepository(private val webSocketManager: LedWebSocketManager) { class LedRepository(private val webSocketManager: LedWebSocketManager) {


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


fun connect(url: String) { fun connect(url: String) {
webSocketManager.connect(url) webSocketManager.connect(url)
webSocketManager.sendCommand(LedCommand.SetPower(status = if (power) "on" else "off")) 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 Wyświetl plik

import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding 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.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlin.math.atan2 import kotlin.math.atan2
@Composable @Composable
fun ColorWheel( fun ColorWheel(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
selectedColor: Color,
onColorChanged: (r: Int, g: Int, b: Int) -> Unit 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)) { BoxWithConstraints(modifier = modifier.aspectRatio(1f)) {
val radius = min(constraints.maxWidth, constraints.maxHeight) / 2f val radius = min(constraints.maxWidth, constraints.maxHeight) / 2f
) { ) {
val sweepGradient = Brush.sweepGradient( val sweepGradient = Brush.sweepGradient(
colors = listOf( 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 center = center
) )

+ 278
- 30
app/src/main/java/ch/spherIC/ledlampcontrol/ui/dashboard/DashboardScreen.kt Wyświetl plik

import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp 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.data.network.LedWebSocketManager
import ch.spherIC.ledlampcontrol.ui.components.ColorWheel import ch.spherIC.ledlampcontrol.ui.components.ColorWheel
import ch.spherIC.ledlampcontrol.ui.theme.LEDLampControlTheme import ch.spherIC.ledlampcontrol.ui.theme.LEDLampControlTheme
@Composable @Composable
fun DashboardScreen(viewModel: LedViewModel) { fun DashboardScreen(viewModel: LedViewModel) {
val ledState by viewModel.ledState.collectAsState() val ledState by viewModel.ledState.collectAsState()
val ledConfig by viewModel.ledConfig.collectAsState()
var selectedTab by remember { mutableIntStateOf(0) } var selectedTab by remember { mutableIntStateOf(0) }
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
NavigationBarItem( NavigationBarItem(
selected = selectedTab == 2, selected = selectedTab == 2,
onClick = { 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( NavigationBarItem(
selected = selectedTab == 3, selected = selectedTab == 3,
onClick = { 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") }, icon = { Icon(Icons.Default.Palette, contentDescription = "Appearance") },
label = { Text("Appearance") } label = { Text("Appearance") }
) )
Box( Box(
modifier = Modifier modifier = Modifier
.padding(paddingValues) .padding(paddingValues)
.imePadding() // Moves the content up when the keyboard appears
.imePadding()
.fillMaxSize() .fillMaxSize()
) { ) {
when (selectedTab) { when (selectedTab) {
0 -> { 0 -> {
AdjustPane( AdjustPane(
ledConfig = ledConfig,
ledState = ledState, ledState = ledState,
onPowerToggle = { viewModel.togglePower() }, 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) } onBrightnessChanged = { b -> viewModel.updateBrightness(b) }
) )
} }
1 -> { 1 -> {
StylePane( StylePane(
ledState = ledState,
ledConfig = ledConfig,
onModeSelected = { viewModel.updateMode(it) }, 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 -> { 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 discoveredDevices by viewModel.discoveredDevices.collectAsState()
val savedDevices by viewModel.devices.collectAsState() val savedDevices by viewModel.devices.collectAsState()
val scanStatus by viewModel.scanStatus.collectAsState() val scanStatus by viewModel.scanStatus.collectAsState()
onStopDiscovery = { viewModel.stopDiscovery() } onStopDiscovery = { viewModel.stopDiscovery() }
) )
} }
3 -> {
4 -> {
val themeMode by viewModel.themeMode.collectAsState() val themeMode by viewModel.themeMode.collectAsState()
val dynamicColor by viewModel.dynamicColor.collectAsState() val dynamicColor by viewModel.dynamicColor.collectAsState()


@Composable @Composable
fun AdjustPane( fun AdjustPane(
ledConfig: LedConfig,
ledState: LedState, ledState: LedState,
onPowerToggle: () -> Unit, onPowerToggle: () -> Unit,
onColorChanged: (Int, Int, Int) -> Unit, onColorChanged: (Int, Int, Int) -> Unit,
onWhiteChanged: (Int) -> Unit, onWhiteChanged: (Int) -> Unit,
onBrightnessChanged: (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( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
modifier = Modifier modifier = Modifier
.size(320.dp) .size(320.dp)
.padding(8.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)) Spacer(modifier = Modifier.height(24.dp))


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


// White Channel Slider // White Channel Slider
Text("White Channel", style = MaterialTheme.typography.titleMedium)
Text("White Channel: ${localWhite.toInt()}", style = MaterialTheme.typography.titleMedium)
Slider( Slider(
value = ledState.color.w.toFloat(),
onValueChange = { onWhiteChanged(it.toInt()) },
value = localWhite,
onValueChange = {
localWhite = it
onWhiteChanged(it.toInt())
},
valueRange = 0f..255f, valueRange = 0f..255f,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
ColorItem(Color(0xFFFFEB3B), "CCT"), ColorItem(Color(0xFFFFEB3B), "CCT"),
ColorItem(Color(0xFFBDBDBD), "W") 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 -> onColorSelected = { color ->
onColorChanged(
handleRgbChange(
(color.red * 255).toInt(), (color.red * 255).toInt(),
(color.green * 255).toInt(), (color.green * 255).toInt(),
(color.blue * 255).toInt() (color.blue * 255).toInt()
ColorItem(Color.Green), ColorItem(Color.Green),
ColorItem(Color.Blue) 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 -> onColorSelected = { color ->
onColorChanged(
handleRgbChange(
(color.red * 255).toInt(), (color.red * 255).toInt(),
(color.green * 255).toInt(), (color.green * 255).toInt(),
(color.blue * 255).toInt() (color.blue * 255).toInt()


@Composable @Composable
fun StylePane( fun StylePane(
ledState: LedState,
ledConfig: LedConfig,
onModeSelected: (LedModeType) -> Unit, onModeSelected: (LedModeType) -> Unit,
onSpeedChanged: (Int) -> Unit
onParamChanged: (String, String) -> Unit,
onSegmentColorChanged: (String, Int, Int, Int, Int) -> Unit
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
val displayName = mode.name.replace("_", " ").lowercase().replaceFirstChar { it.uppercase() } val displayName = mode.name.replace("_", " ").lowercase().replaceFirstChar { it.uppercase() }
NavigationDrawerItem( NavigationDrawerItem(
label = { Text(displayName) }, label = { Text(displayName) },
selected = ledState.mode == mode,
selected = ledConfig.mode == mode,
onClick = { onModeSelected(mode) }, onClick = { onModeSelected(mode) },
modifier = Modifier.padding(vertical = 4.dp), modifier = Modifier.padding(vertical = 4.dp),
colors = NavigationDrawerItemDefaults.colors( colors = NavigationDrawerItemDefaults.colors(
} }
Spacer(modifier = Modifier.height(32.dp)) 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( 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() 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 @Composable
fun DevicesPane( fun DevicesPane(
savedDevices: List<LedDevice>, savedDevices: List<LedDevice>,
value = ipText, value = ipText,
onValueChange = { ipText = it }, onValueChange = { ipText = it },
label = { Text("IP Address") }, label = { Text("IP Address") },
placeholder = { Text("e.g. 192.168.1.100 (Port 81 is default)") },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))

+ 46
- 13
app/src/main/java/ch/spherIC/ledlampcontrol/ui/viewmodel/LedViewModel.kt Wyświetl plik



import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope 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.LedDiscoveryManager
import ch.spherIC.ledlampcontrol.data.network.LedWebSocketManager import ch.spherIC.ledlampcontrol.data.network.LedWebSocketManager
import ch.spherIC.ledlampcontrol.data.repository.LedRepository import ch.spherIC.ledlampcontrol.data.repository.LedRepository
import ch.spherIC.ledlampcontrol.data.repository.SettingsRepository import ch.spherIC.ledlampcontrol.data.repository.SettingsRepository
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.UUID import java.util.UUID
) : ViewModel() { ) : ViewModel() {


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


val devices: StateFlow<List<LedDevice>> = settingsRepository.devicesFlow val devices: StateFlow<List<LedDevice>> = settingsRepository.devicesFlow
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), true) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), true)


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


init { init {
viewModelScope.launch { viewModelScope.launch {
fun selectDevice(deviceId: String?) { fun selectDevice(deviceId: String?) {
viewModelScope.launch { viewModelScope.launch {
if (deviceId != null && deviceId == selectedDeviceId.value) { if (deviceId != null && deviceId == selectedDeviceId.value) {
// If the device is already selected, force a reconnection attempt
devices.value.find { it.id == deviceId }?.let { device -> devices.value.find { it.id == deviceId }?.let { device ->
repository.connect("ws://${device.ip}:${device.port}") repository.connect("ws://${device.ip}:${device.port}")
} }
repository.setPower(!currentPower) 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) { 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)
} }
} }

Ładowanie…
Anuluj
Zapisz