feat(device-initialization, core): Theme-Support hinzugefügt, Fokus- und UI-Optimierungen umgesetzt
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
+35
-21
@@ -7,8 +7,11 @@ import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.input.key.*
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Ein generischer Dropdown zur Auswahl von Enum-Werten.
|
||||
@@ -55,35 +58,46 @@ fun <T : Enum<T>> MsEnumDropdown(
|
||||
) {
|
||||
Text(label, style = MaterialTheme.typography.bodySmall)
|
||||
if (helpDescription != null) {
|
||||
var showHelp by remember { mutableStateOf(false) }
|
||||
Box {
|
||||
val tooltipState = rememberTooltipState(isPersistent = true)
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(
|
||||
TooltipAnchorPosition.Above
|
||||
),
|
||||
tooltip = {
|
||||
PlainTooltip(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
shape = MaterialTheme.shapes.small
|
||||
) {
|
||||
Text(
|
||||
text = helpDescription,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(Dimens.SpacingS)
|
||||
)
|
||||
}
|
||||
},
|
||||
state = tooltipState
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { showHelp = !showHelp },
|
||||
modifier = Modifier.size(16.dp)
|
||||
onClick = {
|
||||
scope.launch {
|
||||
if (tooltipState.isVisible) tooltipState.dismiss()
|
||||
else tooltipState.show()
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.focusProperties { canFocus = false }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.HelpOutline,
|
||||
contentDescription = "Hilfe",
|
||||
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f),
|
||||
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f),
|
||||
modifier = Modifier.size(14.dp)
|
||||
)
|
||||
}
|
||||
if (showHelp) {
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(
|
||||
TooltipAnchorPosition.Above
|
||||
),
|
||||
tooltip = {
|
||||
PlainTooltip {
|
||||
Text(helpDescription)
|
||||
}
|
||||
},
|
||||
state = rememberTooltipState(isPersistent = true)
|
||||
) {
|
||||
// Tooltip wird durch Klick auf das Icon getriggert
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+35
-21
@@ -7,8 +7,11 @@ import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.input.key.*
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Ein generischer Dropdown zur Auswahl von Strings (z. B. Druckernamen).
|
||||
@@ -45,35 +48,46 @@ fun MsStringDropdown(
|
||||
) {
|
||||
Text(label, style = MaterialTheme.typography.bodySmall)
|
||||
if (helpDescription != null) {
|
||||
var showHelp by remember { mutableStateOf(false) }
|
||||
Box {
|
||||
val tooltipState = rememberTooltipState(isPersistent = true)
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(
|
||||
TooltipAnchorPosition.Above
|
||||
),
|
||||
tooltip = {
|
||||
PlainTooltip(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
shape = MaterialTheme.shapes.small
|
||||
) {
|
||||
Text(
|
||||
text = helpDescription,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(Dimens.SpacingS)
|
||||
)
|
||||
}
|
||||
},
|
||||
state = tooltipState
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { showHelp = !showHelp },
|
||||
modifier = Modifier.size(16.dp)
|
||||
onClick = {
|
||||
scope.launch {
|
||||
if (tooltipState.isVisible) tooltipState.dismiss()
|
||||
else tooltipState.show()
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.focusProperties { canFocus = false }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.HelpOutline,
|
||||
contentDescription = "Hilfe",
|
||||
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f),
|
||||
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f),
|
||||
modifier = Modifier.size(14.dp)
|
||||
)
|
||||
}
|
||||
if (showHelp) {
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(
|
||||
TooltipAnchorPosition.Above
|
||||
),
|
||||
tooltip = {
|
||||
PlainTooltip {
|
||||
Text(helpDescription)
|
||||
}
|
||||
},
|
||||
state = rememberTooltipState(isPersistent = true)
|
||||
) {
|
||||
// Tooltip wird durch Klick auf das Icon getriggert
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+39
-22
@@ -9,6 +9,7 @@ import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
@@ -16,6 +17,7 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun MsTextField(
|
||||
@@ -56,35 +58,48 @@ fun MsTextField(
|
||||
color = if (isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
if (helpDescription != null) {
|
||||
var showHelp by remember { mutableStateOf(false) }
|
||||
Box {
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
val tooltipState = rememberTooltipState(isPersistent = true)
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(
|
||||
TooltipAnchorPosition.Above
|
||||
),
|
||||
tooltip = {
|
||||
PlainTooltip(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
shape = MaterialTheme.shapes.small
|
||||
) {
|
||||
Text(
|
||||
text = helpDescription,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(Dimens.SpacingS)
|
||||
)
|
||||
}
|
||||
},
|
||||
state = tooltipState
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { showHelp = !showHelp },
|
||||
modifier = Modifier.size(16.dp)
|
||||
onClick = {
|
||||
scope.launch {
|
||||
if (tooltipState.isVisible) tooltipState.dismiss()
|
||||
else tooltipState.show()
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.focusProperties { canFocus = false }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.HelpOutline,
|
||||
contentDescription = "Hilfe",
|
||||
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f),
|
||||
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f),
|
||||
modifier = Modifier.size(14.dp)
|
||||
)
|
||||
}
|
||||
if (showHelp) {
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(
|
||||
TooltipAnchorPosition.Above
|
||||
),
|
||||
tooltip = {
|
||||
PlainTooltip {
|
||||
Text(helpDescription)
|
||||
}
|
||||
},
|
||||
state = rememberTooltipState(isPersistent = true)
|
||||
) {
|
||||
// Tooltip wird durch Klick auf das Icon getriggert
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,7 +136,9 @@ fun MsTextField(
|
||||
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
|
||||
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
||||
unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f),
|
||||
unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f),
|
||||
focusedLabelColor = MaterialTheme.colorScheme.primary,
|
||||
unfocusedLabelColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = keyboardType,
|
||||
|
||||
+4
-2
@@ -1,5 +1,6 @@
|
||||
package at.mocode.frontend.core.designsystem.theme
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -38,7 +39,8 @@ private val DarkColorScheme = darkColorScheme(
|
||||
background = AppColors.BackgroundDark,
|
||||
surface = AppColors.SurfaceDark,
|
||||
onBackground = AppColors.OnBackgroundDark,
|
||||
onSurface = AppColors.OnBackgroundDark,
|
||||
onSurface = AppColors.OnSurfaceDark,
|
||||
outline = AppColors.OutlineDark,
|
||||
|
||||
error = AppColors.Error,
|
||||
onError = AppColors.OnError
|
||||
@@ -63,7 +65,7 @@ private val AppMaterialTypography = Typography(
|
||||
|
||||
@Composable
|
||||
fun AppTheme(
|
||||
darkTheme: Boolean = false, // Kann später via Settings gesteuert werden
|
||||
darkTheme: Boolean = isSystemInDarkTheme(), // Nutzt Systemeinstellung als Default
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
|
||||
|
||||
+5
-3
@@ -28,9 +28,11 @@ object AppColors {
|
||||
val OnBackgroundLight = Color(0xFF172B4D) // Fast Schwarz (besser lesbar)
|
||||
|
||||
// Neutral & Hintergrund (Dark Mode)
|
||||
val BackgroundDark = Color(0xFF1E1E1E) // Angenehmes, dunkles Grau
|
||||
val SurfaceDark = Color(0xFF2C2C2C)
|
||||
val OnBackgroundDark = Color(0xFFEBECF0)
|
||||
val BackgroundDark = Color(0xFF121212) // Tieferes Schwarz für Dark Mode
|
||||
val SurfaceDark = Color(0xFF1E1E1E)
|
||||
val OnBackgroundDark = Color(0xFFE1E1E1)
|
||||
val OnSurfaceDark = Color(0xFFE1E1E1)
|
||||
val OutlineDark = Color(0xFF333333)
|
||||
|
||||
// System Status
|
||||
val Error = Color(0xFFDE350B)
|
||||
|
||||
+3
-3
@@ -34,7 +34,7 @@ object Dimens {
|
||||
val CornerRadiusL = 12.dp
|
||||
|
||||
// Form-Elemente (Eingabefelder, Buttons)
|
||||
val TextFieldHeight = 44.dp // Kompakte Höhe für Desktop-Enterprise-Apps
|
||||
val TextFieldHeightL = 56.dp // Standard Material Höhe (für prominente Felder)
|
||||
val ButtonHeight = 40.dp
|
||||
val TextFieldHeight = 40.dp // Kompakte Höhe für Desktop-Enterprise-Apps
|
||||
val TextFieldHeightL = 48.dp // Etwas weniger als Standard Material (56.dp)
|
||||
val ButtonHeight = 36.dp // Kompakterer Button
|
||||
}
|
||||
|
||||
+1
-4
@@ -2,12 +2,9 @@
|
||||
|
||||
package at.mocode.frontend.features.device.initialization.di
|
||||
|
||||
import at.mocode.frontend.features.device.initialization.domain.model.DeviceInitializationSettings
|
||||
import at.mocode.frontend.features.device.initialization.presentation.DeviceInitializationViewModel
|
||||
import org.koin.dsl.module
|
||||
|
||||
val deviceInitializationModule = module {
|
||||
factory { (onComplete: (DeviceInitializationSettings) -> Unit) ->
|
||||
DeviceInitializationViewModel(get(), onComplete)
|
||||
}
|
||||
factory { DeviceInitializationViewModel(get()) }
|
||||
}
|
||||
|
||||
+9
-1
@@ -24,6 +24,13 @@ data class ExpectedClient(
|
||||
val isSynchronized: Boolean = true
|
||||
)
|
||||
|
||||
@Serializable
|
||||
enum class AppThemeSetting {
|
||||
SYSTEM,
|
||||
LIGHT,
|
||||
DARK
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class DeviceInitializationSettings(
|
||||
val deviceName: String = "",
|
||||
@@ -33,7 +40,8 @@ data class DeviceInitializationSettings(
|
||||
val networkRole: NetworkRole = NetworkRole.CLIENT,
|
||||
val expectedClients: List<ExpectedClient> = emptyList(),
|
||||
val syncInterval: Int = 30, // in Minuten
|
||||
val defaultPrinter: String = ""
|
||||
val defaultPrinter: String = "",
|
||||
val appTheme: AppThemeSetting = AppThemeSetting.SYSTEM
|
||||
) {
|
||||
val isConfigured: Boolean get() = deviceName.isNotBlank() && sharedKey.isNotBlank()
|
||||
}
|
||||
|
||||
+156
-108
@@ -8,8 +8,6 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.NetworkCheck
|
||||
@@ -21,39 +19,43 @@ import androidx.compose.ui.focus.FocusDirection
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component1
|
||||
import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component2
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.input.key.*
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.frontend.features.device.initialization.domain.DeviceInitializationValidator
|
||||
|
||||
@Composable
|
||||
private fun DiscoveryRadar() {
|
||||
val infiniteTransition = rememberInfiniteTransition()
|
||||
private fun DiscoveryRadar(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val infiniteTransition = rememberInfiniteTransition(label = "RadarTransition")
|
||||
val radius by infiniteTransition.animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = 40f,
|
||||
targetValue = 20f, // Kleinerer Radius
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(2000, easing = LinearEasing),
|
||||
animation = tween(2500, easing = LinearOutSlowInEasing), // Langsamer und sanfter
|
||||
repeatMode = RepeatMode.Restart
|
||||
)
|
||||
),
|
||||
label = "RadiusAnimation"
|
||||
)
|
||||
val alpha by infiniteTransition.animateFloat(
|
||||
initialValue = 1f,
|
||||
initialValue = 0.4f, // Noch dezenter
|
||||
targetValue = 0f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(2000, easing = LinearEasing),
|
||||
animation = tween(3000, easing = LinearOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Restart
|
||||
)
|
||||
),
|
||||
label = "AlphaAnimation"
|
||||
)
|
||||
|
||||
val color = MaterialTheme.colorScheme.primary
|
||||
val color = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f) // Dezente Farbe
|
||||
|
||||
Box(
|
||||
modifier = Modifier.size(80.dp),
|
||||
modifier = modifier.size(32.dp), // Noch kleiner
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
@@ -61,22 +63,15 @@ private fun DiscoveryRadar() {
|
||||
color = color,
|
||||
radius = radius.dp.toPx(),
|
||||
center = Offset(size.width / 2, size.height / 2),
|
||||
style = Stroke(width = 2.dp.toPx()),
|
||||
alpha = alpha
|
||||
)
|
||||
drawCircle(
|
||||
color = color,
|
||||
radius = (radius * 0.5f).dp.toPx(),
|
||||
center = Offset(size.width / 2, size.height / 2),
|
||||
style = Stroke(width = 1.dp.toPx()),
|
||||
alpha = alpha * 0.5f
|
||||
alpha = alpha
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
imageVector = Icons.Default.NetworkCheck,
|
||||
contentDescription = null,
|
||||
tint = color,
|
||||
modifier = Modifier.size(24.dp)
|
||||
tint = color.copy(alpha = 0.8f),
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -87,61 +82,142 @@ fun DeviceInitializationScreen(
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
val focusManager = LocalFocusManager.current
|
||||
val (roleSelectorFocus, nextButtonFocus) = remember { FocusRequester.createRefs() }
|
||||
val (roleSelectorFocus, deviceNameFocus) = remember { FocusRequester.createRefs() }
|
||||
|
||||
// Automatische Discovery starten, wenn wir auf Schritt 0 sind
|
||||
LaunchedEffect(uiState.currentStep) {
|
||||
if (uiState.currentStep == 0) {
|
||||
viewModel.startDiscovery()
|
||||
roleSelectorFocus.requestFocus()
|
||||
}
|
||||
// Automatische Discovery starten
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.startDiscovery()
|
||||
roleSelectorFocus.requestFocus()
|
||||
}
|
||||
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(24.dp).verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(
|
||||
"Willkommen bei der Meldestelle",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
if (uiState.currentStep == 0) "Schritt 1: Netzwerk-Rolle festlegen" else "Schritt 2: Rollenspezifische Konfiguration",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.background,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
BoxWithConstraints(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) {
|
||||
val isMobile = maxWidth < 600.dp
|
||||
val contentWidth = if (isMobile) 425.dp else 1024.dp
|
||||
|
||||
if (uiState.currentStep == 0) {
|
||||
// PHASE 1: NETZWERK-ROLLE
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text("🌐 Netzwerk-Rolle wählen", style = MaterialTheme.typography.titleMedium)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.widthIn(max = contentWidth)
|
||||
.fillMaxWidth()
|
||||
.padding(if (isMobile) 16.dp else 32.dp)
|
||||
.verticalScroll(scrollState),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text(
|
||||
"Wähle aus, ob dieses Gerät als Master (zentrale Datenbank) oder als Client fungiert.",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
"Willkommen bei der Meldestelle",
|
||||
style = if (isMobile) MaterialTheme.typography.headlineSmall else MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
"Geräte-Initialisierung",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
// THEME SWITCH
|
||||
Card(
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)),
|
||||
modifier = Modifier.focusProperties { canFocus = false }
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
at.mocode.frontend.features.device.initialization.domain.model.AppThemeSetting.entries.forEach { theme ->
|
||||
val selected = uiState.settings.appTheme == theme
|
||||
FilterChip(
|
||||
selected = selected,
|
||||
onClick = { viewModel.updateSettings { it.copy(appTheme = theme) } },
|
||||
label = {
|
||||
Text(
|
||||
when(theme) {
|
||||
at.mocode.frontend.features.device.initialization.domain.model.AppThemeSetting.SYSTEM -> "System"
|
||||
at.mocode.frontend.features.device.initialization.domain.model.AppThemeSetting.LIGHT -> "Hell"
|
||||
at.mocode.frontend.features.device.initialization.domain.model.AppThemeSetting.DARK -> "Dunkel"
|
||||
},
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
},
|
||||
colors = FilterChipDefaults.filterChipColors(
|
||||
selectedContainerColor = MaterialTheme.colorScheme.primary,
|
||||
selectedLabelColor = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NETZWERK-ROLLE
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = MaterialTheme.shapes.large,
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
|
||||
) {
|
||||
Column(Modifier.padding(if (isMobile) 16.dp else 24.dp), verticalArrangement = Arrangement.spacedBy(20.dp)) {
|
||||
Text("🌐 Netzwerk-Rolle wählen", style = MaterialTheme.typography.titleLarge)
|
||||
Text(
|
||||
"Möchtest du dieses Gerät als Master (Zentrale) oder als Client (Richter/Zeitnehmer) nutzen?",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
DiscoveryRadar()
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Text(
|
||||
"Suche nach Geräten im Netzwerk...",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
if (!uiState.isLocked) {
|
||||
val role = uiState.settings.networkRole
|
||||
val hasDiscoveries = uiState.discoveredMasters.isNotEmpty()
|
||||
|
||||
Surface(
|
||||
color = if (hasDiscoveries) MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.15f)
|
||||
else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.1f),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
border = if (hasDiscoveries) androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.primary.copy(alpha = 0.2f))
|
||||
else null
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
DiscoveryRadar()
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = when {
|
||||
role == at.mocode.frontend.features.device.initialization.domain.model.NetworkRole.MASTER && hasDiscoveries -> "Aktive Clients im Netzwerk gefunden"
|
||||
role == at.mocode.frontend.features.device.initialization.domain.model.NetworkRole.MASTER -> "Suche nach verfügbaren Clients..."
|
||||
hasDiscoveries -> "Master im Netzwerk gefunden"
|
||||
else -> "Suche nach Master-Geräten..."
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = if (hasDiscoveries) MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.7f)
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NetworkRoleSelector(
|
||||
selectedRole = uiState.settings.networkRole,
|
||||
onRoleSelected = {
|
||||
viewModel.setNetworkRole(it)
|
||||
focusManager.moveFocus(FocusDirection.Next)
|
||||
if (uiState.settings.deviceName.isEmpty()) {
|
||||
deviceNameFocus.requestFocus()
|
||||
} else {
|
||||
focusManager.moveFocus(FocusDirection.Next)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.focusRequester(roleSelectorFocus),
|
||||
enabled = !uiState.isLocked
|
||||
@@ -151,7 +227,7 @@ fun DeviceInitializationScreen(
|
||||
AlertDialog(
|
||||
onDismissRequest = { viewModel.dismissRoleChangeWarning() },
|
||||
title = { Text("Netzwerk-Rolle ändern?") },
|
||||
text = { Text("Das Ändern der Netzwerk-Rolle kann Ihre bisherigen Eingaben in Schritt 2 beeinflussen. Wollen Sie fortfahren?") },
|
||||
text = { Text("Das Ändern der Netzwerk-Rolle kann Ihre bisherigen Eingaben beeinflussen. Wollen Sie fortfahren?") },
|
||||
confirmButton = {
|
||||
Button(onClick = { viewModel.confirmNetworkRoleChange() }) { Text("Ja, Ändern") }
|
||||
},
|
||||
@@ -160,52 +236,21 @@ fun DeviceInitializationScreen(
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (!uiState.isLocked) {
|
||||
Button(
|
||||
onClick = { viewModel.nextStep() },
|
||||
modifier = Modifier
|
||||
.align(Alignment.End)
|
||||
.focusRequester(nextButtonFocus)
|
||||
.onKeyEvent {
|
||||
if ((it.key == Key.Enter || it.key == Key.Spacebar) && it.type == KeyEventType.KeyUp) {
|
||||
viewModel.nextStep()
|
||||
true
|
||||
} else false
|
||||
}
|
||||
) {
|
||||
Text("Weiter")
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowForward, contentDescription = null)
|
||||
}
|
||||
} else {
|
||||
Button(
|
||||
onClick = { viewModel.nextStep() },
|
||||
modifier = Modifier.align(Alignment.End)
|
||||
) {
|
||||
Text("Zur Konfiguration")
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowForward, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// PHASE 2 & Review
|
||||
|
||||
// Konfiguration
|
||||
DeviceInitializationConfig(
|
||||
uiState = uiState,
|
||||
viewModel = viewModel
|
||||
viewModel = viewModel,
|
||||
deviceNameFocus = deviceNameFocus
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
TextButton(onClick = { viewModel.previousStep() }) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Zurück zur Rollenauswahl")
|
||||
}
|
||||
|
||||
if (uiState.isLocked) {
|
||||
var showUnlockWarning by remember { mutableStateOf(false) }
|
||||
if (showUnlockWarning) {
|
||||
@@ -229,18 +274,20 @@ fun DeviceInitializationScreen(
|
||||
onClick = { showUnlockWarning = true },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
Text("Konfiguration bearbeiten")
|
||||
Icon(Icons.Default.Edit, null, Modifier.padding(start = 8.dp))
|
||||
Icon(Icons.Default.Edit, null, Modifier.padding(start = 8.dp).size(18.dp))
|
||||
}
|
||||
} else {
|
||||
Button(
|
||||
onClick = { viewModel.completeInitialization() },
|
||||
enabled = DeviceInitializationValidator.canContinue(uiState.settings)
|
||||
enabled = DeviceInitializationValidator.canContinue(uiState.settings),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
Text("Konfiguration finalisieren & Sperren")
|
||||
Icon(Icons.Default.Check, null, Modifier.padding(start = 8.dp))
|
||||
Text("Konfiguration finalisieren")
|
||||
Icon(Icons.Default.Check, null, Modifier.padding(start = 8.dp).size(18.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -252,5 +299,6 @@ fun DeviceInitializationScreen(
|
||||
@Composable
|
||||
expect fun DeviceInitializationConfig(
|
||||
uiState: DeviceInitializationUiState,
|
||||
viewModel: DeviceInitializationViewModel
|
||||
viewModel: DeviceInitializationViewModel,
|
||||
deviceNameFocus: FocusRequester
|
||||
)
|
||||
|
||||
-1
@@ -6,7 +6,6 @@ import at.mocode.frontend.core.network.discovery.DiscoveredService
|
||||
import at.mocode.frontend.features.device.initialization.domain.model.DeviceInitializationSettings
|
||||
|
||||
data class DeviceInitializationUiState(
|
||||
val currentStep: Int = 0,
|
||||
val settings: DeviceInitializationSettings = DeviceInitializationSettings(),
|
||||
val discoveredMasters: List<DiscoveredService> = emptyList(),
|
||||
val isProcessing: Boolean = false,
|
||||
|
||||
+10
-18
@@ -9,32 +9,31 @@ import at.mocode.frontend.core.network.discovery.NetworkDiscoveryService
|
||||
import at.mocode.frontend.features.device.initialization.domain.model.DeviceInitializationSettings
|
||||
import at.mocode.frontend.features.device.initialization.domain.model.ExpectedClient
|
||||
import at.mocode.frontend.features.device.initialization.domain.model.NetworkRole
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class DeviceInitializationViewModel(
|
||||
private val discoveryService: NetworkDiscoveryService,
|
||||
private val onInitializationComplete: (DeviceInitializationSettings) -> Unit
|
||||
private val discoveryService: NetworkDiscoveryService
|
||||
) : ViewModel() {
|
||||
private val _uiState = MutableStateFlow(DeviceInitializationUiState())
|
||||
val uiState: StateFlow<DeviceInitializationUiState> = _uiState.asStateFlow()
|
||||
|
||||
private val _initializationCompleteEvent = MutableSharedFlow<DeviceInitializationSettings>()
|
||||
val initializationCompleteEvent: SharedFlow<DeviceInitializationSettings> = _initializationCompleteEvent.asSharedFlow()
|
||||
|
||||
init {
|
||||
val existingSettings = at.mocode.frontend.features.device.initialization.data.local.DeviceInitializationSettingsManager.loadSettings()
|
||||
if (existingSettings != null) {
|
||||
println("[DeviceInit] Bestehende Einstellungen geladen.")
|
||||
_uiState.update { it.copy(
|
||||
settings = existingSettings,
|
||||
isLocked = existingSettings.isConfigured,
|
||||
currentStep = 1 // Direkt zu Schritt 2 (Konfig), da Rolle schon gewählt
|
||||
isLocked = existingSettings.isConfigured
|
||||
) }
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
discoveryService.discoveredServices.collect { services ->
|
||||
println("[DeviceInit] Discovery Update: ${services.size} Dienste gefunden.")
|
||||
_uiState.update { it.copy(discoveredMasters = services) }
|
||||
}
|
||||
}
|
||||
@@ -44,15 +43,6 @@ class DeviceInitializationViewModel(
|
||||
discoveryService.startDiscovery()
|
||||
}
|
||||
|
||||
fun nextStep() {
|
||||
println("[DeviceInit] Übergang zu Schritt ${uiState.value.currentStep + 1}")
|
||||
_uiState.update { it.copy(currentStep = it.currentStep + 1) }
|
||||
}
|
||||
|
||||
fun previousStep() {
|
||||
println("[DeviceInit] Zurück zu Schritt ${(uiState.value.currentStep - 1).coerceAtLeast(0)}")
|
||||
_uiState.update { it.copy(currentStep = (it.currentStep - 1).coerceAtLeast(0)) }
|
||||
}
|
||||
|
||||
fun updateSettings(update: (DeviceInitializationSettings) -> DeviceInitializationSettings) {
|
||||
_uiState.update {
|
||||
@@ -102,7 +92,9 @@ class DeviceInitializationViewModel(
|
||||
fun completeInitialization() {
|
||||
println("[DeviceInit] Konfiguration wird finalisiert...")
|
||||
_uiState.update { it.copy(isLocked = true) }
|
||||
onInitializationComplete(_uiState.value.settings)
|
||||
viewModelScope.launch {
|
||||
_initializationCompleteEvent.emit(_uiState.value.settings)
|
||||
}
|
||||
}
|
||||
|
||||
fun unlockConfiguration() {
|
||||
|
||||
+12
-10
@@ -2,6 +2,7 @@
|
||||
|
||||
package at.mocode.frontend.features.device.initialization.presentation
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
@@ -21,14 +22,14 @@ fun NetworkRoleSelector(
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Row(modifier = modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
NetworkRoleCard(
|
||||
title = "Master (Host)",
|
||||
description = "Verwaltet die zentrale Datenbank und koordiniert den Sync.",
|
||||
description = "Zentrale Datenbank & Sync-Koordination.",
|
||||
isSelected = selectedRole == NetworkRole.MASTER,
|
||||
onClick = { if (enabled) onRoleSelected(NetworkRole.MASTER) },
|
||||
enabled = enabled,
|
||||
modifier = Modifier.onKeyEvent {
|
||||
modifier = Modifier.weight(1f).onKeyEvent {
|
||||
if (enabled && (it.key == Key.Enter || it.key == Key.Spacebar) && it.type == KeyEventType.KeyUp) {
|
||||
onRoleSelected(NetworkRole.MASTER)
|
||||
true
|
||||
@@ -38,11 +39,11 @@ fun NetworkRoleSelector(
|
||||
|
||||
NetworkRoleCard(
|
||||
title = "Client",
|
||||
description = "Verbindet sich mit einem Master-Gerät im lokalen Netzwerk.",
|
||||
description = "Verbindung zum Master im LAN.",
|
||||
isSelected = selectedRole == NetworkRole.CLIENT,
|
||||
onClick = { if (enabled) onRoleSelected(NetworkRole.CLIENT) },
|
||||
enabled = enabled,
|
||||
modifier = Modifier.onKeyEvent {
|
||||
modifier = Modifier.weight(1f).onKeyEvent {
|
||||
if (enabled && (it.key == Key.Enter || it.key == Key.Spacebar) && it.type == KeyEventType.KeyUp) {
|
||||
onRoleSelected(NetworkRole.CLIENT)
|
||||
true
|
||||
@@ -66,13 +67,14 @@ private fun NetworkRoleCard(
|
||||
enabled = enabled,
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
color = when {
|
||||
isSelected -> MaterialTheme.colorScheme.primaryContainer
|
||||
!enabled -> MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||
else -> MaterialTheme.colorScheme.surfaceVariant
|
||||
isSelected -> MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.7f)
|
||||
!enabled -> MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
|
||||
else -> MaterialTheme.colorScheme.surface
|
||||
},
|
||||
modifier = modifier.fillMaxWidth()
|
||||
border = if (isSelected) BorderStroke(2.dp, MaterialTheme.colorScheme.primary) else BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant),
|
||||
modifier = modifier
|
||||
) {
|
||||
Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(
|
||||
selected = isSelected,
|
||||
onClick = null,
|
||||
|
||||
+70
-29
@@ -40,19 +40,26 @@ import javax.print.PrintServiceLookup
|
||||
@Composable
|
||||
actual fun DeviceInitializationConfig(
|
||||
uiState: DeviceInitializationUiState,
|
||||
viewModel: DeviceInitializationViewModel
|
||||
viewModel: DeviceInitializationViewModel,
|
||||
deviceNameFocus: FocusRequester
|
||||
) {
|
||||
val settings = uiState.settings
|
||||
val focusManager = LocalFocusManager.current
|
||||
val (deviceNameFocus, sharedKeyFocus, backupPathFocus, clientNameFocus, clientRoleFocus) = remember { FocusRequester.createRefs() }
|
||||
val (_, sharedKeyFocus, backupPathFocus, clientNameFocus, clientRoleFocus) = remember { FocusRequester.createRefs() }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
deviceNameFocus.requestFocus()
|
||||
if (settings.deviceName.isEmpty()) {
|
||||
deviceNameFocus.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text("⚙️ Geräte-Konfiguration (${settings.networkRole})", style = MaterialTheme.typography.titleMedium)
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = MaterialTheme.shapes.large,
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
|
||||
) {
|
||||
Column(Modifier.padding(24.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text("⚙️ Geräte-Details", style = MaterialTheme.typography.titleLarge)
|
||||
|
||||
MsTextField(
|
||||
value = settings.deviceName,
|
||||
@@ -65,13 +72,30 @@ actual fun DeviceInitializationConfig(
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) }),
|
||||
modifier = Modifier.focusRequester(deviceNameFocus),
|
||||
enabled = !uiState.isLocked
|
||||
enabled = !uiState.isLocked,
|
||||
compact = true
|
||||
)
|
||||
|
||||
val interfaces = remember {
|
||||
NetworkInterface.getNetworkInterfaces().toList()
|
||||
.filter { it.isUp && !it.isLoopback && it.inetAddresses.hasMoreElements() }
|
||||
.map { "${it.displayName} (${it.inetAddresses.nextElement().hostAddress})" }
|
||||
.map { ni ->
|
||||
val friendlyName = when {
|
||||
ni.displayName.contains("wlan", ignoreCase = true) || ni.displayName.contains("wi-fi", ignoreCase = true) -> "WLAN"
|
||||
ni.displayName.contains("eth", ignoreCase = true) || ni.displayName.contains("ethernet", ignoreCase = true) -> "Ethernet"
|
||||
else -> ni.displayName
|
||||
}
|
||||
val address = ni.inetAddresses.asSequence()
|
||||
.filter { !it.isLinkLocalAddress && it.hostAddress.indexOf(':') == -1 } // Nur IPv4, keine Link-Local
|
||||
.firstOrNull()?.hostAddress ?: ni.inetAddresses.nextElement().hostAddress
|
||||
"$friendlyName ($address)"
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(interfaces) {
|
||||
if (settings.networkInterface.isEmpty() && interfaces.isNotEmpty()) {
|
||||
viewModel.updateSettings { s -> s.copy(networkInterface = interfaces.first()) }
|
||||
}
|
||||
}
|
||||
|
||||
MsStringDropdown(
|
||||
@@ -101,7 +125,8 @@ actual fun DeviceInitializationConfig(
|
||||
modifier = Modifier.focusRequester(sharedKeyFocus),
|
||||
trailingIcon = if (passwordVisible) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
|
||||
onTrailingIconClick = { passwordVisible = !passwordVisible },
|
||||
enabled = !uiState.isLocked
|
||||
enabled = !uiState.isLocked,
|
||||
compact = true
|
||||
)
|
||||
|
||||
MsFilePicker(
|
||||
@@ -116,9 +141,19 @@ actual fun DeviceInitializationConfig(
|
||||
enabled = !uiState.isLocked
|
||||
)
|
||||
|
||||
val printers = remember {
|
||||
PrintServiceLookup.lookupPrintServices(null, null).map { it.name }.sorted()
|
||||
}
|
||||
val printers = remember {
|
||||
val systemPrinters = PrintServiceLookup.lookupPrintServices(null, null).map { it.name }.toMutableList()
|
||||
if (!systemPrinters.contains("PDF-Export (Lokal)")) {
|
||||
systemPrinters.add(0, "PDF-Export (Lokal)")
|
||||
}
|
||||
systemPrinters.sortedBy { it != "PDF-Export (Lokal)" } // PDF immer oben
|
||||
}
|
||||
|
||||
LaunchedEffect(printers) {
|
||||
if (settings.defaultPrinter.isEmpty() && printers.isNotEmpty()) {
|
||||
viewModel.updateSettings { s -> s.copy(defaultPrinter = printers.first()) }
|
||||
}
|
||||
}
|
||||
|
||||
MsStringDropdown(
|
||||
label = "Standard-Drucker",
|
||||
@@ -132,7 +167,8 @@ actual fun DeviceInitializationConfig(
|
||||
)
|
||||
|
||||
if (settings.networkRole == NetworkRole.MASTER) {
|
||||
Text("Sync-Intervall: ${settings.syncInterval} Min.", style = MaterialTheme.typography.labelMedium)
|
||||
HorizontalDivider(Modifier.padding(vertical = 8.dp))
|
||||
Text("⏱️ Sync-Intervall: ${settings.syncInterval} Min.", style = MaterialTheme.typography.titleSmall)
|
||||
Slider(
|
||||
value = settings.syncInterval.toFloat(),
|
||||
onValueChange = { viewModel.updateSettings { s -> s.copy(syncInterval = it.toInt()) } },
|
||||
@@ -140,20 +176,10 @@ actual fun DeviceInitializationConfig(
|
||||
steps = 59,
|
||||
enabled = !uiState.isLocked
|
||||
)
|
||||
} else if (!uiState.isLocked) {
|
||||
// Button zum Abschließen für Clients, da diese keinen Slider/Clients haben
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Button(
|
||||
onClick = { viewModel.completeInitialization() },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = DeviceInitializationValidator.canContinue(settings)
|
||||
) {
|
||||
Text("Konfiguration abschließen")
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.networkRole == NetworkRole.MASTER && !uiState.isLocked) {
|
||||
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
|
||||
HorizontalDivider(Modifier.padding(vertical = 8.dp))
|
||||
Text("👥 Erwartete Clients", style = MaterialTheme.typography.titleSmall)
|
||||
|
||||
settings.expectedClients.forEachIndexed { index, client ->
|
||||
@@ -265,13 +291,28 @@ actual fun DeviceInitializationConfig(
|
||||
}
|
||||
}
|
||||
} else if (settings.networkRole != NetworkRole.MASTER && !uiState.isLocked) {
|
||||
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
|
||||
HorizontalDivider(Modifier.padding(vertical = 8.dp))
|
||||
Text("🔍 Verfügbare Master im Netzwerk", style = MaterialTheme.typography.titleSmall)
|
||||
|
||||
if (uiState.discoveredMasters.isEmpty()) {
|
||||
Box(Modifier.fillMaxWidth().padding(16.dp), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||
Text("Suche nach Master...", modifier = Modifier.padding(start = 40.dp))
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(16.dp), strokeWidth = 2.dp)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Text(
|
||||
"Warte auf Master-Signal...",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,7 +338,7 @@ actual fun DeviceInitializationConfig(
|
||||
)
|
||||
}
|
||||
if (settings.networkRole == NetworkRole.MASTER && uiState.isLocked && settings.expectedClients.isNotEmpty()) {
|
||||
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
|
||||
HorizontalDivider(Modifier.padding(vertical = 8.dp))
|
||||
Text("👥 Erwartete Clients", style = MaterialTheme.typography.titleSmall)
|
||||
settings.expectedClients.forEach { client ->
|
||||
ListItem(
|
||||
|
||||
+9
-4
@@ -12,6 +12,8 @@ import androidx.compose.material.icons.outlined.VisibilityOff
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -20,7 +22,8 @@ import at.mocode.frontend.features.device.initialization.domain.DeviceInitializa
|
||||
@Composable
|
||||
actual fun DeviceInitializationConfig(
|
||||
uiState: DeviceInitializationUiState,
|
||||
viewModel: DeviceInitializationViewModel
|
||||
viewModel: DeviceInitializationViewModel,
|
||||
deviceNameFocus: FocusRequester
|
||||
) {
|
||||
val settings = uiState.settings
|
||||
|
||||
@@ -34,7 +37,8 @@ actual fun DeviceInitializationConfig(
|
||||
label = "Gerätename",
|
||||
placeholder = "z.B. Web-Client",
|
||||
isError = settings.deviceName.isNotEmpty() && !DeviceInitializationValidator.isNameValid(settings.deviceName),
|
||||
errorText = "Mindestens ${DeviceInitializationValidator.MIN_NAME_LENGTH} Zeichen erforderlich."
|
||||
errorText = "Mindestens ${DeviceInitializationValidator.MIN_NAME_LENGTH} Zeichen erforderlich.",
|
||||
modifier = Modifier.focusRequester(deviceNameFocus)
|
||||
)
|
||||
|
||||
var passwordVisible by remember { mutableStateOf(false) }
|
||||
@@ -74,14 +78,15 @@ private fun MsSettingsField(
|
||||
isError: Boolean,
|
||||
errorText: String,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
trailingIcon: @Composable (() -> Unit)? = null
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
label = { Text(label) },
|
||||
placeholder = { Text(placeholder) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
isError = isError,
|
||||
visualTransformation = visualTransformation,
|
||||
trailingIcon = trailingIcon,
|
||||
|
||||
+11
-1
@@ -1,5 +1,6 @@
|
||||
package at.mocode.frontend.shell.desktop
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
@@ -25,7 +26,16 @@ import org.koin.compose.viewmodel.koinViewModel
|
||||
*/
|
||||
@Composable
|
||||
fun DesktopApp() {
|
||||
AppTheme {
|
||||
val deviceInitViewModel: at.mocode.frontend.features.device.initialization.presentation.DeviceInitializationViewModel = koinViewModel()
|
||||
val deviceSettings by deviceInitViewModel.uiState.collectAsState()
|
||||
|
||||
val isDark = when(deviceSettings.settings.appTheme) {
|
||||
at.mocode.frontend.features.device.initialization.domain.model.AppThemeSetting.SYSTEM -> isSystemInDarkTheme()
|
||||
at.mocode.frontend.features.device.initialization.domain.model.AppThemeSetting.LIGHT -> false
|
||||
at.mocode.frontend.features.device.initialization.domain.model.AppThemeSetting.DARK -> true
|
||||
}
|
||||
|
||||
AppTheme(darkTheme = isDark) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background,
|
||||
|
||||
+6
-4
@@ -66,17 +66,19 @@ fun DesktopContentArea(
|
||||
// DeviceInitialization (Geräte-Setup)
|
||||
is AppScreen.DeviceInitialization -> {
|
||||
println("[Screen] Rendering DeviceInitialization")
|
||||
val viewModel = koinViewModel<DeviceInitializationViewModel> {
|
||||
parametersOf({ finalSettings: DeviceInitializationSettings ->
|
||||
val viewModel = koinViewModel<DeviceInitializationViewModel>()
|
||||
|
||||
LaunchedEffect(viewModel) {
|
||||
viewModel.initializationCompleteEvent.collect { finalSettings ->
|
||||
DeviceInitializationSettingsManager.saveSettings(finalSettings)
|
||||
// Vision_04: Sicherheitsschlüssel als Token setzen, damit Cloud-Suche funktioniert
|
||||
val authTokenManager = org.koin.core.context.GlobalContext.get().get<AuthTokenManager>()
|
||||
authTokenManager.setToken(finalSettings.sharedKey)
|
||||
onSettingsChange(finalSettings)
|
||||
// nav.navigateToScreen(...) wird hier nicht direkt gerufen, sondern onNavigate
|
||||
onNavigate(AppScreen.EventVerwaltung)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
DeviceInitializationScreen(viewModel = viewModel)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user