feat(core, device-initialization): Netzwerk-Discovery verbessert, IP-Binding hinzugefügt und UI optimiert

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
2026-04-30 12:12:46 +02:00
parent 46d993e47f
commit 8ab6ab1c2a
25 changed files with 686 additions and 179 deletions
@@ -90,7 +90,8 @@ fun DesktopApp() {
currentScreen is AppScreen.ConnectivityCheck ||
currentScreen is AppScreen.Dashboard ||
currentScreen is AppScreen.Profile ||
currentScreen is AppScreen.ProfileOnboarding
currentScreen is AppScreen.ProfileOnboarding ||
currentScreen is AppScreen.Chat
if (!authState.isAuthenticated && !isAllowedScreen) {
LaunchedEffect(currentScreen) {
@@ -0,0 +1,167 @@
package at.mocode.frontend.shell.desktop.screens.chat
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import at.mocode.frontend.core.designsystem.theme.AppColors
import at.mocode.frontend.core.designsystem.theme.Dimens
import java.time.LocalTime
import java.time.format.DateTimeFormatter
data class ChatMessage(
val id: String,
val sender: String,
val text: String,
val time: String,
val isFromMe: Boolean
)
@Composable
fun ChatScreen(
onBack: () -> Unit
) {
var messageText by remember { mutableStateOf("") }
val messages = remember { mutableStateListOf<ChatMessage>() }
val timeFormatter = DateTimeFormatter.ofPattern("HH:mm")
// Mock initial messages
LaunchedEffect(Unit) {
if (messages.isEmpty()) {
messages.add(ChatMessage("1", "Richter-Turm 1", "Startliste für Bewerb 5 ist fertig?", "10:45", false))
messages.add(ChatMessage("2", "Meldestelle", "Ja, wird gerade gedruckt.", "10:46", true))
}
}
Column(modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.surface)) {
// Header
Surface(
tonalElevation = 2.dp,
shadowElevation = 2.dp,
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.padding(Dimens.SpacingM),
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
"Veranstaltungs-Chat",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
Text(
"LAN-Kanal: aktiv (3 Teilnehmer)",
style = MaterialTheme.typography.labelMedium,
color = AppColors.Success
)
}
}
}
// Chat Messages
LazyColumn(
modifier = Modifier.weight(1f).fillMaxWidth().padding(horizontal = Dimens.SpacingM),
contentPadding = PaddingValues(vertical = Dimens.SpacingM),
verticalArrangement = Arrangement.spacedBy(Dimens.SpacingS)
) {
items(messages) { msg ->
ChatBubble(msg)
}
}
// Input Area
Surface(
tonalElevation = 4.dp,
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.padding(Dimens.SpacingM),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingS)
) {
OutlinedTextField(
value = messageText,
onValueChange = { messageText = it },
placeholder = { Text("Nachricht schreiben...") },
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(24.dp),
maxLines = 3
)
IconButton(
onClick = {
if (messageText.isNotBlank()) {
messages.add(
ChatMessage(
id = messages.size.toString(),
sender = "Meldestelle",
text = messageText,
time = LocalTime.now().format(timeFormatter),
isFromMe = true
)
)
messageText = ""
}
},
colors = IconButtonDefaults.iconButtonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary
),
modifier = Modifier.size(48.dp)
) {
Icon(Icons.AutoMirrored.Filled.Send, contentDescription = "Senden")
}
}
}
}
}
@Composable
private fun ChatBubble(msg: ChatMessage) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = if (msg.isFromMe) Alignment.End else Alignment.Start
) {
if (!msg.isFromMe) {
Text(
msg.sender,
style = MaterialTheme.typography.labelSmall,
modifier = Modifier.padding(start = 4.dp, bottom = 2.dp)
)
}
Surface(
color = if (msg.isFromMe) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.secondaryContainer,
shape = RoundedCornerShape(
topStart = 12.dp,
topEnd = 12.dp,
bottomStart = if (msg.isFromMe) 12.dp else 0.dp,
bottomEnd = if (msg.isFromMe) 0.dp else 12.dp
),
modifier = Modifier.widthIn(max = 400.dp)
) {
Column(modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp)) {
Text(msg.text, style = MaterialTheme.typography.bodyMedium)
Text(
msg.time,
style = MaterialTheme.typography.labelSmall.copy(
fontSize = 9.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
),
modifier = Modifier.align(Alignment.End)
)
}
}
}
}
@@ -86,7 +86,8 @@ fun DesktopMainLayout(
HorizontalDivider(thickness = Dimens.BorderThin, color = MaterialTheme.colorScheme.outlineVariant)
DesktopFooterBar(
settings = onboardingSettings,
onSetupClick = { onNavigate(AppScreen.DeviceInitialization) }
onSetupClick = { onNavigate(AppScreen.DeviceInitialization) },
onNavigate = onNavigate
)
}
}
@@ -42,6 +42,7 @@ import at.mocode.frontend.features.veranstalter.presentation.VeranstaltungKonfig
import at.mocode.frontend.features.verein.presentation.VereinScreen
import at.mocode.frontend.features.verein.presentation.VereinViewModel
import at.mocode.frontend.features.zns.import.presentation.StammdatenImportScreen
import at.mocode.frontend.shell.desktop.screens.chat.ChatScreen
import at.mocode.frontend.shell.desktop.screens.management.VeranstalterAuswahl
import at.mocode.frontend.shell.desktop.screens.management.VeranstalterDetail
import at.mocode.frontend.shell.desktop.screens.management.VeranstalterVerwaltungScreen
@@ -341,6 +342,12 @@ fun DesktopContentArea(
)
}
is AppScreen.Chat -> {
ChatScreen(
onBack = onBack
)
}
is AppScreen.EntryManagement -> {
val viewModel = koinViewModel<NennungViewModel>()
NennungManagementScreen(viewModel = viewModel)
@@ -3,6 +3,7 @@ package at.mocode.frontend.shell.desktop.screens.layout.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Chat
import androidx.compose.material.icons.filled.CloudDone
import androidx.compose.material.icons.filled.CloudOff
import androidx.compose.material.icons.filled.Dataset
@@ -18,6 +19,7 @@ import androidx.compose.ui.unit.sp
import at.mocode.frontend.core.designsystem.theme.AppColors
import at.mocode.frontend.core.designsystem.theme.Dimens
import at.mocode.frontend.core.domain.zns.ZnsImportProvider
import at.mocode.frontend.core.navigation.AppScreen
import at.mocode.frontend.core.network.ConnectivityTracker
import at.mocode.frontend.core.network.discovery.NetworkDiscoveryService
import at.mocode.frontend.features.device.initialization.domain.model.DeviceInitializationSettings
@@ -28,7 +30,8 @@ import kotlin.time.Duration.Companion.milliseconds
@Composable
fun DesktopFooterBar(
settings: DeviceInitializationSettings,
onSetupClick: () -> Unit = {}
onSetupClick: () -> Unit = {},
onNavigate: (AppScreen) -> Unit = {}
) {
val connectivityTracker = koinInject<ConnectivityTracker>()
val discoveryService = koinInject<NetworkDiscoveryService>()
@@ -102,7 +105,26 @@ fun DesktopFooterBar(
)
}
Row(verticalAlignment = Alignment.CenterVertically) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM)
) {
// Chat Trigger
Button(
onClick = { onNavigate(AppScreen.Chat) },
contentPadding = PaddingValues(horizontal = Dimens.SpacingS, vertical = 0.dp),
modifier = Modifier.height(22.dp),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
),
shape = MaterialTheme.shapes.small
) {
Icon(Icons.AutoMirrored.Filled.Chat, contentDescription = null, modifier = Modifier.size(12.dp))
Spacer(Modifier.width(4.dp))
Text("Chat", style = MaterialTheme.typography.labelSmall.copy(fontSize = 10.sp))
}
Text(
text = "v2.4.0-rc1 | Desktop-Alpha",
style = MaterialTheme.typography.labelSmall.copy(fontSize = 10.sp),
Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

@@ -180,6 +180,46 @@ fun Erfolgsscreen(email: String, onBack: () -> Unit) {
}
}
@Composable
fun DownloadDesktopAppCard() {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = AppColors.PrimaryContainer),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Row(
modifier = Modifier.padding(24.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(24.dp)
) {
Column(modifier = Modifier.weight(1f)) {
Text(
"Meldestelle Desktop",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold,
color = AppColors.OnPrimaryContainer
)
Text(
"Laden Sie die professionelle Desktop-App für die Offline-Verwaltung Ihres Turniers herunter.",
style = MaterialTheme.typography.bodyLarge,
color = AppColors.OnPrimaryContainer.copy(alpha = 0.8f),
modifier = Modifier.padding(top = 8.dp)
)
}
Button(
onClick = { /* In POC: Zeigt Hinweis oder simuliert Download */ },
colors = ButtonDefaults.buttonColors(containerColor = AppColors.Primary),
modifier = Modifier.height(56.dp)
) {
Icon(Icons.Default.Description, contentDescription = null) // Verwende Description als Ersatz für Download
Spacer(Modifier.width(12.dp))
Text("Desktop-App laden", style = MaterialTheme.typography.titleMedium)
}
}
}
}
@Composable
fun LandingPage(
onVeranstaltungClick: (Long) -> Unit,
@@ -205,6 +245,10 @@ fun LandingPage(
modifier = Modifier.fillMaxSize().padding(16.dp),
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
item {
DownloadDesktopAppCard()
}
item {
Text(
"Willkommen bei der Meldestelle Online",
@@ -23,7 +23,8 @@ fun main() {
}
ComposeViewport("compose-target") {
AppTheme {
// Web-Shell wird hart auf Light-Mode gesetzt (Ablesbarkeit am Turnierplatz)
AppTheme(darkTheme = false) {
WebMainScreen()
}
}