chore: implementiere Logo-Upload-Zone mit Base64-Unterstützung, verbessere Vereinsverwaltung mit kompakten Feldern und nutzerspezifischen Uploadoptionen, optimiere Desktop-UX und Navigation
This commit is contained in:
+57
-37
@@ -10,13 +10,13 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Business
|
||||
import androidx.compose.material.icons.filled.Image
|
||||
import androidx.compose.material.icons.filled.Map
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -25,6 +25,17 @@ import at.mocode.frontend.core.designsystem.models.PlaceholderContent
|
||||
import at.mocode.frontend.features.verein.domain.Bundesland
|
||||
import at.mocode.frontend.features.verein.domain.Verein
|
||||
import at.mocode.frontend.features.verein.domain.VereinStatus
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
@OptIn(ExperimentalEncodingApi::class)
|
||||
expect fun decodeBase64ToImage(base64: String): ImageBitmap?
|
||||
|
||||
@Composable
|
||||
expect fun LogoUploadZone(
|
||||
modifier: Modifier = Modifier,
|
||||
onFileSelected: (ByteArray) -> Unit
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun VereinScreen(
|
||||
@@ -54,6 +65,7 @@ fun VereinScreen(
|
||||
hausnummer = if (uiState.isEditing) uiState.editHausnummer else uiState.selectedVerein?.hausnummer,
|
||||
bundesland = if (uiState.isEditing) uiState.editBundesland else uiState.selectedVerein?.bundesland,
|
||||
logoUrl = if (uiState.isEditing) uiState.editLogoUrl else uiState.selectedVerein?.logoUrl,
|
||||
logoBase64 = if (uiState.isEditing) uiState.editLogoBase64 else uiState.selectedVerein?.logoBase64,
|
||||
status = if (uiState.isEditing) uiState.editStatus else uiState.selectedVerein?.status ?: VereinStatus.AKTIV
|
||||
)
|
||||
|
||||
@@ -74,6 +86,7 @@ fun VereinScreen(
|
||||
onBundeslandChange = viewModel::onEditBundeslandChange,
|
||||
onStatusChange = viewModel::onEditStatusChange,
|
||||
onLogoUrlChange = viewModel::onEditLogoUrlChange,
|
||||
onLogoFileSelected = viewModel::onLogoFileSelected,
|
||||
onSave = viewModel::onSave,
|
||||
onCancel = viewModel::onCancel
|
||||
)
|
||||
@@ -99,6 +112,7 @@ private fun VereinCardPreview(
|
||||
hausnummer: String?,
|
||||
bundesland: String?,
|
||||
logoUrl: String?,
|
||||
logoBase64: String?,
|
||||
status: VereinStatus
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
@@ -122,10 +136,22 @@ private fun VereinCardPreview(
|
||||
.border(1.dp, MaterialTheme.colorScheme.primary.copy(alpha = 0.2f), CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (!logoUrl.isNullOrBlank()) {
|
||||
Icon(Icons.Default.Business, null, modifier = Modifier.size(40.dp), tint = MaterialTheme.colorScheme.primary)
|
||||
if (!logoBase64.isNullOrBlank()) {
|
||||
val bitmap = remember(logoBase64) { decodeBase64ToImage(logoBase64) }
|
||||
if (bitmap != null) {
|
||||
androidx.compose.foundation.Image(
|
||||
bitmap = bitmap,
|
||||
contentDescription = "Vereinslogo",
|
||||
modifier = Modifier.fillMaxSize().clip(CircleShape),
|
||||
contentScale = androidx.compose.ui.layout.ContentScale.Crop
|
||||
)
|
||||
} else {
|
||||
Icon(Icons.Default.Image, "Logo Fehler", modifier = Modifier.size(40.dp), tint = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
} else if (!logoUrl.isNullOrBlank()) {
|
||||
Icon(Icons.Default.Business, "Logo URL", modifier = Modifier.size(40.dp), tint = MaterialTheme.colorScheme.primary)
|
||||
} else {
|
||||
Icon(Icons.Default.Business, null, modifier = Modifier.size(40.dp), tint = MaterialTheme.colorScheme.primary)
|
||||
Icon(Icons.Default.Business, null, modifier = Modifier.size(40.dp), tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,6 +287,7 @@ private fun VereinEditorContent(
|
||||
onBundeslandChange: (String) -> Unit,
|
||||
onStatusChange: (VereinStatus) -> Unit,
|
||||
onLogoUrlChange: (String) -> Unit,
|
||||
onLogoFileSelected: (ByteArray) -> Unit,
|
||||
onSave: () -> Unit,
|
||||
onCancel: () -> Unit
|
||||
) {
|
||||
@@ -279,40 +306,28 @@ private fun VereinEditorContent(
|
||||
value = uiState.editName,
|
||||
onValueChange = onNameChange,
|
||||
label = "Name (Kurz)",
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
compact = true
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
MsTextField(
|
||||
value = uiState.editLangname,
|
||||
onValueChange = onLangnameChange,
|
||||
label = "Vollständiger Name",
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
compact = true
|
||||
)
|
||||
}
|
||||
|
||||
// Logo Upload Sektion
|
||||
Column(
|
||||
LogoUploadZone(
|
||||
modifier = Modifier
|
||||
.width(200.dp)
|
||||
.height(120.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f))
|
||||
.border(1.dp, MaterialTheme.colorScheme.outlineVariant, RoundedCornerShape(8.dp)),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(Icons.Default.Image, null, tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f))
|
||||
Text("Logo hierher ziehen", style = MaterialTheme.typography.labelSmall, color = Color.Gray)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
MsButton(
|
||||
text = "Wählen",
|
||||
onClick = { /* FilePicker Call via JFileChooser (JVM only) */ },
|
||||
variant = ButtonVariant.SECONDARY,
|
||||
size = ButtonSize.SMALL
|
||||
)
|
||||
}
|
||||
.width(180.dp)
|
||||
.height(110.dp),
|
||||
onFileSelected = onLogoFileSelected
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
@@ -322,7 +337,8 @@ private fun VereinEditorContent(
|
||||
value = uiState.editOepsNr,
|
||||
onValueChange = onOepsNrChange,
|
||||
label = "OePS-Nr",
|
||||
modifier = Modifier.weight(1f)
|
||||
modifier = Modifier.weight(0.5f),
|
||||
compact = true
|
||||
)
|
||||
MsEnumDropdown(
|
||||
label = "Status",
|
||||
@@ -330,49 +346,53 @@ private fun VereinEditorContent(
|
||||
selectedOption = uiState.editStatus,
|
||||
onOptionSelected = onStatusChange,
|
||||
optionLabel = { it.label },
|
||||
modifier = Modifier.weight(1f)
|
||||
modifier = Modifier.weight(0.5f)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Spacer(Modifier.height(12.dp))
|
||||
|
||||
Text("Adresse", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.Bold)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text("Adresse", style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
MsTextField(
|
||||
value = uiState.editStrasse,
|
||||
onValueChange = onStrasseChange,
|
||||
label = "Straße",
|
||||
modifier = Modifier.weight(0.7f)
|
||||
modifier = Modifier.weight(0.7f),
|
||||
compact = true
|
||||
)
|
||||
MsTextField(
|
||||
value = uiState.editHausnummer,
|
||||
onValueChange = onHausnummerChange,
|
||||
label = "Nr.",
|
||||
modifier = Modifier.weight(0.3f)
|
||||
modifier = Modifier.weight(0.3f),
|
||||
compact = true
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
MsTextField(
|
||||
value = uiState.editPlz,
|
||||
onValueChange = onPlzChange,
|
||||
label = "PLZ",
|
||||
modifier = Modifier.weight(0.2f)
|
||||
modifier = Modifier.weight(0.2f),
|
||||
compact = true
|
||||
)
|
||||
MsTextField(
|
||||
value = uiState.editOrt,
|
||||
onValueChange = onOrtChange,
|
||||
label = "Ort",
|
||||
modifier = Modifier.weight(0.4f)
|
||||
modifier = Modifier.weight(0.4f),
|
||||
compact = true
|
||||
)
|
||||
MsEnumDropdown(
|
||||
label = "Bundesland",
|
||||
options = Bundesland.entries.toTypedArray(),
|
||||
selectedOption = Bundesland.entries.find { it.label == uiState.editBundesland },
|
||||
selectedOption = Bundesland.entries.find { it.label == uiState.editBundesland } ?: Bundesland.WIEN,
|
||||
onOptionSelected = { onBundeslandChange(it.label) },
|
||||
optionLabel = { it.label },
|
||||
modifier = Modifier.weight(0.4f)
|
||||
|
||||
+14
@@ -9,6 +9,8 @@ import at.mocode.frontend.features.verein.domain.Verein
|
||||
import at.mocode.frontend.features.verein.domain.VereinRepository
|
||||
import at.mocode.frontend.features.verein.domain.VereinStatus
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
/**
|
||||
* UI-State für die Vereins-Verwaltung.
|
||||
@@ -157,6 +159,18 @@ open class VereinViewModel(
|
||||
uiState = uiState.copy(editStatus = value)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalEncodingApi::class)
|
||||
fun onLogoFileSelected(bytes: ByteArray) {
|
||||
println("[VereinViewModel] Logo Datei empfangen, konvertiere zu Base64...")
|
||||
try {
|
||||
val base64 = Base64.encode(bytes)
|
||||
uiState = uiState.copy(editLogoBase64 = base64)
|
||||
println("[VereinViewModel] Logo erfolgreich in Base64 konvertiert (Länge: ${base64.length})")
|
||||
} catch (e: Exception) {
|
||||
println("[VereinViewModel] Fehler bei Base64 Konvertierung: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun onSave() {
|
||||
uiState = uiState.copy(isLoading = true, error = null)
|
||||
val verein = (uiState.selectedVerein ?: Verein(
|
||||
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
package at.mocode.frontend.features.verein.presentation
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Image
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.frontend.core.designsystem.components.ButtonSize
|
||||
import at.mocode.frontend.core.designsystem.components.ButtonVariant
|
||||
import at.mocode.frontend.core.designsystem.components.MsButton
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.compose.ui.awt.ComposeWindow
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.toComposeImageBitmap
|
||||
import org.jetbrains.skia.Image
|
||||
import java.awt.FileDialog
|
||||
import java.awt.Frame
|
||||
import java.awt.Window
|
||||
import java.io.File
|
||||
import javax.swing.SwingUtilities
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
@OptIn(ExperimentalEncodingApi::class)
|
||||
actual fun decodeBase64ToImage(base64: String): ImageBitmap? {
|
||||
return try {
|
||||
val bytes = Base64.decode(base64)
|
||||
Image.makeFromEncoded(bytes).toComposeImageBitmap()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
actual fun LogoUploadZone(
|
||||
modifier: Modifier,
|
||||
onFileSelected: (ByteArray) -> Unit
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f))
|
||||
.border(1.dp, MaterialTheme.colorScheme.outlineVariant, RoundedCornerShape(8.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Image,
|
||||
null,
|
||||
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f),
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
Text("Logo auswählen", style = MaterialTheme.typography.labelSmall, color = Color.Gray)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
MsButton(
|
||||
text = "Datei wählen",
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
println("[LogoUpload] Öffne Datei-Dialog...")
|
||||
val fileDialog = FileDialog(null as Frame?, "Logo auswählen", FileDialog.LOAD)
|
||||
fileDialog.isVisible = true
|
||||
val directory = fileDialog.directory
|
||||
val file = fileDialog.file
|
||||
if (directory != null && file != null) {
|
||||
val selectedFile = File(directory, file)
|
||||
println("[LogoUpload] Datei ausgewählt: ${selectedFile.absolutePath}")
|
||||
val bytes = selectedFile.readBytes()
|
||||
println("[LogoUpload] Bytes gelesen: ${bytes.size}")
|
||||
onFileSelected(bytes)
|
||||
} else {
|
||||
println("[LogoUpload] Auswahl abgebrochen")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("[LogoUpload] FEHLER: ${e.message}")
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
},
|
||||
variant = ButtonVariant.SECONDARY,
|
||||
size = ButtonSize.SMALL
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package at.mocode.frontend.features.verein.presentation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
@OptIn(ExperimentalEncodingApi::class)
|
||||
actual fun decodeBase64ToImage(base64: String): ImageBitmap? = null
|
||||
|
||||
@Composable
|
||||
actual fun LogoUploadZone(
|
||||
modifier: Modifier,
|
||||
onFileSelected: (ByteArray) -> Unit
|
||||
) {
|
||||
// Nicht implementiert für WasmJs
|
||||
}
|
||||
Reference in New Issue
Block a user