chore: update .gitignore and enhance backend and mobile app functionality
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 44s

- Added mobile app build directories and configuration files to .gitignore for cleaner repository management.
- Improved error handling in diaryMemberController by requiring diaryDateId and memberId query parameters.
- Refactored DiaryMemberService to log tag IDs instead of raw values for better debugging.
- Enhanced TournamentParticipantsTab and TournamentTab components with improved touch-action properties for better user experience.
- Updated mobile app's gradle.properties and build.gradle.kts for compatibility with AGP 9.x and Kotlin 2.1.21, including new dependencies for Coil and UCrop.
- Refactored MainApplication to simplify initialization and improved MainActivity to handle dependencies more robustly.
- Updated various UI components in the mobile app to enhance layout and functionality, including MemberDetailScreen and MemberEditScreen.
This commit is contained in:
Torsten Schulz (local)
2026-05-12 23:14:31 +02:00
parent 27f8af559b
commit 48f71b9df1
138 changed files with 54488 additions and 56 deletions

View File

@@ -1,3 +1,9 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
val backendBaseUrl = providers.gradleProperty("backendBaseUrl")
.orElse("https://tt-tagebuch.de")
.get()
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidApplication)
@@ -8,8 +14,8 @@ plugins {
kotlin {
androidTarget {
compilations.all {
kotlinOptions {
jvmTarget = "1.8"
compilerOptions.configure {
jvmTarget.set(JvmTarget.JVM_17)
}
}
}
@@ -20,6 +26,7 @@ kotlin {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material)
implementation(compose.materialIconsExtended)
implementation(compose.ui)
implementation(compose.components.resources)
implementation(libs.voyager.navigator)
@@ -32,6 +39,8 @@ kotlin {
implementation(libs.androidx.appcompat)
implementation(libs.androidx.core.ktx)
implementation(libs.koin.android)
implementation(libs.coil.compose)
implementation(libs.yalantis.ucrop)
}
}
}
@@ -46,6 +55,10 @@ android {
targetSdk = libs.versions.android.targetSdk.get().toInt()
versionCode = 1
versionName = "1.0.0"
buildConfigField("String", "BACKEND_BASE_URL", "\"$backendBaseUrl\"")
}
buildFeatures {
buildConfig = true
}
packaging {
resources {
@@ -58,8 +71,8 @@ android {
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}

View File

@@ -8,18 +8,34 @@
android:allowBackup="true"
android:icon="@android:mipmap/sym_def_app_icon"
android:label="Trainingstagebuch"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@android:mipmap/sym_def_app_icon"
android:supportsRtl="true"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
android:theme="@android:style/Theme.Material.Light.NoActionBar"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="com.yalantis.ucrop.UCropActivity"
android:exported="false"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>

View File

@@ -0,0 +1,116 @@
package de.tt_tagebuch.app
import android.content.Context
import android.content.Intent
import android.net.Uri
import de.tt_tagebuch.shared.api.AccidentApi
import de.tt_tagebuch.shared.api.ApiConfig
import de.tt_tagebuch.shared.api.AuthApi
import de.tt_tagebuch.shared.api.PublicAuthApi
import de.tt_tagebuch.shared.api.ClubsApi
import de.tt_tagebuch.shared.api.DiaryApi
import de.tt_tagebuch.shared.api.DiaryMemberActivitiesApi
import de.tt_tagebuch.shared.api.DiaryMemberApi
import de.tt_tagebuch.shared.api.GroupApi
import de.tt_tagebuch.shared.api.ParticipantsApi
import de.tt_tagebuch.shared.api.PredefinedActivitiesApi
import de.tt_tagebuch.shared.api.MemberActivitiesApi
import de.tt_tagebuch.shared.api.MemberGroupPhotosApi
import de.tt_tagebuch.shared.api.MembersApi
import de.tt_tagebuch.shared.api.PermissionsApi
import de.tt_tagebuch.shared.api.SessionApi
import de.tt_tagebuch.shared.api.TrainingGroupsApi
import de.tt_tagebuch.shared.api.TrainingStatsApi
import de.tt_tagebuch.shared.api.TrainingTimesApi
import de.tt_tagebuch.shared.api.http.AndroidHttpClientEngineFactory
import de.tt_tagebuch.shared.api.http.AuthedHttpClient
import de.tt_tagebuch.shared.api.http.PublicHttpClient
import de.tt_tagebuch.shared.state.AndroidClubStorage
import de.tt_tagebuch.shared.state.AndroidLanguageStorage
import de.tt_tagebuch.shared.state.AndroidTokenStorage
import de.tt_tagebuch.shared.state.AuthManager
import de.tt_tagebuch.shared.state.ClubManager
import de.tt_tagebuch.shared.state.DiaryManager
import de.tt_tagebuch.shared.state.LanguageManager
import de.tt_tagebuch.shared.state.MembersManager
import de.tt_tagebuch.shared.state.MutableTokenProvider
import de.tt_tagebuch.shared.state.TrainingStatsManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
class AppDependencies(context: Context) {
private val appContext = context.applicationContext
private val applicationJob = SupervisorJob()
/**
* Für Suspend-Aufrufe aus Button-Callbacks: überlebt das Verlassen der Composition
* (z. B. Club wählen → anderer Screen), im Gegensatz zu [rememberCoroutineScope].
*/
val applicationScope = CoroutineScope(applicationJob + Dispatchers.Main.immediate)
val apiConfig = ApiConfig(baseUrl = BuildConfig.BACKEND_BASE_URL)
val unauthorizedEvents = MutableStateFlow(0)
private val tokenProvider = MutableTokenProvider()
private val client = AuthedHttpClient(
apiConfig = apiConfig,
tokenProvider = tokenProvider,
httpClientEngineFactory = AndroidHttpClientEngineFactory(),
onUnauthorized = { unauthorizedEvents.value += 1 },
)
private val publicHttpClient = PublicHttpClient(
apiConfig = apiConfig,
httpClientEngineFactory = AndroidHttpClientEngineFactory(),
)
val publicAuthApi = PublicAuthApi(publicHttpClient)
val authManager = AuthManager(
tokenProvider = tokenProvider,
tokenStorage = AndroidTokenStorage(context.applicationContext),
authApi = AuthApi(client),
sessionApi = SessionApi(client),
)
val clubManager = ClubManager(
clubStorage = AndroidClubStorage(context.applicationContext),
clubsApi = ClubsApi(client),
permissionsApi = PermissionsApi(client),
)
val diaryManager = DiaryManager(
DiaryApi(client),
ParticipantsApi(client),
GroupApi(client),
DiaryMemberActivitiesApi(client),
DiaryMemberApi(client),
PredefinedActivitiesApi(client),
AccidentApi(client),
MemberGroupPhotosApi(client),
)
val membersManager = MembersManager(
MembersApi(client),
TrainingGroupsApi(client),
MemberActivitiesApi(client),
TrainingTimesApi(client),
)
val trainingStatsManager = TrainingStatsManager(TrainingStatsApi(client))
val languageManager = LanguageManager(AndroidLanguageStorage(context.applicationContext))
val sessionApi = SessionApi(client)
/** Header wie [AuthedHttpClient] (für Coil-Bilder, Downloads). */
fun diaryAuthHeaders(): Map<String, String> = buildMap {
tokenProvider.token?.let { put("authcode", it) }
tokenProvider.username?.let { put("userid", it) }
}
/** Öffnet einen Pfad auf dem konfigurierten Backend im Browser (z. B. Impressum, Datenschutz). */
fun openBackendPath(path: String) {
val base = apiConfig.baseUrl.trimEnd('/')
val suffix = path.trim().let { p -> if (p.startsWith("/")) p else "/$p" }
val uri = Uri.parse("$base$suffix")
val intent = Intent(Intent.ACTION_VIEW, uri).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
runCatching { appContext.startActivity(intent) }
}
}

View File

@@ -3,12 +3,36 @@ package de.tt_tagebuch.app
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import de.tt_tagebuch.app.ui.AppRoot
import de.tt_tagebuch.app.ui.TtTagebuchTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
App()
val context = LocalContext.current
val dependencies = remember {
try {
AppDependencies(context.applicationContext)
} catch (t: Throwable) {
android.util.Log.e("MainActivity", "Failed to initialize AppDependencies", t)
throw t
}
}
TtTagebuchTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background,
) {
AppRoot(dependencies)
}
}
}
}
}

View File

@@ -1,18 +1,6 @@
package de.tt_tagebuch.app
import android.app.Application
import de.tt_tagebuch.app.di.appModule
import de.tt_tagebuch.shared.di.initKoin
import org.koin.android.ext.koin.androidContext
class MainApplication : Application() {
override fun onCreate() {
super.onCreate()
initKoin(
baseUrl = "https://tt-tagebuch.de",
additionalModules = listOf(appModule)
) {
androidContext(this@MainApplication)
}
}
}
class MainApplication : Application()

View File

@@ -0,0 +1,191 @@
package de.tt_tagebuch.app.pdf
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Typeface
import android.graphics.pdf.PdfDocument
import android.text.Layout
import android.text.StaticLayout
import android.text.TextPaint
import de.tt_tagebuch.shared.api.models.DiaryDateActivityItem
import de.tt_tagebuch.shared.api.models.DiaryFreeformActivity
import de.tt_tagebuch.shared.api.models.DiaryTrainingParticipant
import de.tt_tagebuch.shared.api.models.Member
import de.tt_tagebuch.shared.api.models.displayTitle
import de.tt_tagebuch.shared.api.models.isPresentParticipant
import java.io.File
import java.io.FileOutputStream
import java.util.Locale
private const val PAGE_W = 595
private const val PAGE_H = 842
private const val MARGIN = 40f
private const val MAX_TEXT_W = (PAGE_W - MARGIN * 2).toInt()
private fun newTitlePaint(): TextPaint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.BLACK
textSize = 16f
typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
}
private fun newBodyPaint(): TextPaint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.BLACK
textSize = 11f
typeface = Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)
textLocale = Locale.GERMANY
}
private fun Canvas.drawStatic(text: String, paint: TextPaint, x: Float, y: Float): Float {
if (text.isEmpty()) return y
val layout = StaticLayout.Builder.obtain(text, 0, text.length, paint, MAX_TEXT_W)
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
.setLineSpacing(0f, 1.05f)
.setIncludePad(false)
.build()
save()
translate(x, y)
layout.draw(this)
restore()
return y + layout.height + 6f
}
fun writeTrainingPlanPdf(
outFile: File,
clubName: String,
dateLabel: String,
timeLabel: String,
planItems: List<DiaryDateActivityItem>,
timeblockFallback: String,
) {
val doc = PdfDocument()
var pageSeq = 0
fun openPage(): PdfDocument.Page {
pageSeq++
return doc.startPage(PdfDocument.PageInfo.Builder(PAGE_W, PAGE_H, pageSeq).create())
}
var page = openPage()
var canvas = page.canvas
var y = MARGIN
val titlePaint = newTitlePaint()
val bodyPaint = newBodyPaint()
fun newPageIfNeeded(extra: Float) {
if (y + extra > PAGE_H - MARGIN) {
doc.finishPage(page)
page = openPage()
canvas = page.canvas
y = MARGIN
}
}
y = canvas.drawStatic("Trainingsplan $clubName", titlePaint, MARGIN, y)
y = canvas.drawStatic("Datum: $dateLabel", bodyPaint, MARGIN, y)
y = canvas.drawStatic("Zeiten: $timeLabel", bodyPaint, MARGIN, y)
y = canvas.drawStatic("", bodyPaint, MARGIN, y)
val sorted = planItems.sortedWith(compareBy({ it.groupId ?: 0 }, { it.orderId }, { it.id }))
sorted.forEach { item ->
val line = buildString {
append(item.displayTitle(timeblockFallback).ifBlank { "Eintrag ${item.id}" })
item.durationText?.takeIf { it.isNotBlank() }?.let { append("$it") }
?: item.duration?.let { append("${it} min") }
item.planGroup?.name?.takeIf { it.isNotBlank() }?.let { append(" — Gruppe: $it") }
}
newPageIfNeeded(40f)
y = canvas.drawStatic("$line", bodyPaint, MARGIN, y)
}
doc.finishPage(page)
FileOutputStream(outFile).use { doc.writeTo(it) }
doc.close()
}
fun writeTrainingDaySummaryPdf(
outFile: File,
clubName: String,
dateLabel: String,
timeLabel: String,
activeMembers: List<Member>,
participants: List<DiaryTrainingParticipant>,
freeform: List<DiaryFreeformActivity>,
planItems: List<DiaryDateActivityItem>,
timeblockFallback: String,
) {
val doc = PdfDocument()
var pageSeq = 0
fun openPage(): PdfDocument.Page {
pageSeq++
return doc.startPage(PdfDocument.PageInfo.Builder(PAGE_W, PAGE_H, pageSeq).create())
}
var page = openPage()
var canvas = page.canvas
var y = MARGIN
val titlePaint = newTitlePaint()
val bodyPaint = newBodyPaint()
fun newPageIfNeeded(extra: Float) {
if (y + extra > PAGE_H - MARGIN) {
doc.finishPage(page)
page = openPage()
canvas = page.canvas
y = MARGIN
}
}
y = canvas.drawStatic("Trainingstag $clubName", titlePaint, MARGIN, y)
y = canvas.drawStatic("Datum: $dateLabel", bodyPaint, MARGIN, y)
y = canvas.drawStatic("Zeiten: $timeLabel", bodyPaint, MARGIN, y)
y = canvas.drawStatic("", bodyPaint, MARGIN, y)
y = canvas.drawStatic("Teilnehmer (laut App-Stand)", titlePaint, MARGIN, y)
val partLines = activeMembers.map { m ->
val row = participants.find { it.memberId == m.id }
val base = "${m.lastName}, ${m.firstName}"
if (row == null) {
"$base — (keine Teilnahme-Zeile)"
} else if (row.isPresentParticipant()) {
"$base — anwesend"
} else {
when (row.attendanceStatus?.lowercase()) {
"excused" -> "$base — entschuldigt"
"cancelled" -> "$base — abgesagt"
else -> "$base — (${row.attendanceStatus ?: "?"})"
}
}
}
if (partLines.isEmpty()) {
y = canvas.drawStatic("", bodyPaint, MARGIN, y)
} else {
partLines.forEach { n ->
newPageIfNeeded(30f)
y = canvas.drawStatic("$n", bodyPaint, MARGIN, y)
}
}
y = canvas.drawStatic("", bodyPaint, MARGIN, y)
y = canvas.drawStatic("Weitere Tages-Aktivitäten", titlePaint, MARGIN, y)
if (freeform.isEmpty()) {
y = canvas.drawStatic("", bodyPaint, MARGIN, y)
} else {
freeform.forEach { f ->
newPageIfNeeded(30f)
y = canvas.drawStatic("${f.description}", bodyPaint, MARGIN, y)
}
}
y = canvas.drawStatic("", bodyPaint, MARGIN, y)
y = canvas.drawStatic("Trainingsplan (Aktivitäten)", titlePaint, MARGIN, y)
val sorted = planItems.sortedWith(compareBy({ it.groupId ?: 0 }, { it.orderId }, { it.id }))
if (sorted.isEmpty()) {
y = canvas.drawStatic("", bodyPaint, MARGIN, y)
} else {
sorted.forEach { item ->
newPageIfNeeded(40f)
val line = buildString {
append(item.displayTitle(timeblockFallback).ifBlank { "Eintrag ${item.id}" })
item.durationText?.takeIf { it.isNotBlank() }?.let { append("$it") }
?: item.duration?.let { append("${it} min") }
}
y = canvas.drawStatic("$line", bodyPaint, MARGIN, y)
}
}
doc.finishPage(page)
FileOutputStream(outFile).use { doc.writeTo(it) }
doc.close()
}

View File

@@ -0,0 +1,22 @@
package de.tt_tagebuch.app.pdf
import android.content.Context
import android.content.Intent
import androidx.core.content.FileProvider
import de.tt_tagebuch.app.BuildConfig
import java.io.File
fun sharePdfFile(context: Context, file: File, chooserTitle: String) {
val uri = FileProvider.getUriForFile(
context,
"${BuildConfig.APPLICATION_ID}.fileprovider",
file,
)
val send = Intent(Intent.ACTION_SEND).apply {
type = "application/pdf"
putExtra(Intent.EXTRA_STREAM, uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
val chooser = Intent.createChooser(send, chooserTitle).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(chooser)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,35 @@
package de.tt_tagebuch.app.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import coil.compose.AsyncImage
import coil.request.ImageRequest
import okhttp3.Headers
@Composable
fun AuthenticatedAsyncImage(
imageUrl: String,
authHeaders: Map<String, String>,
modifier: Modifier = Modifier,
contentDescription: String? = null,
) {
val context = LocalContext.current
val request = remember(imageUrl, authHeaders) {
val hb = Headers.Builder()
authHeaders.forEach { (k, v) -> hb.add(k, v) }
ImageRequest.Builder(context)
.data(imageUrl)
.headers(hb.build())
.crossfade(true)
.build()
}
AsyncImage(
model = request,
contentDescription = contentDescription,
modifier = modifier,
contentScale = ContentScale.Fit,
)
}

View File

@@ -0,0 +1,92 @@
package de.tt_tagebuch.app.ui
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.FileProvider
import com.yalantis.ucrop.UCrop
import java.io.File
private fun cropCacheDir(context: Context): File =
File(context.cacheDir, "member_crop").also { it.mkdirs() }
/**
* Kopiert die gewählte [Uri] in eine Cache-Datei (FileProvider), damit UCrop zuverlässig lesen kann.
*/
fun copyPickedImageToCacheForCrop(context: Context, source: Uri, memberId: Int): Uri {
val dir = cropCacheDir(context)
val inFile = File(dir, "pick-$memberId-${System.currentTimeMillis()}.jpg")
context.contentResolver.openInputStream(source)?.use { input ->
inFile.outputStream().use { output -> input.copyTo(output) }
}
return FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
inFile,
)
}
fun createCropOutputUri(context: Context, memberId: Int): Pair<File, Uri> {
val dir = cropCacheDir(context)
val outFile = File(dir, "crop-out-$memberId-${System.currentTimeMillis()}.jpg")
if (outFile.exists()) outFile.delete()
outFile.createNewFile()
val uri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
outFile,
)
return outFile to uri
}
fun buildMemberPortraitCropIntent(context: Context, sourceUri: Uri, destinationUri: Uri): Intent {
val options = UCrop.Options().apply {
setCompressionFormat(Bitmap.CompressFormat.JPEG)
setCompressionQuality(90)
setHideBottomControls(false)
setFreeStyleCropEnabled(true)
}
return UCrop.of(sourceUri, destinationUri)
.withAspectRatio(1f, 1f)
.withMaxResultSize(2048, 2048)
.withOptions(options)
.getIntent(context)
.apply {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
}
@Composable
fun rememberMemberPortraitCropLauncher(
onCropped: (ByteArray) -> Unit,
onCancelledOrError: (() -> Unit)? = null,
): ActivityResultLauncher<Intent> {
val context = LocalContext.current
val appContext = remember(context) { context.applicationContext }
return rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode != Activity.RESULT_OK || result.data == null) {
onCancelledOrError?.invoke()
return@rememberLauncherForActivityResult
}
val data = result.data!!
if (UCrop.getError(data) != null) {
onCancelledOrError?.invoke()
return@rememberLauncherForActivityResult
}
val outUri = UCrop.getOutput(data) ?: run {
onCancelledOrError?.invoke()
return@rememberLauncherForActivityResult
}
runCatching {
appContext.contentResolver.openInputStream(outUri)?.use { it.readBytes() }
}.getOrNull()?.let { onCropped(it) } ?: onCancelledOrError?.invoke()
}
}

View File

@@ -0,0 +1,28 @@
package de.tt_tagebuch.app.ui
import androidx.compose.material.MaterialTheme
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
/** Orientierung an `frontend/src/assets/css/main.scss` (:root). */
object TtAppColors {
val NavRailBackground = Color(0xFFEDF4F0)
}
private val TtLightColors = lightColors(
primary = Color(0xFF2F7A5F),
primaryVariant = Color(0xFF184636),
secondary = Color(0xFFB56E41),
background = Color(0xFFF4F6F3),
surface = Color(0xFFFFFFFF),
onPrimary = Color.White,
onSecondary = Color.White,
onBackground = Color(0xFF333333),
onSurface = Color(0xFF333333),
)
@Composable
fun TtTagebuchTheme(content: @Composable () -> Unit) {
MaterialTheme(colors = TtLightColors, content = content)
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name="pdf_cache" path="." />
</paths>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Lokales HTTP (Cleartext) für Entwicklung: ab API 28 sonst "CLEARTEXT ... not permitted". -->
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">10.0.2.2</domain>
<domain includeSubdomains="false">localhost</domain>
<domain includeSubdomains="false">127.0.0.1</domain>
</domain-config>
</network-security-config>

View File

@@ -10,6 +10,7 @@ import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.getScreenModel
import de.tt_tagebuch.app.viewmodel.DiaryScreenModel
@@ -18,6 +19,7 @@ import de.tt_tagebuch.app.viewmodel.DiaryState
class DiaryScreen(private val clubId: Int) : Screen {
@Composable
override fun Content() {
val navigator = LocalNavigator.current
val viewModel = getScreenModel<DiaryScreenModel>()
val state by viewModel.state.collectAsState()

View File

@@ -1,10 +1,13 @@
package de.tt_tagebuch.app.ui
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DateRange
import androidx.compose.material.icons.filled.Person
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
@@ -30,7 +33,7 @@ class HomeScreen(private val clubId: Int) : Screen {
}
}
) { padding ->
Box(modifier = androidx.compose.foundation.layout.padding(padding)) {
Box(modifier = Modifier.padding(padding)) {
when (selectedItem) {
0 -> DiaryScreen(clubId).Content()
1 -> MemberScreen(clubId).Content()

View File

@@ -6,6 +6,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Email
import androidx.compose.material.icons.filled.Phone
import androidx.compose.runtime.*
@@ -35,7 +36,7 @@ class MemberDetailScreen(private val member: Member) : Screen {
IconButton(onClick = {
navigator?.push(MemberEditScreen(member.clubId, member))
}) {
Icon(androidx.compose.material.icons.filled.Edit, contentDescription = "Edit")
Icon(Icons.Default.Edit, contentDescription = "Edit")
}
}
)
@@ -57,7 +58,7 @@ class MemberDetailScreen(private val member: Member) : Screen {
}
if (member.testMembership) {
Spacer(Modifier.width(8.dp))
StatusBadge("Test", Color.Orange)
StatusBadge("Test", Color(0xFFFFA500))
}
}

View File

@@ -34,7 +34,7 @@ class MemberEditScreen(private val clubId: Int, private val member: Member? = nu
var gender by remember { mutableStateOf(member?.gender ?: "unknown") }
var active by remember { mutableStateOf(member?.active ?: true) }
var testMembership by remember { mutableStateOf(member?.testMembership ?: true) }
var contacts by remember { mutableStateOf(member?.contacts?.toMutableList() ?: mutableListOf()) }
var contacts by remember { mutableStateOf<List<MemberContact>>(member?.contacts?.toMutableList() ?: mutableListOf<MemberContact>()) }
Scaffold(
topBar = {

View File

@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.*
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@@ -67,6 +68,7 @@ class MemberScreen(private val clubId: Int) : Screen {
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun MemberItem(member: de.tt_tagebuch.shared.models.Member, onClick: () -> Unit) {
ListItem(