Просмотр исходного кода

add translation for German and Italian

master
gituser 2 недель назад
Родитель
Сommit
83909fd379

+ 17
- 14
app/src/main/java/ch/spherIC/ledlampcontrol/data/network/LedDiscoveryManager.kt Просмотреть файл



class LedDiscoveryManager(context: Context) { class LedDiscoveryManager(context: Context) {


sealed class ScanStatus {
object Idle : ScanStatus()
object Scanning : ScanStatus()
data class Found(val name: String) : ScanStatus()
data class Error(val message: String? = null) : ScanStatus()
object Failed : ScanStatus()
}

private val nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager private val nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager
private val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager private val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
private var multicastLock: WifiManager.MulticastLock? = null private var multicastLock: WifiManager.MulticastLock? = null
private val _discoveredDevices = MutableStateFlow<List<LedDevice>>(emptyList()) private val _discoveredDevices = MutableStateFlow<List<LedDevice>>(emptyList())
val discoveredDevices: StateFlow<List<LedDevice>> = _discoveredDevices.asStateFlow() val discoveredDevices: StateFlow<List<LedDevice>> = _discoveredDevices.asStateFlow()


private val _scanStatus = MutableStateFlow("Idle")
val scanStatus: StateFlow<String> = _scanStatus.asStateFlow()
private val _scanStatus = MutableStateFlow<ScanStatus>(ScanStatus.Idle)
val scanStatus: StateFlow<ScanStatus> = _scanStatus.asStateFlow()


private val _isScanning = MutableStateFlow(false) private val _isScanning = MutableStateFlow(false)
val isScanning: StateFlow<Boolean> = _isScanning.asStateFlow() val isScanning: StateFlow<Boolean> = _isScanning.asStateFlow()


// Service type from your ESP8266
private val SERVICE_TYPE = "_LEDLamp._tcp" private val SERVICE_TYPE = "_LEDLamp._tcp"
private val resolvedNames = mutableSetOf<String>() private val resolvedNames = mutableSetOf<String>()


private val discoveryListener = object : NsdManager.DiscoveryListener { private val discoveryListener = object : NsdManager.DiscoveryListener {
override fun onDiscoveryStarted(regType: String) { override fun onDiscoveryStarted(regType: String) {
_scanStatus.value = "Scanning..."
_scanStatus.value = ScanStatus.Scanning
_isScanning.value = true _isScanning.value = true
} }


override fun onServiceFound(service: NsdServiceInfo) { override fun onServiceFound(service: NsdServiceInfo) {
// Prevent duplicate resolve requests for the same name
if (resolvedNames.contains(service.serviceName)) return if (resolvedNames.contains(service.serviceName)) return
_scanStatus.value = "Found: ${service.serviceName}"
nsdManager.resolveService(service, object : NsdManager.ResolveListener { nsdManager.resolveService(service, object : NsdManager.ResolveListener {
override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) { override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
_scanStatus.value = "Error resolving ${serviceInfo.serviceName}"
_scanStatus.value = ScanStatus.Error("Resolve failed: $errorCode")
} }


override fun onServiceResolved(serviceInfo: NsdServiceInfo) { override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
if (ip.isNotBlank() && !resolvedNames.contains(serviceInfo.serviceName)) { if (ip.isNotBlank() && !resolvedNames.contains(serviceInfo.serviceName)) {
resolvedNames.add(serviceInfo.serviceName) resolvedNames.add(serviceInfo.serviceName)
// Extract name from TXT record if available, else use service name
val displayName = serviceInfo.attributes["name"]?.let { String(it) } val displayName = serviceInfo.attributes["name"]?.let { String(it) }
?: serviceInfo.serviceName ?: serviceInfo.serviceName


port = 81 port = 81
) )
_discoveredDevices.update { (it + device).distinctBy { d -> d.ip } } _discoveredDevices.update { (it + device).distinctBy { d -> d.ip } }
_scanStatus.value = "Discovered: $displayName"
_scanStatus.value = ScanStatus.Found(displayName)
} }
} }
}) })
} }


override fun onDiscoveryStopped(regType: String) { override fun onDiscoveryStopped(regType: String) {
_scanStatus.value = "Idle"
_scanStatus.value = ScanStatus.Idle
_isScanning.value = false _isScanning.value = false
} }


override fun onStartDiscoveryFailed(regType: String, errorCode: Int) { override fun onStartDiscoveryFailed(regType: String, errorCode: Int) {
_scanStatus.value = "Failed to start"
_scanStatus.value = ScanStatus.Failed
_isScanning.value = false _isScanning.value = false
stopDiscovery() stopDiscovery()
} }
try { try {
nsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener) nsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener)
} catch (e: Exception) { } catch (e: Exception) {
_scanStatus.value = "Discovery error"
_scanStatus.value = ScanStatus.Error(e.message)
_isScanning.value = false _isScanning.value = false
} }
} }
try { try {
nsdManager.stopServiceDiscovery(discoveryListener) nsdManager.stopServiceDiscovery(discoveryListener)
} catch (e: Exception) { } catch (e: Exception) {
// Already stopped or not started
// Already stopped
} }
_isScanning.value = false _isScanning.value = false
if (multicastLock?.isHeld == true) { if (multicastLock?.isHeld == true) {

+ 92
- 75
app/src/main/java/ch/spherIC/ledlampcontrol/ui/dashboard/DashboardScreen.kt Просмотреть файл

import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
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.R
import ch.spherIC.ledlampcontrol.data.model.* 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.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
drawerState = drawerState, drawerState = drawerState,
drawerContent = { drawerContent = {
ModalDrawerSheet { ModalDrawerSheet {
Text("Select Device", modifier = Modifier.padding(16.dp), style = MaterialTheme.typography.titleLarge)
Text(stringResource(R.string.drawer_title), modifier = Modifier.padding(16.dp), style = MaterialTheme.typography.titleLarge)
HorizontalDivider() HorizontalDivider()
devices.forEach { device -> devices.forEach { device ->
val isSelected = selectedDeviceId == device.id val isSelected = selectedDeviceId == device.id
val statusText = if (isSelected) { val statusText = if (isSelected) {
when (connectionState) { when (connectionState) {
is LedWebSocketManager.ConnectionState.Connected -> "Connected"
is LedWebSocketManager.ConnectionState.Connecting -> "Connecting..."
is LedWebSocketManager.ConnectionState.Disconnected -> "Disconnected"
is LedWebSocketManager.ConnectionState.Error -> "Connection failed"
is LedWebSocketManager.ConnectionState.Connected -> stringResource(R.string.status_connected)
is LedWebSocketManager.ConnectionState.Connecting -> stringResource(R.string.status_connecting)
is LedWebSocketManager.ConnectionState.Disconnected -> stringResource(R.string.status_disconnected)
is LedWebSocketManager.ConnectionState.Error -> stringResource(R.string.status_failed)
} }
} else "Inactive"
} else stringResource(R.string.status_inactive)


val statusColor = if (isSelected) { val statusColor = if (isSelected) {
when (connectionState) { when (connectionState) {
} }
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
NavigationDrawerItem( NavigationDrawerItem(
label = { Text("Manage Devices") },
label = { Text(stringResource(R.string.drawer_manage_devices)) },
selected = selectedTab == 3, selected = selectedTab == 3,
onClick = { onClick = {
selectedTab = 3 selectedTab = 3
CenterAlignedTopAppBar( CenterAlignedTopAppBar(
title = { title = {
val currentDevice = devices.find { it.id == selectedDeviceId } val currentDevice = devices.find { it.id == selectedDeviceId }
Text(currentDevice?.name ?: "LED Control")
Text(currentDevice?.name ?: stringResource(R.string.app_name))
}, },
navigationIcon = { navigationIcon = {
IconButton(onClick = { scope.launch { drawerState.open() } }) { IconButton(onClick = { scope.launch { drawerState.open() } }) {
NavigationBarItem( NavigationBarItem(
selected = selectedTab == 0, selected = selectedTab == 0,
onClick = { selectedTab = 0 }, onClick = { selectedTab = 0 },
icon = { Icon(Icons.Default.Tune, contentDescription = "Adjust") },
label = { Text("Adjust") }
icon = { Icon(Icons.Default.Tune, contentDescription = stringResource(R.string.tab_adjust)) },
label = { Text(stringResource(R.string.tab_adjust)) }
) )
NavigationBarItem( NavigationBarItem(
selected = selectedTab == 1, selected = selectedTab == 1,
onClick = { selectedTab = 1 }, onClick = { selectedTab = 1 },
icon = { Icon(Icons.Default.GridView, contentDescription = "Modes") },
label = { Text("Modes") }
icon = { Icon(Icons.Default.GridView, contentDescription = stringResource(R.string.tab_modes)) },
label = { Text(stringResource(R.string.tab_modes)) }
) )
NavigationBarItem( NavigationBarItem(
selected = selectedTab == 2, selected = selectedTab == 2,
onClick = { selectedTab = 2 }, onClick = { selectedTab = 2 },
icon = { Icon(Icons.Default.Timer, contentDescription = "Timer") },
label = { Text("Timer") }
icon = { Icon(Icons.Default.Timer, contentDescription = stringResource(R.string.tab_timer)) },
label = { Text(stringResource(R.string.tab_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") }
icon = { Icon(Icons.Default.Router, contentDescription = stringResource(R.string.tab_devices)) },
label = { Text(stringResource(R.string.tab_devices)) }
) )
NavigationBarItem( NavigationBarItem(
selected = selectedTab == 4, selected = selectedTab == 4,
onClick = { selectedTab = 4 }, onClick = { selectedTab = 4 },
icon = { Icon(Icons.Default.Palette, contentDescription = "Style") },
label = { Text("Style") }
icon = { Icon(Icons.Default.Palette, contentDescription = stringResource(R.string.tab_style)) },
label = { Text(stringResource(R.string.tab_style)) }
) )
} }
} }
if (w > 0) { if (w > 0) {
viewModel.updateColor("singleColor", 0, 0, 0, w) viewModel.updateColor("singleColor", 0, 0, 0, w)
} else { } 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) viewModel.updateColor("singleColor", 0, 0, 0, 0)
} }
}, },
var localBrightness by remember(ledConfig.brightness) { mutableFloatStateOf(ledConfig.brightness.toFloat()) } var localBrightness by remember(ledConfig.brightness) { mutableFloatStateOf(ledConfig.brightness.toFloat()) }
var localWhite by remember(ledConfig.singleColor.w) { mutableFloatStateOf(ledConfig.singleColor.w.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 -> val handleRgbChange: (Int, Int, Int) -> Unit = { r, g, b ->
localWhite = 0f localWhite = 0f
onColorChanged(r, g, b) onColorChanged(r, g, b)
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text("Power", style = MaterialTheme.typography.titleLarge)
Text(stringResource(R.string.label_power), style = MaterialTheme.typography.titleLarge)
Switch( Switch(
checked = ledState.power, checked = ledState.power,
onCheckedChange = { onPowerToggle() } onCheckedChange = { onPowerToggle() }


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


// Brightness Slider
Text("Brightness: ${localBrightness.toInt()}", style = MaterialTheme.typography.titleMedium)
Text(stringResource(R.string.label_brightness, localBrightness.toInt()), style = MaterialTheme.typography.titleMedium)
Slider( Slider(
value = localBrightness, value = localBrightness,
onValueChange = { onValueChange = {


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


// White Channel Slider
Text("White Channel: ${localWhite.toInt()}", style = MaterialTheme.typography.titleMedium)
Text(stringResource(R.string.label_white_channel, localWhite.toInt()), style = MaterialTheme.typography.titleMedium)
Slider( Slider(
value = localWhite, value = localWhite,
onValueChange = { onValueChange = {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))


PresetColorSection( PresetColorSection(
title = "Preset",
title = stringResource(R.string.label_preset),
items = listOf( items = listOf(
ColorItem(Color(0xFF4CAF50)), // Green ColorItem(Color(0xFF4CAF50)), // Green
ColorItem(Color(0xFFE91E63)), // Magenta ColorItem(Color(0xFFE91E63)), // Magenta
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))


PresetColorSection( PresetColorSection(
title = "Classic",
title = stringResource(R.string.label_classic),
items = listOf( items = listOf(
ColorItem(Color.Yellow), ColorItem(Color.Yellow),
ColorItem(Color.White), ColorItem(Color.White),
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Text("Animated Modes", style = MaterialTheme.typography.headlineMedium)
Text(stringResource(R.string.title_modes), style = MaterialTheme.typography.headlineMedium)
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
LedModeType.entries.forEach { mode -> LedModeType.entries.forEach { mode ->
val displayName = mode.name.replace("_", " ").lowercase().replaceFirstChar { it.uppercase() }
val stringId = when (mode) {
LedModeType.SINGLE_COLOR -> R.string.mode_single_color
LedModeType.RAINBOW -> R.string.mode_rainbow
LedModeType.SPECTRAL -> R.string.mode_spectral
LedModeType.FADING -> R.string.mode_fading
LedModeType.FLASH -> R.string.mode_flash
LedModeType.SEGMENTS -> R.string.mode_segments
}
val displayName = stringResource(stringId)
NavigationDrawerItem( NavigationDrawerItem(
label = { Text(displayName) }, label = { Text(displayName) },
selected = ledConfig.mode == mode, selected = ledConfig.mode == mode,
Spacer(modifier = Modifier.height(32.dp)) Spacer(modifier = Modifier.height(32.dp))


// Mode specific parameters
when (ledConfig.mode) { when (ledConfig.mode) {
LedModeType.RAINBOW -> { LedModeType.RAINBOW -> {
ParamSlider( ParamSlider(
label = "Rainbow Delay",
label = stringResource(R.string.param_rainbow_delay, ledConfig.rainbowDelay.toInt()),
value = ledConfig.rainbowDelay, value = ledConfig.rainbowDelay,
range = 0f..500f, range = 0f..500f,
onValueChange = { onParamChanged("rainbowDelay", it.toString()) } onValueChange = { onParamChanged("rainbowDelay", it.toString()) }
} }
LedModeType.SPECTRAL -> { LedModeType.SPECTRAL -> {
ParamSlider( ParamSlider(
label = "Spectral Step Time",
label = stringResource(R.string.param_spectral_step, ledConfig.spectralStepTime.toInt()),
value = ledConfig.spectralStepTime, value = ledConfig.spectralStepTime,
range = 100f..30000f, range = 100f..30000f,
onValueChange = { onParamChanged("spectralStepTime", it.toString()) } onValueChange = { onParamChanged("spectralStepTime", it.toString()) }
} }
LedModeType.FADING -> { LedModeType.FADING -> {
ParamSlider( ParamSlider(
label = "Hold Time",
label = stringResource(R.string.param_fading_hold, ledConfig.fadingHoldTime.toInt()),
value = ledConfig.fadingHoldTime, value = ledConfig.fadingHoldTime,
range = 100f..10000f, range = 100f..10000f,
onValueChange = { onParamChanged("fadingHoldTime", it.toString()) } onValueChange = { onParamChanged("fadingHoldTime", it.toString()) }
) )
ParamSlider( ParamSlider(
label = "Fade Time",
label = stringResource(R.string.param_fading_fade, ledConfig.fadingFadeTime.toInt()),
value = ledConfig.fadingFadeTime, value = ledConfig.fadingFadeTime,
range = 100f..5000f, range = 100f..5000f,
onValueChange = { onParamChanged("fadingFadeTime", it.toString()) } onValueChange = { onParamChanged("fadingFadeTime", it.toString()) }
} }
LedModeType.FLASH -> { LedModeType.FLASH -> {
ParamSlider( ParamSlider(
label = "On Time",
label = stringResource(R.string.param_flash_on, ledConfig.flashOnTime.toInt()),
value = ledConfig.flashOnTime, value = ledConfig.flashOnTime,
range = 100f..5000f, range = 100f..5000f,
onValueChange = { onParamChanged("flashOnTime", it.toString()) } onValueChange = { onParamChanged("flashOnTime", it.toString()) }
) )
ParamSlider( ParamSlider(
label = "Off Time",
label = stringResource(R.string.param_flash_off, ledConfig.flashOffTime.toInt()),
value = ledConfig.flashOffTime, value = ledConfig.flashOffTime,
range = 100f..5000f, range = 100f..5000f,
onValueChange = { onParamChanged("flashOffTime", it.toString()) } onValueChange = { onParamChanged("flashOffTime", it.toString()) }
) )
} }
LedModeType.SEGMENTS -> { LedModeType.SEGMENTS -> {
SegmentColorPicker("Segment 1", "segmentColor1", ledConfig.segmentColor1, onSegmentColorChanged)
SegmentColorPicker(stringResource(R.string.label_segment, 1), "segmentColor1", ledConfig.segmentColor1, onSegmentColorChanged)
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
SegmentColorPicker("Segment 2", "segmentColor2", ledConfig.segmentColor2, onSegmentColorChanged)
SegmentColorPicker(stringResource(R.string.label_segment, 2), "segmentColor2", ledConfig.segmentColor2, onSegmentColorChanged)
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
SegmentColorPicker("Segment 3", "segmentColor3", ledConfig.segmentColor3, onSegmentColorChanged)
SegmentColorPicker(stringResource(R.string.label_segment, 3), "segmentColor3", ledConfig.segmentColor3, onSegmentColorChanged)
} }
else -> {} else -> {}
} }
var localValue by remember(value) { mutableFloatStateOf(value.toFloat()) } var localValue by remember(value) { mutableFloatStateOf(value.toFloat()) }
Column { Column {
Text("$label: ${localValue.toInt()}ms", style = MaterialTheme.typography.titleMedium)
Text(label, style = MaterialTheme.typography.titleMedium)
Slider( Slider(
value = localValue, value = localValue,
onValueChange = { onValueChange = {
if (showDialog) { if (showDialog) {
AlertDialog( AlertDialog(
onDismissRequest = { showDialog = false }, onDismissRequest = { showDialog = false },
title = { Text("Select Color for $label") },
title = { Text(stringResource(R.string.btn_change_color) + ": " + label) },
text = { text = {
Column { 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) val presets = listOf(Color.Red, Color.Green, Color.Blue, Color.Yellow, Color.Magenta, Color.Cyan, Color.White)
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
presets.forEach { preset -> presets.forEach { preset ->
} }
} }
}, },
confirmButton = { TextButton(onClick = { showDialog = false }) { Text("Close") } }
confirmButton = { TextButton(onClick = { showDialog = false }) { Text(stringResource(R.string.btn_cancel)) } }
) )
} }
} }
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Text("Timer Settings", style = MaterialTheme.typography.headlineMedium)
Text(stringResource(R.string.title_timer), style = MaterialTheme.typography.headlineMedium)
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))


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


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


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


TimerIntervalItem("Interval 2", "timeInterval2", timer.timeInterval2, onIntervalChange)
TimerIntervalItem(stringResource(R.string.label_interval_2), "timeInterval2", timer.timeInterval2, onIntervalChange)
} }
} }


Column(modifier = Modifier.padding(16.dp)) { Column(modifier = Modifier.padding(16.dp)) {
Text(label, style = MaterialTheme.typography.titleMedium) Text(label, style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text("From: ${formatSeconds(interval.start)} To: ${formatSeconds(interval.end)}")
Text(stringResource(R.string.label_time_range, formatSeconds(interval.start), formatSeconds(interval.end)))
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { showDialog = true }) { Button(onClick = { showDialog = true }) {
Text("Edit Range")
Text(stringResource(R.string.btn_edit_range))
} }
} }
} }


AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = { Text("Set Time Range for $label") },
title = { Text(stringResource(R.string.dialog_timer_title, label)) },
text = { text = {
Column(horizontalAlignment = Alignment.CenterHorizontally) { Column(horizontalAlignment = Alignment.CenterHorizontally) {
TabRow(selectedTabIndex = if (pickingStart) 0 else 1) { TabRow(selectedTabIndex = if (pickingStart) 0 else 1) {
Tab(selected = pickingStart, onClick = { pickingStart = true }) { Tab(selected = pickingStart, onClick = { pickingStart = true }) {
Text("Start Time", modifier = Modifier.padding(16.dp))
Text(stringResource(R.string.label_start_time), modifier = Modifier.padding(16.dp))
} }
Tab(selected = !pickingStart, onClick = { pickingStart = false }) { Tab(selected = !pickingStart, onClick = { pickingStart = false }) {
Text("End Time", modifier = Modifier.padding(16.dp))
Text(stringResource(R.string.label_end_time), modifier = Modifier.padding(16.dp))
} }
} }
val endSeconds = endState.hour * 3600L + endState.minute * 60L val endSeconds = endState.hour * 3600L + endState.minute * 60L
onConfirm(startSeconds, endSeconds) onConfirm(startSeconds, endSeconds)
}) { }) {
Text("Confirm")
Text(stringResource(R.string.btn_confirm))
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismiss) { TextButton(onClick = onDismiss) {
Text("Cancel")
Text(stringResource(R.string.btn_cancel))
} }
} }
) )
fun DevicesPane( fun DevicesPane(
savedDevices: List<LedDevice>, savedDevices: List<LedDevice>,
discoveredDevices: List<LedDevice>, discoveredDevices: List<LedDevice>,
scanStatus: String,
scanStatus: LedDiscoveryManager.ScanStatus,
isScanning: Boolean, isScanning: Boolean,
onAddDevice: (String, String) -> Unit, onAddDevice: (String, String) -> Unit,
onRemoveDevice: (String) -> Unit, onRemoveDevice: (String) -> Unit,
} }
} }


val statusString = when (scanStatus) {
is LedDiscoveryManager.ScanStatus.Idle -> stringResource(R.string.scan_idle)
is LedDiscoveryManager.ScanStatus.Scanning -> stringResource(R.string.scan_scanning)
is LedDiscoveryManager.ScanStatus.Found -> stringResource(R.string.scan_found, scanStatus.name)
is LedDiscoveryManager.ScanStatus.Error -> stringResource(R.string.scan_error) + (scanStatus.message?.let { ": $it" } ?: "")
is LedDiscoveryManager.ScanStatus.Failed -> stringResource(R.string.scan_failed)
}

Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Text("Manage Devices", style = MaterialTheme.typography.headlineMedium)
Text("Scanner Status: $scanStatus", style = MaterialTheme.typography.bodySmall, color = Color.Gray)
Text(stringResource(R.string.title_devices), style = MaterialTheme.typography.headlineMedium)
Text(stringResource(R.string.status_scanner, statusString), style = MaterialTheme.typography.bodySmall, color = Color.Gray)
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
if (isScanning) { if (isScanning) {
CircularProgressIndicator(modifier = Modifier.size(32.dp)) CircularProgressIndicator(modifier = Modifier.size(32.dp))
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text("Searching for LED strips...", style = MaterialTheme.typography.bodySmall)
Text(stringResource(R.string.status_searching), style = MaterialTheme.typography.bodySmall)
} }


Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
) { ) {
Icon(if (isScanning) Icons.Default.Stop else Icons.Default.Search, contentDescription = null) Icon(if (isScanning) Icons.Default.Stop else Icons.Default.Search, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text(if (isScanning) "Stop Scanning" else "Scan for Devices")
Text(if (isScanning) stringResource(R.string.btn_stop_scan) else stringResource(R.string.btn_scan))
} }


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


Card(modifier = Modifier.fillMaxWidth()) { Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) { Column(modifier = Modifier.padding(16.dp)) {
Text("Add Manual Device", style = MaterialTheme.typography.titleMedium)
Text(stringResource(R.string.label_add_manual), style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField( OutlinedTextField(
value = nameText, value = nameText,
onValueChange = { nameText = it }, onValueChange = { nameText = it },
label = { Text("Name") },
label = { Text(stringResource(R.string.label_name)) },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
OutlinedTextField( OutlinedTextField(
value = ipText, value = ipText,
onValueChange = { ipText = it }, onValueChange = { ipText = it },
label = { Text("IP Address") },
placeholder = { Text("e.g. 192.168.1.100 (Port 81 is default)") },
label = { Text(stringResource(R.string.label_ip_address)) },
placeholder = { Text(stringResource(R.string.placeholder_ip)) },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
}, },
modifier = Modifier.align(Alignment.End) modifier = Modifier.align(Alignment.End)
) { ) {
Text("Add")
Text(stringResource(R.string.btn_add))
} }
} }
} }
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))


if (discoveredDevices.isNotEmpty()) { if (discoveredDevices.isNotEmpty()) {
Text("Discovered Devices", style = MaterialTheme.typography.titleMedium)
Text(stringResource(R.string.title_discovered), style = MaterialTheme.typography.titleMedium)
discoveredDevices.forEach { device -> discoveredDevices.forEach { device ->
var editedName by remember(device.id) { mutableStateOf(device.name) } var editedName by remember(device.id) { mutableStateOf(device.name) }
Card( Card(
OutlinedTextField( OutlinedTextField(
value = editedName, value = editedName,
onValueChange = { editedName = it }, onValueChange = { editedName = it },
label = { Text("Device Name") },
label = { Text(stringResource(R.string.label_device_name)) },
singleLine = true, singleLine = true,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
contentColor = MaterialTheme.colorScheme.onPrimaryContainer contentColor = MaterialTheme.colorScheme.onPrimaryContainer
) )
) { ) {
Icon(Icons.Default.Add, contentDescription = "Add")
Icon(Icons.Default.Add, contentDescription = stringResource(R.string.btn_add))
} }
} }
} }
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
} }


Text("Saved Devices", style = MaterialTheme.typography.titleMedium)
Text(stringResource(R.string.title_saved), style = MaterialTheme.typography.titleMedium)
savedDevices.forEach { device -> savedDevices.forEach { device ->
ListItem( ListItem(
headlineContent = { Text(device.name) }, headlineContent = { Text(device.name) },
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Text("Style", style = MaterialTheme.typography.headlineMedium)
Text(stringResource(R.string.tab_style), style = MaterialTheme.typography.headlineMedium)
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))


Text("Theme Mode", style = MaterialTheme.typography.titleMedium)
Text(stringResource(R.string.label_theme_mode), style = MaterialTheme.typography.titleMedium)
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
listOf("System", "Light", "Dark").forEachIndexed { index, label ->
val themeLabels = listOf(
stringResource(R.string.theme_system),
stringResource(R.string.theme_light),
stringResource(R.string.theme_dark)
)
themeLabels.forEachIndexed { index, label ->
FilterChip( FilterChip(
selected = themeMode == index, selected = themeMode == index,
onClick = { onThemeChange(index) }, onClick = { onThemeChange(index) },
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
Column { Column {
Text("Dynamic Color", style = MaterialTheme.typography.titleMedium)
Text("Use wallpaper colors (Android 12+)", style = MaterialTheme.typography.bodySmall)
Text(stringResource(R.string.label_dynamic_color), style = MaterialTheme.typography.titleMedium)
Text(stringResource(R.string.desc_dynamic_color), style = MaterialTheme.typography.bodySmall)
} }
Switch( Switch(
checked = dynamicColor, checked = dynamicColor,
discoveredDevices = listOf( discoveredDevices = listOf(
LedDevice("3", "Kitchen", "192.168.1.103") LedDevice("3", "Kitchen", "192.168.1.103")
), ),
scanStatus = "Scanning...",
scanStatus = LedDiscoveryManager.ScanStatus.Scanning,
isScanning = true, isScanning = true,
onAddDevice = { _, _ -> }, onAddDevice = { _, _ -> },
onRemoveDevice = { _ -> }, onRemoveDevice = { _ -> },

+ 1
- 1
app/src/main/java/ch/spherIC/ledlampcontrol/ui/viewmodel/LedViewModel.kt Просмотреть файл

} }
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())


val scanStatus: StateFlow<String> = discoveryManager.scanStatus
val scanStatus: StateFlow<LedDiscoveryManager.ScanStatus> = discoveryManager.scanStatus
val isScanning: StateFlow<Boolean> = discoveryManager.isScanning val isScanning: StateFlow<Boolean> = discoveryManager.isScanning


val themeMode: StateFlow<Int> = settingsRepository.themeModeFlow val themeMode: StateFlow<Int> = settingsRepository.themeModeFlow

+ 89
- 0
app/src/main/res/values-de/strings.xml Просмотреть файл

<resources>
<string name="app_name">LEDLamp Control</string>

<!-- Navigation -->
<string name="tab_adjust">Anpassen</string>
<string name="tab_modes">Modi</string>
<string name="tab_timer">Timer</string>
<string name="tab_devices">Geräte</string>
<string name="tab_style">Stil</string>

<!-- Drawer -->
<string name="drawer_title">Gerät auswählen</string>
<string name="drawer_manage_devices">Geräte verwalten</string>
<string name="status_connected">Verbunden</string>
<string name="status_connecting">Verbinde…</string>
<string name="status_disconnected">Getrennt</string>
<string name="status_failed">Verbindung fehlgeschlagen</string>
<string name="status_inactive">Inaktiv</string>

<!-- Adjust Pane -->
<string name="label_power">Ein-/Ausschalten</string>
<string name="label_brightness">Helligkeit: %1$d</string>
<string name="label_white_channel">Weiß-Kanal: %1$d</string>
<string name="label_preset">Voreinstellung</string>
<string name="label_classic">Klassisch</string>

<!-- Modes Pane -->
<string name="title_modes">Modi</string>
<string name="label_speed">Animationsgeschwindigkeit</string>
<string name="param_rainbow_delay">Verzögerung: %1$dms</string>
<string name="param_spectral_step">Spektralschritt-Zeit: %1$dms</string>
<string name="param_fading_hold">Haltezeit: %1$dms</string>
<string name="param_fading_fade">Überblendzeit: %1$dms</string>
<string name="param_flash_on">An-Zeit: %1$dms</string>
<string name="param_flash_off">Aus-Zeit: %1$dms</string>
<string name="label_segment">Segment %1$d</string>
<string name="btn_change_color">Farbe ändern</string>

<!-- Timer Pane -->
<string name="title_timer">Timer-Einstellungen</string>
<string name="label_enable_timer">Timer aktivieren</string>
<string name="label_interval_1">Intervall 1</string>
<string name="label_interval_2">Intervall 2</string>
<string name="label_time_range">Von: %1$s Bis: %2$s</string>
<string name="btn_edit_range">Bereich bearbeiten</string>
<string name="dialog_timer_title">Zeitbereich für %1$s festlegen</string>
<string name="label_start_time">Startzeit</string>
<string name="label_end_time">Endzeit</string>
<string name="btn_confirm">Bestätigen</string>
<string name="btn_cancel">Abbrechen</string>

<!-- Devices Pane -->
<string name="title_devices">Geräte verwalten</string>
<string name="status_scanner">Scanner-Status: %1$s</string>
<string name="status_searching">Suche nach LED-Strips…</string>
<string name="btn_scan">Nach Geräten suchen</string>
<string name="btn_stop_scan">Suche stoppen</string>
<string name="label_add_manual">Manuell hinzufügen</string>
<string name="label_name">Name</string>
<string name="label_ip_address">IP-Adresse</string>
<string name="placeholder_ip">z.B. 192.168.1.100 (Port 81 ist Standard)</string>
<string name="btn_add">Hinzufügen</string>
<string name="title_discovered">Entdeckte Geräte</string>
<string name="title_saved">Gespeicherte Geräte</string>
<string name="label_device_name">Gerätename</string>

<!-- Style Pane -->
<string name="title_style">Stil</string>
<string name="label_theme_mode">Design-Modus</string>
<string name="theme_system">System</string>
<string name="theme_light">Hell</string>
<string name="theme_dark">Dunkel</string>
<string name="label_dynamic_color">Dynamische Farbe</string>
<string name="desc_dynamic_color">Hintergrundfarben verwenden (Android 12+)</string>

<!-- Scan Statuses -->
<string name="scan_idle">Bereit</string>
<string name="scan_scanning">Suche läuft…</string>
<string name="scan_found">Gefunden: %1$s</string>
<string name="scan_error">Fehler bei Suche</string>
<string name="scan_failed">Start fehlgeschlagen</string>
<!-- Modes -->
<string name="mode_single_color">Einfarbiger Modus</string>
<string name="mode_rainbow">Regenbogen</string>
<string name="mode_spectral">Spektral</string>
<string name="mode_fading">Überblenden</string>
<string name="mode_flash">Blinken</string>
<string name="mode_segments">Segmente</string>
</resources>

+ 89
- 0
app/src/main/res/values-it/strings.xml Просмотреть файл

<resources>
<string name="app_name">LEDLamp Control</string>

<!-- Navigation -->
<string name="tab_adjust">Regola</string>
<string name="tab_modes">Modalità</string>
<string name="tab_timer">Timer</string>
<string name="tab_devices">Dispositivi</string>
<string name="tab_style">Stile</string>

<!-- Drawer -->
<string name="drawer_title">Seleziona Dispositivo</string>
<string name="drawer_manage_devices">Gestisci Dispositivi</string>
<string name="status_connected">Connesso</string>
<string name="status_connecting">Connessione in corso…</string>
<string name="status_disconnected">Disconnesso</string>
<string name="status_failed">Connessione fallita</string>
<string name="status_inactive">Inattivo</string>

<!-- Adjust Pane -->
<string name="label_power">Alimentazione</string>
<string name="label_brightness">Luminosità: %1$d</string>
<string name="label_white_channel">Canale Bianco: %1$d</string>
<string name="label_preset">Preimpostato</string>
<string name="label_classic">Classico</string>

<!-- Modes Pane -->
<string name="title_modes">Modalità</string>
<string name="label_speed">Velocità animazione</string>
<string name="param_rainbow_delay">Ritardo: %1$dms</string>
<string name="param_spectral_step">Tempo Passo Spettrale: %1$dms</string>
<string name="param_fading_hold">Tempo Tenuta: %1$dms</string>
<string name="param_fading_fade">Tempo Dissolvenza: %1$dms</string>
<string name="param_flash_on">Tempo Acceso: %1$dms</string>
<string name="param_flash_off">Tempo Spento: %1$dms</string>
<string name="label_segment">Segmento %1$d</string>
<string name="btn_change_color">Cambia Colore</string>

<!-- Timer Pane -->
<string name="title_timer">Impostazioni Timer</string>
<string name="label_enable_timer">Abilita Timer</string>
<string name="label_interval_1">Intervallo 1</string>
<string name="label_interval_2">Intervallo 2</string>
<string name="label_time_range">Da: %1$s A: %2$s</string>
<string name="btn_edit_range">Modifica Intervallo</string>
<string name="dialog_timer_title">Imposta Intervallo di Tempo per %1$s</string>
<string name="label_start_time">Ora di Inizio</string>
<string name="label_end_time">Ora di Fine</string>
<string name="btn_confirm">Conferma</string>
<string name="btn_cancel">Annulla</string>

<!-- Devices Pane -->
<string name="title_devices">Gestisci Dispositivi</string>
<string name="status_scanner">Stato Scanner: %1$s</string>
<string name="status_searching">Ricerca strisce LED in corso…</string>
<string name="btn_scan">Cerca Dispositivi</string>
<string name="btn_stop_scan">Ferma Ricerca</string>
<string name="label_add_manual">Aggiungi Dispositivo Manualmente</string>
<string name="label_name">Nome</string>
<string name="label_ip_address">Indirizzo IP</string>
<string name="placeholder_ip">es. 192.168.1.100 (Porta 81 predefinita)</string>
<string name="btn_add">Aggiungi</string>
<string name="title_discovered">Dispositivi Trovati</string>
<string name="title_saved">Dispositivi Salvati</string>
<string name="label_device_name">Nome Dispositivo</string>

<!-- Style Pane -->
<string name="title_style">Stile</string>
<string name="label_theme_mode">Modalità Tema</string>
<string name="theme_system">Sistema</string>
<string name="theme_light">Chiaro</string>
<string name="theme_dark">Scuro</string>
<string name="label_dynamic_color">Colore Dinamico</string>
<string name="desc_dynamic_color">Usa i colori dello sfondo (Android 12+)</string>

<!-- Scan Statuses -->
<string name="scan_idle">Inattivo</string>
<string name="scan_scanning">Ricerca in corso…</string>
<string name="scan_found">Trovato: %1$s</string>
<string name="scan_error">Errore di ricerca</string>
<string name="scan_failed">Avvio fallito</string>
<!-- Modes -->
<string name="mode_single_color">Colore singolo</string>
<string name="mode_rainbow">Arcobaleno</string>
<string name="mode_spectral">Spettrale</string>
<string name="mode_fading">Dissolvenza</string>
<string name="mode_flash">Lampeggio</string>
<string name="mode_segments">Segmenti</string>
</resources>

+ 87
- 1
app/src/main/res/values/strings.xml Просмотреть файл

<resources> <resources>
<string name="app_name">LEDLampControl</string>
<string name="app_name">LEDLamp Control</string>

<!-- Navigation -->
<string name="tab_adjust">Adjust</string>
<string name="tab_modes">Modes</string>
<string name="tab_timer">Timer</string>
<string name="tab_devices">Devices</string>
<string name="tab_style">Style</string>

<!-- Drawer -->
<string name="drawer_title">Select Device</string>
<string name="drawer_manage_devices">Manage Devices</string>
<string name="status_connected">Connected</string>
<string name="status_connecting">Connecting…</string>
<string name="status_disconnected">Disconnected</string>
<string name="status_failed">Connection failed</string>
<string name="status_inactive">Inactive</string>

<!-- Adjust Pane -->
<string name="label_power">Power</string>
<string name="label_brightness">Brightness: %1$d</string>
<string name="label_white_channel">White Channel: %1$d</string>
<string name="label_preset">Preset</string>
<string name="label_classic">Classic</string>

<!-- Modes Pane -->
<string name="title_modes">Modes</string>
<string name="label_speed">Animation Speed</string>
<string name="param_rainbow_delay">Rainbow Delay: %1$dms</string>
<string name="param_spectral_step">Spectral Step Time: %1$dms</string>
<string name="param_fading_hold">Hold Time: %1$dms</string>
<string name="param_fading_fade">Fade Time: %1$dms</string>
<string name="param_flash_on">On Time: %1$dms</string>
<string name="param_flash_off">Off Time: %1$dms</string>
<string name="label_segment">Segment %1$d</string>
<string name="btn_change_color">Change Color</string>

<!-- Timer Pane -->
<string name="title_timer">Timer Settings</string>
<string name="label_enable_timer">Enable Timer</string>
<string name="label_interval_1">Interval 1</string>
<string name="label_interval_2">Interval 2</string>
<string name="label_time_range">From: %1$s To: %2$s</string>
<string name="btn_edit_range">Edit Range</string>
<string name="dialog_timer_title">Set Time Range for %1$s</string>
<string name="label_start_time">Start Time</string>
<string name="label_end_time">End Time</string>
<string name="btn_confirm">Confirm</string>
<string name="btn_cancel">Cancel</string>

<!-- Devices Pane -->
<string name="title_devices">Manage Devices</string>
<string name="status_scanner">Scanner Status: %1$s</string>
<string name="status_searching">Searching for LED strips…</string>
<string name="btn_scan">Scan for Devices</string>
<string name="btn_stop_scan">Stop Scanning</string>
<string name="label_add_manual">Add Manual Device</string>
<string name="label_name">Name</string>
<string name="label_ip_address">IP Address</string>
<string name="placeholder_ip">e.g. 192.168.1.100 (Port 81 is default)</string>
<string name="btn_add">Add</string>
<string name="title_discovered">Discovered Devices</string>
<string name="title_saved">Saved Devices</string>
<string name="label_device_name">Device Name</string>

<!-- Style Pane -->
<string name="title_style">Style</string>
<string name="label_theme_mode">Theme Mode</string>
<string name="theme_system">System</string>
<string name="theme_light">Light</string>
<string name="theme_dark">Dark</string>
<string name="label_dynamic_color">Dynamic Color</string>
<string name="desc_dynamic_color">Use wallpaper colors (Android 12+)</string>

<!-- Scan Statuses -->
<string name="scan_idle">Idle</string>
<string name="scan_scanning">Scanning…</string>
<string name="scan_found">Found: %1$s</string>
<string name="scan_error">Discovery error</string>
<string name="scan_failed">Failed to start</string>
<!-- Modes -->
<string name="mode_single_color">Single color</string>
<string name="mode_rainbow">Rainbow</string>
<string name="mode_spectral">Spectral</string>
<string name="mode_fading">Fading</string>
<string name="mode_flash">Flash</string>
<string name="mode_segments">Segments</string>
</resources> </resources>

Загрузка…
Отмена
Сохранить