|
|
|
|
|
|
|
|
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 = { _ -> }, |