Refactor code structure for improved readability and maintainability
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 53s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 53s
This commit is contained in:
Binary file not shown.
@@ -11,6 +11,7 @@ import android.text.TextPaint
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.DiaryDateActivityItem
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.DiaryFreeformActivity
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.DiaryTrainingParticipant
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.DiaryPlanGroup
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.Member
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.displayTitle
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.isPresentParticipant
|
||||
@@ -106,6 +107,8 @@ fun writeTrainingDaySummaryPdf(
|
||||
participants: List<DiaryTrainingParticipant>,
|
||||
freeform: List<DiaryFreeformActivity>,
|
||||
planItems: List<DiaryDateActivityItem>,
|
||||
planGroups: List<DiaryPlanGroup>,
|
||||
activityParticipantRows: Map<Int, List<Int>>,
|
||||
timeblockFallback: String,
|
||||
) {
|
||||
val doc = PdfDocument()
|
||||
@@ -178,10 +181,33 @@ fun writeTrainingDaySummaryPdf(
|
||||
newPageIfNeeded(40f)
|
||||
val line = buildString {
|
||||
append(item.displayTitle(timeblockFallback).ifBlank { "Eintrag ${item.id}" })
|
||||
item.groupId?.let { gid ->
|
||||
val groupName = planGroups.find { it.id == gid }?.name ?: "Gruppe $gid"
|
||||
append(" [$groupName]")
|
||||
}
|
||||
item.durationText?.takeIf { it.isNotBlank() }?.let { append(" — $it") }
|
||||
?: item.duration?.let { append(" — ${it} min") }
|
||||
}
|
||||
y = canvas.drawStatic("• $line", bodyPaint, MARGIN, y)
|
||||
val assigned = activityParticipantRows[item.id].orEmpty().mapNotNull { participantId ->
|
||||
val participant = participants.find { it.id == participantId } ?: return@mapNotNull null
|
||||
activeMembers.find { it.id == participant.memberId }?.let { "${it.firstName} ${it.lastName}" }
|
||||
}
|
||||
if (assigned.isNotEmpty()) {
|
||||
y = canvas.drawStatic(" Teilnehmer: ${assigned.joinToString(", ")}", bodyPaint, MARGIN, y)
|
||||
}
|
||||
item.groupActivities.forEach { nested ->
|
||||
val nestedId = nested.id ?: return@forEach
|
||||
val nestedAssigned = activityParticipantRows[nestedId].orEmpty().mapNotNull { participantId ->
|
||||
val participant = participants.find { it.id == participantId } ?: return@mapNotNull null
|
||||
activeMembers.find { it.id == participant.memberId }?.let { "${it.firstName} ${it.lastName}" }
|
||||
}
|
||||
val nestedTitle = nested.groupPredefinedActivity?.name ?: nested.groupPredefinedActivity?.code ?: "Gruppenaktivitaet"
|
||||
y = canvas.drawStatic(" - $nestedTitle", bodyPaint, MARGIN, y)
|
||||
if (nestedAssigned.isNotEmpty()) {
|
||||
y = canvas.drawStatic(" Teilnehmer: ${nestedAssigned.joinToString(", ")}", bodyPaint, MARGIN, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,326 @@
|
||||
package de.tsschulz.tt_tagebuch.app.ui
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.AlertDialog
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedButton
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.PredefinedActivityDto
|
||||
import de.tsschulz.tt_tagebuch.shared.api.models.PredefinedActivityUpsertBody
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlinx.serialization.json.putJsonArray
|
||||
|
||||
internal data class DiaryCourtStroke(
|
||||
val side: String = "VH",
|
||||
val type: String = "US",
|
||||
val target: String = "",
|
||||
)
|
||||
|
||||
internal data class DiaryCourtDrawingData(
|
||||
val start: String = "AS2",
|
||||
val stroke: String = "VH",
|
||||
val spin: String = "",
|
||||
val target: String = "",
|
||||
val additionalStrokes: List<DiaryCourtStroke> = emptyList(),
|
||||
) {
|
||||
val valid: Boolean get() = start.isNotBlank() && stroke.isNotBlank() && spin.isNotBlank() && target.isNotBlank()
|
||||
|
||||
fun fullCode(): String = buildString {
|
||||
append("$start $stroke $spin")
|
||||
if (target.isNotBlank()) append(" -> $target")
|
||||
additionalStrokes.forEach { extra ->
|
||||
append(" / ${extra.side} ${extra.type} -> ${extra.target}")
|
||||
}
|
||||
}.trim()
|
||||
|
||||
fun toJson(): String = buildJsonObject {
|
||||
put("selectedStartPosition", start)
|
||||
put("selectedCirclePosition", when (start) {
|
||||
"AS1" -> "top"
|
||||
"AS3" -> "bottom"
|
||||
else -> "middle"
|
||||
})
|
||||
put("strokeType", stroke)
|
||||
put("spinType", spin)
|
||||
put("targetPosition", target)
|
||||
put("exerciseCounter", 1 + additionalStrokes.size)
|
||||
put("code", fullCode())
|
||||
put("renderCode", fullCode())
|
||||
putJsonArray("additionalStrokes") {
|
||||
additionalStrokes.forEachIndexed { index, extra ->
|
||||
add(buildJsonObject {
|
||||
put("side", extra.side)
|
||||
put("type", extra.type)
|
||||
put("targetPosition", extra.target)
|
||||
put("counter", index + 2)
|
||||
})
|
||||
}
|
||||
}
|
||||
}.toString()
|
||||
|
||||
companion object {
|
||||
fun fromJson(raw: String?): DiaryCourtDrawingData {
|
||||
if (raw.isNullOrBlank()) return DiaryCourtDrawingData()
|
||||
val obj = runCatching { Json.parseToJsonElement(raw) as? JsonObject }.getOrNull() ?: return DiaryCourtDrawingData()
|
||||
fun string(key: String, fallback: String) = obj[key]?.jsonPrimitive?.contentOrNull ?: fallback
|
||||
val extras = (obj["additionalStrokes"] as? JsonArray).orEmpty().mapNotNull { rawStroke ->
|
||||
val strokeObj = rawStroke as? JsonObject ?: return@mapNotNull null
|
||||
val target = strokeObj["targetPosition"]?.jsonPrimitive?.contentOrNull.orEmpty()
|
||||
if (target.isBlank()) return@mapNotNull null
|
||||
DiaryCourtStroke(
|
||||
side = strokeObj["side"]?.jsonPrimitive?.contentOrNull ?: "VH",
|
||||
type = strokeObj["type"]?.jsonPrimitive?.contentOrNull ?: "US",
|
||||
target = target,
|
||||
)
|
||||
}.take(4)
|
||||
return DiaryCourtDrawingData(
|
||||
start = string("selectedStartPosition", "AS2"),
|
||||
stroke = string("strokeType", "VH"),
|
||||
spin = string("spinType", ""),
|
||||
target = string("targetPosition", ""),
|
||||
additionalStrokes = extras,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun DiaryCourtDrawingDialog(
|
||||
initial: PredefinedActivityDto?,
|
||||
onDismiss: () -> Unit,
|
||||
onSave: (PredefinedActivityUpsertBody) -> Unit,
|
||||
) {
|
||||
var name by remember(initial?.id) { mutableStateOf(initial?.name.orEmpty()) }
|
||||
var code by remember(initial?.id) { mutableStateOf(initial?.code.orEmpty()) }
|
||||
var duration by remember(initial?.id) { mutableStateOf(initial?.duration?.toString().orEmpty()) }
|
||||
var durationText by remember(initial?.id) { mutableStateOf(initial?.durationText.orEmpty()) }
|
||||
var drawing by remember(initial?.id) { mutableStateOf(DiaryCourtDrawingData.fromJson(initial?.drawingData)) }
|
||||
var nextSide by remember(initial?.id) { mutableStateOf("VH") }
|
||||
var nextType by remember(initial?.id) { mutableStateOf("US") }
|
||||
var nextTarget by remember(initial?.id) { mutableStateOf("") }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("Uebungszeichnung") },
|
||||
text = {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().heightIn(max = 600.dp).verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
OutlinedTextField(value = name, onValueChange = { name = it }, label = { Text("Name") }, singleLine = true)
|
||||
OutlinedTextField(value = code, onValueChange = { code = it }, label = { Text("Kuerzel") }, singleLine = true)
|
||||
DrawingChoiceRow("Start", listOf("AS1", "AS2", "AS3"), drawing.start) { drawing = drawing.copy(start = it) }
|
||||
DrawingChoiceRow("Schlagseite", listOf("VH", "RH"), drawing.stroke) { drawing = drawing.copy(stroke = it) }
|
||||
DrawingChoiceRow("Spin", listOf("US", "OS", "SS", "SUS"), drawing.spin) { drawing = drawing.copy(spin = it) }
|
||||
DrawingChoiceRow("Ziel", (1..9).map(Int::toString), drawing.target) { drawing = drawing.copy(target = it) }
|
||||
DiaryCourtDrawingPreview(drawing)
|
||||
if (drawing.additionalStrokes.isNotEmpty()) {
|
||||
Text("Folgeschlaege", style = MaterialTheme.typography.caption, fontWeight = FontWeight.SemiBold)
|
||||
drawing.additionalStrokes.forEachIndexed { index, extra ->
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text("${index + 2}. ${extra.side} ${extra.type} -> ${extra.target}")
|
||||
TextButton(onClick = {
|
||||
drawing = drawing.copy(additionalStrokes = drawing.additionalStrokes.filterIndexed { itemIndex, _ -> itemIndex != index })
|
||||
}) { Text("Entfernen") }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (drawing.additionalStrokes.size < 4) {
|
||||
Text("Folgeschlag hinzufuegen", style = MaterialTheme.typography.caption, fontWeight = FontWeight.SemiBold)
|
||||
DrawingChoiceRow("Seite", listOf("VH", "RH"), nextSide) { nextSide = it }
|
||||
DrawingChoiceRow("Schlag", listOf("US", "OS", "SS", "SUS"), nextType) { nextType = it }
|
||||
DrawingChoiceRow("Ziel", (1..9).map(Int::toString), nextTarget) { nextTarget = it }
|
||||
OutlinedButton(
|
||||
enabled = nextTarget.isNotBlank(),
|
||||
onClick = {
|
||||
drawing = drawing.copy(
|
||||
additionalStrokes = drawing.additionalStrokes + DiaryCourtStroke(nextSide, nextType, nextTarget),
|
||||
)
|
||||
nextTarget = ""
|
||||
},
|
||||
) { Text("Folgeschlag uebernehmen") }
|
||||
}
|
||||
Text(drawing.fullCode(), style = MaterialTheme.typography.caption)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(
|
||||
value = durationText,
|
||||
onValueChange = { durationText = it },
|
||||
label = { Text("Dauer (Text)") },
|
||||
modifier = Modifier.weight(1f),
|
||||
singleLine = true,
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = duration,
|
||||
onValueChange = { duration = it.filter(Char::isDigit) },
|
||||
label = { Text("Min.") },
|
||||
modifier = Modifier.weight(0.65f),
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
enabled = name.isNotBlank() && drawing.valid,
|
||||
onClick = {
|
||||
onSave(
|
||||
PredefinedActivityUpsertBody(
|
||||
name = name.trim(),
|
||||
code = code.trim().ifBlank { null },
|
||||
duration = duration.toIntOrNull(),
|
||||
durationText = durationText.trim().ifBlank { null },
|
||||
drawingData = drawing.toJson(),
|
||||
excludeFromStats = initial?.excludeFromStats ?: false,
|
||||
),
|
||||
)
|
||||
},
|
||||
) { Text("Speichern") }
|
||||
},
|
||||
dismissButton = { TextButton(onClick = onDismiss) { Text("Abbrechen") } },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun DiaryCourtDrawingViewerDialog(
|
||||
title: String,
|
||||
data: DiaryCourtDrawingData,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(title) },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
DiaryCourtDrawingPreview(data)
|
||||
Text(data.fullCode(), style = MaterialTheme.typography.body2)
|
||||
}
|
||||
},
|
||||
confirmButton = { TextButton(onClick = onDismiss) { Text("Schliessen") } },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DrawingChoiceRow(label: String, values: List<String>, selected: String, select: (String) -> Unit) {
|
||||
Column {
|
||||
Text(label, style = MaterialTheme.typography.caption, fontWeight = FontWeight.SemiBold)
|
||||
Row(
|
||||
modifier = Modifier.horizontalScroll(rememberScrollState()),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
values.forEach { value ->
|
||||
OutlinedButton(onClick = { select(value) }) {
|
||||
Text(if (selected == value) "[$value]" else value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun DiaryCourtDrawingPreview(data: DiaryCourtDrawingData, modifier: Modifier = Modifier) {
|
||||
Canvas(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(155.dp)
|
||||
.background(Color(0xFFF0F4F1))
|
||||
.padding(10.dp),
|
||||
) {
|
||||
val left = size.width * 0.15f
|
||||
val top = size.height * 0.10f
|
||||
val right = size.width * 0.92f
|
||||
val bottom = size.height * 0.90f
|
||||
val midX = (left + right) / 2f
|
||||
val midY = (top + bottom) / 2f
|
||||
drawRect(Color(0xFF315F43), topLeft = Offset(left, top), size = Size(right - left, bottom - top))
|
||||
drawRect(Color.White, topLeft = Offset(left, top), size = Size(right - left, bottom - top), style = Stroke(width = 4f))
|
||||
drawLine(Color.White, Offset(midX, top - 5f), Offset(midX, bottom + 5f), strokeWidth = 4f)
|
||||
drawLine(Color(0xFFD9E4DD), Offset(left, midY), Offset(right, midY), strokeWidth = 2f)
|
||||
|
||||
fun targetPoint(target: String, onRight: Boolean): Offset {
|
||||
val number = target.toIntOrNull() ?: 5
|
||||
val row = (number - 1) / 3
|
||||
val column = (number - 1) % 3
|
||||
val halfLeft = if (onRight) midX else left
|
||||
val halfRight = if (onRight) right else midX
|
||||
return Offset(
|
||||
halfLeft + (halfRight - halfLeft) * (0.20f + column * 0.30f),
|
||||
top + (bottom - top) * (0.18f + row * 0.32f),
|
||||
)
|
||||
}
|
||||
|
||||
val startY = when (data.start) {
|
||||
"AS1" -> top + (bottom - top) * 0.18f
|
||||
"AS3" -> bottom - (bottom - top) * 0.18f
|
||||
else -> midY
|
||||
}
|
||||
val start = Offset(left - 14f, startY)
|
||||
drawCircle(Color(0xFFC84C32), radius = 8f, center = start)
|
||||
if (data.target.isNotBlank()) {
|
||||
var previous = start
|
||||
var target = targetPoint(data.target, true)
|
||||
drawCourtArrow(previous, target, Color(0xFFC84C32))
|
||||
drawCircle(Color(0xFF90AF55), radius = 9f, center = target)
|
||||
previous = target
|
||||
data.additionalStrokes.forEachIndexed { index, extra ->
|
||||
target = targetPoint(extra.target, index % 2 != 0)
|
||||
drawCourtArrow(previous, target, Color(0xFFE8A52A))
|
||||
drawCircle(Color(0xFF90AF55), radius = 7f, center = target)
|
||||
previous = target
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DrawScope.drawCourtArrow(start: Offset, target: Offset, color: Color) {
|
||||
val path = Path().apply {
|
||||
moveTo(start.x, start.y)
|
||||
lineTo(target.x, target.y)
|
||||
}
|
||||
drawPath(path, color, style = Stroke(width = 5f, cap = StrokeCap.Round))
|
||||
val vector = start - target
|
||||
val len = kotlin.math.sqrt(vector.x * vector.x + vector.y * vector.y).coerceAtLeast(1f)
|
||||
val unit = Offset(vector.x / len, vector.y / len)
|
||||
val perpendicular = Offset(-unit.y, unit.x)
|
||||
val head = Path().apply {
|
||||
moveTo(target.x, target.y)
|
||||
lineTo(target.x + unit.x * 16f + perpendicular.x * 7f, target.y + unit.y * 16f + perpendicular.y * 7f)
|
||||
lineTo(target.x + unit.x * 16f - perpendicular.x * 7f, target.y + unit.y * 16f - perpendicular.y * 7f)
|
||||
close()
|
||||
}
|
||||
drawPath(head, color)
|
||||
}
|
||||
Reference in New Issue
Block a user