| <?xml version="1.0" encoding="UTF-8"?> | |||||
| <project version="4"> | |||||
| <component name="VcsDirectoryMappings"> | |||||
| <mapping directory="$PROJECT_DIR$" vcs="Git" /> | |||||
| </component> | |||||
| </project> |
| 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() | |||||
| } | } |
| 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 { |
| 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()) | |||||
| } | } | ||||
| } | } |
| 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 | ||||
| ) | ) |
| 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)) |
| 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) | |||||
| } | } | ||||
| } | } |