From 0d66a704f130666e6f6c055962c6a4a4b3eb047a Mon Sep 17 00:00:00 2001 From: Frederik Kischewski Date: Sun, 15 Mar 2026 09:23:27 +0100 Subject: [PATCH 01/12] Fix mobile UI bugs: keyboard handling, layout, and iOS share crash - Fix white spots behind rounded keyboard corners on Android by setting window background color to match app theme - Add keyboard dismiss on tap outside for Login, Register, NewEventPage, NewParticipant, and RecipeListScreen using pointerInput/detectTapGestures - Fix Register password fields: first field uses ImeAction.Next, second field closes keyboard on Done - Add Done action to event name field on NewEventPage - Fix CookingGroups button height by removing IntrinsicSize.Min - Change Row to FlowRow for buttons (shopping/material/recipe plan and participant/cooking groups) to support narrow screens - Fix iOS share crash by dispatching UIActivityViewController on main queue - Make date input fields read-only on mobile platforms Co-Authored-By: Claude Opus 4.6 --- .../kotlin/org/futterbock/app/MainActivity.kt | 1 + .../admin/new_participant/NewParicipant.kt | 12 ++++- .../view/event/new_event/NewEventPage.kt | 49 ++++++++++++------- .../event/recipe_list/RecipeListScreen.kt | 8 +++ .../src/commonMain/kotlin/view/login/Login.kt | 13 +++-- .../kotlin/view/login/PasswordTextField.kt | 5 +- .../commonMain/kotlin/view/login/Register.kt | 22 +++++++-- .../date/dateinputfield/DateInputField.kt | 5 +- .../services/pdfService/PdfService.native.kt | 23 +++++---- 9 files changed, 99 insertions(+), 39 deletions(-) diff --git a/composeApp/src/androidMain/kotlin/org/futterbock/app/MainActivity.kt b/composeApp/src/androidMain/kotlin/org/futterbock/app/MainActivity.kt index a3eb13dc..52f5fda3 100644 --- a/composeApp/src/androidMain/kotlin/org/futterbock/app/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/org/futterbock/app/MainActivity.kt @@ -21,6 +21,7 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() + window.decorView.setBackgroundColor(android.graphics.Color.parseColor("#F9FAF7")) FirebaseApp.initializeApp(this) setContent { App(PdfServiceImpl(this)) diff --git a/composeApp/src/commonMain/kotlin/view/admin/new_participant/NewParicipant.kt b/composeApp/src/commonMain/kotlin/view/admin/new_participant/NewParicipant.kt index cf9ec236..93162806 100644 --- a/composeApp/src/commonMain/kotlin/view/admin/new_participant/NewParicipant.kt +++ b/composeApp/src/commonMain/kotlin/view/admin/new_participant/NewParicipant.kt @@ -1,7 +1,8 @@ package view.admin.new_participant -import androidx.compose.foundation.clickable import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Box @@ -40,6 +41,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -106,10 +109,15 @@ fun NewParicipant( availableGroups: Set ) { + val focusManager = LocalFocusManager.current + AppTheme { Scaffold( contentColor = MaterialTheme.colorScheme.onBackground, - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize() + .pointerInput(Unit) { + detectTapGestures(onTap = { focusManager.clearFocus() }) + }, topBar = { TopAppBar(title = { Text(text = "Teilnehmende hinzufügen") diff --git a/composeApp/src/commonMain/kotlin/view/event/new_event/NewEventPage.kt b/composeApp/src/commonMain/kotlin/view/event/new_event/NewEventPage.kt index 4542276e..3a67e424 100644 --- a/composeApp/src/commonMain/kotlin/view/event/new_event/NewEventPage.kt +++ b/composeApp/src/commonMain/kotlin/view/event/new_event/NewEventPage.kt @@ -5,10 +5,15 @@ import CategorizedShoppingListViewModel import MaterialListViewModel import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -49,6 +54,9 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController @@ -97,18 +105,26 @@ fun NewEventScreen( ) } +@OptIn(ExperimentalLayoutApi::class) @Composable fun NewEventPage( sharedState: ResultState, onAction: (BaseAction) -> Unit ) { - Scaffold(topBar = { - TopBarEventPage( - onAction = onAction, - sharedState = sharedState - ) - }) { + val focusManager = LocalFocusManager.current + + Scaffold( + modifier = Modifier.pointerInput(Unit) { + detectTapGestures(onTap = { focusManager.clearFocus() }) + }, + topBar = { + TopBarEventPage( + onAction = onAction, + sharedState = sharedState + ) + } + ) { Surface( color = MaterialTheme.colorScheme.background, contentColor = MaterialTheme.colorScheme.onBackground, @@ -133,14 +149,15 @@ fun NewEventPage( onAction(EditEventActions.ChangeEventName(value)) }, label = { Text("Name:") }, - modifier = Modifier.padding(8.dp) + modifier = Modifier.padding(8.dp), + singleLine = true, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }) ) } - Row( + FlowRow( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { ParticipantNumberTextField(sharedState, onAction) CookingGroupsButton(onAction) @@ -255,7 +272,7 @@ private fun CookingGroupsButton( onClick = { onAction(NavigationActions.GoToRoute(Routes.CookingGroups)) }, - modifier = Modifier.padding(8.dp).height(IntrinsicSize.Min), + modifier = Modifier.padding(8.dp), ) { Icon( imageVector = Icons.Default.Group, @@ -266,6 +283,7 @@ private fun CookingGroupsButton( } } +@OptIn(ExperimentalLayoutApi::class) @Composable private fun ShoppingAndMaterialList( onAction: (BaseAction) -> Unit, @@ -275,8 +293,7 @@ private fun ShoppingAndMaterialList( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 32.dp) ) { - Row( - verticalAlignment = Alignment.CenterVertically, + FlowRow( horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth() ) { @@ -289,8 +306,7 @@ private fun ShoppingAndMaterialList( ) ) }, - modifier = Modifier.padding(8.dp).height(IntrinsicSize.Min) - .align(Alignment.CenterVertically), + modifier = Modifier.padding(8.dp).height(IntrinsicSize.Min), colors = ButtonColors( contentColor = MaterialTheme.colorScheme.onPrimary, containerColor = MaterialTheme.colorScheme.primary, @@ -315,8 +331,7 @@ private fun ShoppingAndMaterialList( ) ) }, - modifier = Modifier.padding(8.dp).height(IntrinsicSize.Min) - .align(Alignment.CenterVertically), + modifier = Modifier.padding(8.dp).height(IntrinsicSize.Min), border = BorderStroke( width = 2.dp, color = MaterialTheme.colorScheme.primary diff --git a/composeApp/src/commonMain/kotlin/view/event/recipe_list/RecipeListScreen.kt b/composeApp/src/commonMain/kotlin/view/event/recipe_list/RecipeListScreen.kt index 36c8ef7c..0c015165 100644 --- a/composeApp/src/commonMain/kotlin/view/event/recipe_list/RecipeListScreen.kt +++ b/composeApp/src/commonMain/kotlin/view/event/recipe_list/RecipeListScreen.kt @@ -1,6 +1,7 @@ package view.event.recipe_list import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -10,6 +11,8 @@ import androidx.compose.material.icons.filled.Search import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController @@ -54,7 +57,12 @@ fun RecipeListScreen(navController: NavHostController) { var selectedSkillLevelFilter by remember { mutableStateOf(null) } var selectedIngredientFilters by remember { mutableStateOf(setOf()) } + val focusManager = LocalFocusManager.current + Scaffold( + modifier = Modifier.pointerInput(Unit) { + detectTapGestures(onTap = { focusManager.clearFocus() }) + }, topBar = { RecipeListTopBar( searchText = searchText, diff --git a/composeApp/src/commonMain/kotlin/view/login/Login.kt b/composeApp/src/commonMain/kotlin/view/login/Login.kt index ed43fa13..a25c16f0 100644 --- a/composeApp/src/commonMain/kotlin/view/login/Login.kt +++ b/composeApp/src/commonMain/kotlin/view/login/Login.kt @@ -3,6 +3,7 @@ package view.login import EmailTextField import androidx.compose.foundation.Image import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -30,6 +31,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.autofill.AutofillNode import androidx.compose.ui.autofill.AutofillType import androidx.compose.ui.composed @@ -72,8 +74,14 @@ fun LoginScreen( val scope = rememberCoroutineScope() val loginService: LoginAndRegister = koinInject() + val focusManager = LocalFocusManager.current + Scaffold( - modifier = Modifier.fillMaxSize().imePadding(), + modifier = Modifier.fillMaxSize() + .pointerInput(Unit) { + detectTapGestures(onTap = { focusManager.clearFocus() }) + } + .imePadding(), ) { LoginContent( onPasswordChange = { value: String -> password = value }, @@ -120,8 +128,7 @@ fun LoginContent( Column( modifier = Modifier.fillMaxSize().navigationBarsPadding().imePadding() - .verticalScroll(rememberScrollState()).padding(16.dp) - .clickable { focusManager.clearFocus() }, + .verticalScroll(rememberScrollState()).padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { diff --git a/composeApp/src/commonMain/kotlin/view/login/PasswordTextField.kt b/composeApp/src/commonMain/kotlin/view/login/PasswordTextField.kt index d0a50350..20374d36 100644 --- a/composeApp/src/commonMain/kotlin/view/login/PasswordTextField.kt +++ b/composeApp/src/commonMain/kotlin/view/login/PasswordTextField.kt @@ -32,6 +32,8 @@ fun PasswordTextField( onPasswordChange: (value: String) -> Unit, loading: Boolean, onDone: () -> Unit = {}, + onNext: () -> Unit = {}, + imeAction: ImeAction = ImeAction.Done, passwordName: String = "Password" ) { var passwordVisibility by remember { mutableStateOf(false) } @@ -52,11 +54,12 @@ fun PasswordTextField( keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Password, - imeAction = ImeAction.Done, + imeAction = imeAction, ), keyboardActions = KeyboardActions( onDone = { onDone() }, + onNext = { onNext() }, ), trailingIcon = { IconButton(onClick = { diff --git a/composeApp/src/commonMain/kotlin/view/login/Register.kt b/composeApp/src/commonMain/kotlin/view/login/Register.kt index 7b021d76..ecde1b41 100644 --- a/composeApp/src/commonMain/kotlin/view/login/Register.kt +++ b/composeApp/src/commonMain/kotlin/view/login/Register.kt @@ -2,7 +2,8 @@ package view.login import EmailTextField import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -38,6 +39,7 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import co.touchlab.kermit.Logger +import dev.gitlive.firebase.auth.FirebaseAuthEmailException import dev.gitlive.firebase.auth.FirebaseAuthInvalidCredentialsException import dev.gitlive.firebase.auth.FirebaseAuthInvalidUserException import dev.gitlive.firebase.auth.FirebaseAuthUserCollisionException @@ -102,14 +104,17 @@ fun Register( onRegisterNavigation() } catch (e: FirebaseAuthInvalidCredentialsException) { registerError = - "Fehler beim Registrieren: Bitte geben Sie eine valide email an." + "Fehler beim Registrieren: Bitte geben Sie eine valide Email-Adresse an." } catch (e: FirebaseAuthUserCollisionException) { registerError = "Fehler beim Registrieren: Der Username existiert bereits. Bitte verwenden Sie einen anderen Usernamen." } catch (e: FirebaseAuthInvalidUserException) { registerError = "Fehler beim Registrieren: Der Username existiert bereits. Bitte verwenden Sie einen anderen Usernamen." - } catch (e: Exception) { + } catch (e: FirebaseAuthEmailException){ + registerError = "Bitte geben Sie eine valide Email-Adresse an." + } + catch (e: Exception) { Logger.e(e.stackTraceToString()) registerError = "Ein unbekannter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut: " @@ -119,7 +124,11 @@ fun Register( } } - Scaffold(modifier = Modifier.fillMaxSize().imePadding(), topBar = { + Scaffold(modifier = Modifier.fillMaxSize() + .pointerInput(Unit) { + detectTapGestures(onTap = { focusManager.clearFocus() }) + } + .imePadding(), topBar = { TopAppBar( title = { Text("Registrieren") }, navigationIcon = { NavigationIconButton(onLeave = onBackNavigation) }) @@ -156,13 +165,16 @@ fun Register( password = password, onPasswordChange = { value: String -> password = value }, loading = loading, + imeAction = ImeAction.Next, + onNext = { focusManager.moveFocus(FocusDirection.Down) }, ) Spacer(modifier = Modifier.padding(4.dp)) PasswordTextField( password = passwordConfirm, onPasswordChange = { value: String -> passwordConfirm = value }, loading = loading, - passwordName = "Passwort bestätigen" + passwordName = "Passwort bestätigen", + onDone = { focusManager.clearFocus() }, ) Spacer(modifier = Modifier.padding(8.dp)) ErrorField(errorMessage = registerError) diff --git a/composeApp/src/commonMain/kotlin/view/shared/date/dateinputfield/DateInputField.kt b/composeApp/src/commonMain/kotlin/view/shared/date/dateinputfield/DateInputField.kt index 1d8a8886..28511c82 100644 --- a/composeApp/src/commonMain/kotlin/view/shared/date/dateinputfield/DateInputField.kt +++ b/composeApp/src/commonMain/kotlin/view/shared/date/dateinputfield/DateInputField.kt @@ -18,6 +18,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import getPlatformName import kotlinx.datetime.Instant import view.shared.HelperFunctions import view.shared.date.FutureOrPresentSelectableDates @@ -50,7 +51,7 @@ fun DateInputField( isError = error.isNotEmpty(), supportingText = { Text(error, color = MaterialTheme.colorScheme.error) }, singleLine = true, - readOnly = !isInputFieldEditable, + readOnly = !isInputFieldEditable || getPlatformName() != "desktop", value = text, trailingIcon = trailingIcon, onValueChange = {}, @@ -59,4 +60,4 @@ fun DateInputField( .padding(8.dp).height(IntrinsicSize.Min), visualTransformation = DateTransformation() ) -} \ No newline at end of file +} diff --git a/composeApp/src/nativeMain/kotlin/services/pdfService/PdfService.native.kt b/composeApp/src/nativeMain/kotlin/services/pdfService/PdfService.native.kt index bd0e66c1..85114947 100644 --- a/composeApp/src/nativeMain/kotlin/services/pdfService/PdfService.native.kt +++ b/composeApp/src/nativeMain/kotlin/services/pdfService/PdfService.native.kt @@ -9,6 +9,8 @@ import model.Meal import model.MultiDayShoppingList import model.ShoppingIngredient import services.pdfService.RecipePlanPdfProcessor +import platform.darwin.dispatch_async +import platform.darwin.dispatch_get_main_queue import platform.CoreGraphics.CGRect import platform.CoreGraphics.CGRectMake import platform.Foundation.NSAttributedString @@ -255,15 +257,18 @@ actual class PdfServiceImpl { return } - // Create share activity - val activityViewController = UIActivityViewController( - activityItems = listOf(pdfFileURL), - applicationActivities = null - ) - - // Present the share sheet from the root view controller - val currentViewController = UIApplication.sharedApplication.keyWindow?.rootViewController - currentViewController?.presentViewController(activityViewController, true, null) + // UIKit presentation must happen on the main queue + dispatch_async(dispatch_get_main_queue()) { + // Create share activity + val activityViewController = UIActivityViewController( + activityItems = listOf(pdfFileURL), + applicationActivities = null + ) + + // Present the share sheet from the root view controller + val currentViewController = UIApplication.sharedApplication.keyWindow?.rootViewController + currentViewController?.presentViewController(activityViewController, true, null) + } } @OptIn(ExperimentalForeignApi::class, BetaInteropApi::class) From e664326e3b4b65be461a532ccbe61b040ee19a8f Mon Sep 17 00:00:00 2001 From: Frederik Kischewski Date: Sun, 15 Mar 2026 09:57:29 +0100 Subject: [PATCH 02/12] Fix FAB sizing, participant list scrolling, and typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FABs now use IntrinsicSize.Max with widthIn(max=400.dp) for consistent sizing across mobile and desktop - Participant search list is now scrollable with bottom padding for FABs - FABs in ParticipantSearchBar moved into Box overlay within SearchBar content so they remain visible over expanded search - Keyboard search action now hides keyboard instead of closing search - Fix typo: Teilnehmendeliste → Teilnehmendenliste Co-Authored-By: Claude Opus 4.6 --- .../view/event/new_meal_screen/NewMealPage.kt | 54 +++--- .../event/participants/ParticipantPage.kt | 2 +- .../participants/ParticipantSearchBar.kt | 178 +++++++++--------- 3 files changed, 112 insertions(+), 122 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/view/event/new_meal_screen/NewMealPage.kt b/composeApp/src/commonMain/kotlin/view/event/new_meal_screen/NewMealPage.kt index bec2ef46..dc351931 100644 --- a/composeApp/src/commonMain/kotlin/view/event/new_meal_screen/NewMealPage.kt +++ b/composeApp/src/commonMain/kotlin/view/event/new_meal_screen/NewMealPage.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -13,9 +14,11 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.clickable import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.draw.clip import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -47,7 +50,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -167,6 +169,7 @@ fun NewMealPage( Column( modifier = Modifier.padding(top = it.calculateTopPadding()).padding(8.dp) .verticalScroll(rememberScrollState()).fillMaxHeight() + .padding(bottom = 165.dp) ) { when (state) { is ResultState.Success -> { @@ -436,27 +439,21 @@ fun ActionButtons( ) { Column( verticalArrangement = Arrangement.spacedBy(12.dp), - horizontalAlignment = Alignment.End + horizontalAlignment = Alignment.End, + modifier = Modifier.width(IntrinsicSize.Max).widthIn(max = 400.dp) ) { ExtendedFloatingActionButton( onClick = onOpenSearch, - modifier = Modifier - .width(400.dp) - .clip(shape = RoundedCornerShape(75)), + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(75), containerColor = MaterialTheme.colorScheme.secondary, ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Start, - modifier = Modifier.fillMaxWidth() - ) { - Icon( - imageVector = Icons.Default.Search, - contentDescription = "Rezept hinzufügen" - ) - Spacer(modifier = Modifier.width(8.dp)) - Text("Rezept hinzufügen") - } + Icon( + imageVector = Icons.Default.Search, + contentDescription = "Rezept hinzufügen" + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("Rezept hinzufügen") } ExtendedFloatingActionButton( @@ -467,23 +464,16 @@ fun ActionButtons( ) ) }, - modifier = Modifier - .width(400.dp) - .clip(shape = RoundedCornerShape(75)), + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(75), containerColor = MaterialTheme.colorScheme.primary, ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Start, - modifier = Modifier.fillMaxWidth() - ) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = "Add Icon" - ) - Spacer(modifier = Modifier.width(8.dp)) - Text("Weitere Mahlzeit anlegen") - } + Icon( + imageVector = Icons.Default.Add, + contentDescription = "Add Icon" + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("Weitere Mahlzeit anlegen") } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/view/event/participants/ParticipantPage.kt b/composeApp/src/commonMain/kotlin/view/event/participants/ParticipantPage.kt index ce9de7e4..95f15821 100644 --- a/composeApp/src/commonMain/kotlin/view/event/participants/ParticipantPage.kt +++ b/composeApp/src/commonMain/kotlin/view/event/participants/ParticipantPage.kt @@ -76,7 +76,7 @@ fun ParticipantPage( Scaffold( topBar = { TopAppBar( - title = { Text("Teilnehmendeliste") }, + title = { Text("Teilnehmendenliste") }, navigationIcon = { NavigationIconButton( onLeave = { diff --git a/composeApp/src/commonMain/kotlin/view/event/participants/ParticipantSearchBar.kt b/composeApp/src/commonMain/kotlin/view/event/participants/ParticipantSearchBar.kt index 47d0a156..dfde0936 100644 --- a/composeApp/src/commonMain/kotlin/view/event/participants/ParticipantSearchBar.kt +++ b/composeApp/src/commonMain/kotlin/view/event/participants/ParticipantSearchBar.kt @@ -8,11 +8,17 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material.icons.filled.Add @@ -38,6 +44,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController @@ -93,6 +100,7 @@ fun ParticipantSearchBar( var searchText by remember { mutableStateOf("") } var active by remember { mutableStateOf(true) } var participantsAddedInThisStep by remember { mutableStateOf(listOf()) } + val keyboardController = LocalSoftwareKeyboardController.current Scaffold( @@ -107,8 +115,7 @@ fun ParticipantSearchBar( query = searchText, onQueryChange = { searchText = it }, onSearch = { - active = false - // Optional: handle search + keyboardController?.hide() }, expanded = active, onExpandedChange = { active = it }, @@ -159,7 +166,6 @@ fun ParticipantSearchBar( }, onClick = { participantsAddedInThisStep = participantsAddedInThisStep.filter { p -> p.uid != it.uid } - // Find matching ParticipantTime in event state val participantTime = (state as? ResultState.Success)?.data?.participantList?.find { pt -> pt.participantRef == it.uid } if (participantTime != null) { onAction( @@ -179,7 +185,6 @@ fun ParticipantSearchBar( .padding(start = 4.dp) .clickable { participantsAddedInThisStep = participantsAddedInThisStep.filter { p -> p.uid != it.uid } - // Find matching ParticipantTime in event state val participantTime = (state as? ResultState.Success)?.data?.participantList?.find { pt -> pt.participantRef == it.uid } if (participantTime != null) { onAction( @@ -195,99 +200,49 @@ fun ParticipantSearchBar( ) } } - when (allParticipants) { - is ResultState.Success -> { - getSelectableParticipants( - allParticipants = allParticipants.data.allParticipants, - participantsOfEvent = state.data.participantList - ).filter { - it.firstName.lowercase().contains(searchText.lowercase()) || - it.lastName.lowercase() - .contains(searchText.lowercase()) || - (it.firstName.lowercase() + " " + it.lastName.lowercase()).contains( - searchText.lowercase() - ) - }.sortedBy { it.firstName }.forEach { - Row( - modifier = Modifier.padding(16.dp).clickable { - searchText = "" - participantsAddedInThisStep = participantsAddedInThisStep + it - onAction(EditParticipantActions.AddParticipant(it)) + Box(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier.verticalScroll(rememberScrollState()) + .padding(bottom = 165.dp) + ) { + when (allParticipants) { + is ResultState.Success -> { + getSelectableParticipants( + allParticipants = allParticipants.data.allParticipants, + participantsOfEvent = state.data.participantList + ).filter { + it.firstName.lowercase().contains(searchText.lowercase()) || + it.lastName.lowercase() + .contains(searchText.lowercase()) || + (it.firstName.lowercase() + " " + it.lastName.lowercase()).contains( + searchText.lowercase() + ) + }.sortedBy { it.firstName }.forEach { + Row( + modifier = Modifier.padding(16.dp).clickable { + searchText = "" + participantsAddedInThisStep = + participantsAddedInThisStep + it + onAction(EditParticipantActions.AddParticipant(it)) + } + ) { + Text(text = it.firstName.trim() + " " + it.lastName.trim()) + } + HorizontalDivider() } - - ) { - Text(text = it.firstName.trim() + " " + it.lastName.trim()) } - HorizontalDivider() + + else -> {} } } - - else -> {} - } - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.BottomEnd - ) { - Column( - horizontalAlignment = Alignment.End, + Box( + modifier = Modifier.align(Alignment.BottomEnd) + .padding(16.dp) ) { - ExtendedFloatingActionButton( - onClick = { - onAction(ActionsNewParticipant.InitWithoutParticipant) - onAction(NavigationActions.GoToRoute(Routes.CreateOrEditParticipant)) - }, - modifier = Modifier.padding(bottom = 16.dp) - .width(400.dp) - .clip(shape = RoundedCornerShape(75)), // Limit the width to prevent stretching, - containerColor = MaterialTheme.colorScheme.onPrimary, - - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = "Add Icon" - ) - Text( - text = "Teilnehmende anlegen", - modifier = Modifier.padding(start = 8.dp) - ) - } - } - ExtendedFloatingActionButton( - onClick = { - onAction(NavigationActions.GoBack) - }, - modifier = Modifier.padding(bottom = 16.dp) - .width(400.dp) - .clip(shape = RoundedCornerShape(75)), // Limit the width to prevent stretching, - elevation = FloatingActionButtonDefaults.elevation(16.dp), - containerColor = MaterialTheme.colorScheme.primary - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowForward, - contentDescription = "Add Icon" - ) - Text( - text = "Teilnehmende übernehmen", - modifier = Modifier.padding(start = 8.dp) - ) - } - } + ParticipantActionButtons(onAction = onAction) } } } - - //elevation = AppBarDefaults.TopAppBarElevation - } is ResultState.Error -> ErrorField(errorMessage = state.message) @@ -298,4 +253,49 @@ fun ParticipantSearchBar( } +} + +@Composable +fun ParticipantActionButtons( + onAction: (BaseAction) -> Unit +) { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalAlignment = Alignment.End, + modifier = Modifier.width(IntrinsicSize.Max).widthIn(max = 400.dp) + ) { + ExtendedFloatingActionButton( + onClick = { + onAction(ActionsNewParticipant.InitWithoutParticipant) + onAction(NavigationActions.GoToRoute(Routes.CreateOrEditParticipant)) + }, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(75), + containerColor = MaterialTheme.colorScheme.onPrimary, + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = "Add Icon" + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("Teilnehmende anlegen") + } + + ExtendedFloatingActionButton( + onClick = { + onAction(NavigationActions.GoBack) + }, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(75), + elevation = FloatingActionButtonDefaults.elevation(16.dp), + containerColor = MaterialTheme.colorScheme.primary + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowForward, + contentDescription = "Übernehmen" + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("Teilnehmende übernehmen") + } + } } \ No newline at end of file From 53fecb6ff69c91218d7f0d69e2dc968ccc5a2dbf Mon Sep 17 00:00:00 2001 From: Frederik Kischewski Date: Sun, 15 Mar 2026 10:31:40 +0100 Subject: [PATCH 03/12] Fix iOS keyboard corners, recipe layout, date picker, and participant form - Fix white corners at keyboard edges on iOS login/register by removing duplicate imePadding() (was on both Scaffold and Column) - Remove hardcoded setBackgroundColor from Android MainActivity - Fix recipe name overflow squishing delete icon (use weight modifier) - Fix DatePicker crash: use dynamic year range instead of hardcoded 2025 - Add keyboard actions (Next/Done) to participant name fields - Fix birthday field crash on iOS (use LaunchedEffect for side effect) - Localize allergy picker search placeholder to German - Enable multiSelect for allergy picker dialog - Add imePadding to participant search bar Co-Authored-By: Claude Opus 4.6 --- .../kotlin/org/futterbock/app/MainActivity.kt | 1 - .../new_participant/IngredientPickerDialog.kt | 4 +-- .../admin/new_participant/NewParicipant.kt | 27 ++++++++++++++++--- .../view/event/new_meal_screen/NewMealPage.kt | 7 +++-- .../participants/ParticipantSearchBar.kt | 4 ++- .../src/commonMain/kotlin/view/login/Login.kt | 3 +-- .../commonMain/kotlin/view/login/Register.kt | 3 +-- .../view/shared/date/DatePickerDialog.kt | 4 ++- 8 files changed, 36 insertions(+), 17 deletions(-) diff --git a/composeApp/src/androidMain/kotlin/org/futterbock/app/MainActivity.kt b/composeApp/src/androidMain/kotlin/org/futterbock/app/MainActivity.kt index 52f5fda3..a3eb13dc 100644 --- a/composeApp/src/androidMain/kotlin/org/futterbock/app/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/org/futterbock/app/MainActivity.kt @@ -21,7 +21,6 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() - window.decorView.setBackgroundColor(android.graphics.Color.parseColor("#F9FAF7")) FirebaseApp.initializeApp(this) setContent { App(PdfServiceImpl(this)) diff --git a/composeApp/src/commonMain/kotlin/view/admin/new_participant/IngredientPickerDialog.kt b/composeApp/src/commonMain/kotlin/view/admin/new_participant/IngredientPickerDialog.kt index 77332548..7637ff13 100644 --- a/composeApp/src/commonMain/kotlin/view/admin/new_participant/IngredientPickerDialog.kt +++ b/composeApp/src/commonMain/kotlin/view/admin/new_participant/IngredientPickerDialog.kt @@ -121,7 +121,7 @@ private fun IngredientPickerContent( ), placeholder = { Text( - text = "Search", + text = "Suchen", style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -129,7 +129,7 @@ private fun IngredientPickerContent( leadingIcon = { Icon( imageVector = Icons.Default.Search, - contentDescription = "Search" + contentDescription = "Suchen" ) }, trailingIcon = { diff --git a/composeApp/src/commonMain/kotlin/view/admin/new_participant/NewParicipant.kt b/composeApp/src/commonMain/kotlin/view/admin/new_participant/NewParicipant.kt index 93162806..e7068b82 100644 --- a/composeApp/src/commonMain/kotlin/view/admin/new_participant/NewParicipant.kt +++ b/composeApp/src/commonMain/kotlin/view/admin/new_participant/NewParicipant.kt @@ -15,6 +15,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Edit @@ -34,6 +36,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -42,7 +45,9 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -140,7 +145,12 @@ fun NewParicipant( onAction(ActionsNewParticipant.ChangeFirstName(it)) }, label = { Text("Vorname:") }, - modifier = Modifier.padding(8.dp) + modifier = Modifier.padding(8.dp), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + keyboardActions = KeyboardActions(onNext = { + focusManager.moveFocus(FocusDirection.Down) + }), + singleLine = true ) } @@ -149,7 +159,12 @@ fun NewParicipant( value = state.data.lastName, onValueChange = { onAction(ActionsNewParticipant.ChangeLastName(it)) }, label = { Text("Nachname:") }, - modifier = Modifier.padding(8.dp) + modifier = Modifier.padding(8.dp), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { + focusManager.clearFocus() + }), + singleLine = true ) } Row { @@ -260,6 +275,7 @@ private fun AllergySelection( selectedIngredients = state.allergies, onSelected = { onAction(ActionsNewParticipant.AddOrRemoveAllergy(allergy = it.uid)) }, onDismiss = { showDialog = false }, + multiSelect = true ) } } @@ -309,9 +325,12 @@ private fun BirthdaySelectionField( onAction: (ActionsNewParticipant) -> Unit ) { val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() - if (interactionSource.collectIsPressedAsState().value) { - onAction(ActionsNewParticipant.ShowDatePicker) + LaunchedEffect(isPressed) { + if (isPressed) { + onAction(ActionsNewParticipant.ShowDatePicker) + } } DateInputField( diff --git a/composeApp/src/commonMain/kotlin/view/event/new_meal_screen/NewMealPage.kt b/composeApp/src/commonMain/kotlin/view/event/new_meal_screen/NewMealPage.kt index dc351931..52bb0564 100644 --- a/composeApp/src/commonMain/kotlin/view/event/new_meal_screen/NewMealPage.kt +++ b/composeApp/src/commonMain/kotlin/view/event/new_meal_screen/NewMealPage.kt @@ -189,21 +189,20 @@ fun NewMealPage( ) { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center + modifier = Modifier.padding(start = 8.dp) ) { Icon( imageVector = Icons.Default.Info, contentDescription = "Rezept ansehen", - modifier = Modifier.padding(start = 16.dp).clickable { + modifier = Modifier.padding(start = 8.dp).clickable { navigateToRecipe(onAction, it) }) Text( "${it.selectedRecipeName} (${it.eaterIds.size + it.guestCount} ${if (it.eaterIds.size + it.guestCount == 1) "Person" else "Personen"})", style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(8.dp).clickable { + modifier = Modifier.weight(1f).padding(8.dp).clickable { navigateToRecipe(onAction, it) }, - textAlign = TextAlign.Center ) IconButton( onClick = { diff --git a/composeApp/src/commonMain/kotlin/view/event/participants/ParticipantSearchBar.kt b/composeApp/src/commonMain/kotlin/view/event/participants/ParticipantSearchBar.kt index dfde0936..9b9a1f2a 100644 --- a/composeApp/src/commonMain/kotlin/view/event/participants/ParticipantSearchBar.kt +++ b/composeApp/src/commonMain/kotlin/view/event/participants/ParticipantSearchBar.kt @@ -12,6 +12,8 @@ import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width @@ -200,7 +202,7 @@ fun ParticipantSearchBar( ) } } - Box(modifier = Modifier.fillMaxSize()) { + Box(modifier = Modifier.fillMaxSize().navigationBarsPadding().imePadding()) { Column( modifier = Modifier.verticalScroll(rememberScrollState()) .padding(bottom = 165.dp) diff --git a/composeApp/src/commonMain/kotlin/view/login/Login.kt b/composeApp/src/commonMain/kotlin/view/login/Login.kt index a25c16f0..52bbf04d 100644 --- a/composeApp/src/commonMain/kotlin/view/login/Login.kt +++ b/composeApp/src/commonMain/kotlin/view/login/Login.kt @@ -80,8 +80,7 @@ fun LoginScreen( modifier = Modifier.fillMaxSize() .pointerInput(Unit) { detectTapGestures(onTap = { focusManager.clearFocus() }) - } - .imePadding(), + }, ) { LoginContent( onPasswordChange = { value: String -> password = value }, diff --git a/composeApp/src/commonMain/kotlin/view/login/Register.kt b/composeApp/src/commonMain/kotlin/view/login/Register.kt index ecde1b41..09a7bcd1 100644 --- a/composeApp/src/commonMain/kotlin/view/login/Register.kt +++ b/composeApp/src/commonMain/kotlin/view/login/Register.kt @@ -127,8 +127,7 @@ fun Register( Scaffold(modifier = Modifier.fillMaxSize() .pointerInput(Unit) { detectTapGestures(onTap = { focusManager.clearFocus() }) - } - .imePadding(), topBar = { + }, topBar = { TopAppBar( title = { Text("Registrieren") }, navigationIcon = { NavigationIconButton(onLeave = onBackNavigation) }) diff --git a/composeApp/src/commonMain/kotlin/view/shared/date/DatePickerDialog.kt b/composeApp/src/commonMain/kotlin/view/shared/date/DatePickerDialog.kt index 38463e64..8d9feca4 100644 --- a/composeApp/src/commonMain/kotlin/view/shared/date/DatePickerDialog.kt +++ b/composeApp/src/commonMain/kotlin/view/shared/date/DatePickerDialog.kt @@ -12,6 +12,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import kotlinx.datetime.Clock +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -20,7 +22,7 @@ fun DatePickerDialog( ) { val datePickerState = rememberDatePickerState( initialSelectedDateMillis = Clock.System.now().toEpochMilliseconds(), - yearRange = IntRange(1950, 2025), + yearRange = IntRange(1950, Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).year), initialDisplayMode = DisplayMode.Picker, selectableDates = PastOrPresentSelectableDates ) From 301457f81ec974ad578a7a127d87ebf472dc41c5 Mon Sep 17 00:00:00 2001 From: Frederik Kischewski Date: Sun, 15 Mar 2026 11:00:51 +0100 Subject: [PATCH 04/12] Handle deleted recipe gracefully instead of crashing - Make getRecipeById return nullable Recipe? - Check doc.exists before deserializing in FireBaseRepository - Show error state instead of crash when recipe was deleted Co-Authored-By: Claude Opus 4.6 --- .../src/commonMain/kotlin/data/EventRepository.kt | 2 +- .../src/commonMain/kotlin/data/FireBaseRepository.kt | 10 ++++++---- .../RecipeOverviewViewModel.kt | 11 ++++++++++- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/data/EventRepository.kt b/composeApp/src/commonMain/kotlin/data/EventRepository.kt index d23cefff..a5fb00db 100644 --- a/composeApp/src/commonMain/kotlin/data/EventRepository.kt +++ b/composeApp/src/commonMain/kotlin/data/EventRepository.kt @@ -43,7 +43,7 @@ interface EventRepository { suspend fun getAllRecipes(): List suspend fun getUserCreatedRecipes(): List suspend fun getMealById(eventId: String, mealId: String): Meal - suspend fun getRecipeById(recipeId: String): Recipe + suspend fun getRecipeById(recipeId: String): Recipe? suspend fun createRecipe(recipe: Recipe) suspend fun updateRecipe(recipe: Recipe) suspend fun deleteRecipe(recipeId: String) diff --git a/composeApp/src/commonMain/kotlin/data/FireBaseRepository.kt b/composeApp/src/commonMain/kotlin/data/FireBaseRepository.kt index 45618c65..cd3f5301 100644 --- a/composeApp/src/commonMain/kotlin/data/FireBaseRepository.kt +++ b/composeApp/src/commonMain/kotlin/data/FireBaseRepository.kt @@ -365,13 +365,15 @@ class FireBaseRepository(private val loginAndRegister: LoginAndRegister) : Event return recipes } - override suspend fun getRecipeById(recipeId: String): Recipe { - val recipe = firestore + override suspend fun getRecipeById(recipeId: String): Recipe? { + val doc = firestore .collection(RECIPES) .document(recipeId) .get() - .data { - } + + if (!doc.exists) return null + + val recipe = doc.data() // Batch load all ingredients in a single request instead of individual requests val ingredientRefs = recipe.shoppingIngredients.map { it.ingredientRef }.distinct() diff --git a/composeApp/src/commonMain/kotlin/view/event/recepie_overview_screen/RecipeOverviewViewModel.kt b/composeApp/src/commonMain/kotlin/view/event/recepie_overview_screen/RecipeOverviewViewModel.kt index 3e4e8aa7..259bbf68 100644 --- a/composeApp/src/commonMain/kotlin/view/event/recepie_overview_screen/RecipeOverviewViewModel.kt +++ b/composeApp/src/commonMain/kotlin/view/event/recepie_overview_screen/RecipeOverviewViewModel.kt @@ -29,7 +29,12 @@ class RecipeOverviewViewModel( private fun initializeViewModel(recipeSelection: RecipeSelection, eventId: String?) { _recipeState.value = ResultState.Loading viewModelScope.launch { - recipeSelection.recipe = eventRepository.getRecipeById(recipeSelection.recipeRef) + val recipe = eventRepository.getRecipeById(recipeSelection.recipeRef) + if (recipe == null) { + _recipeState.value = ResultState.Error(Exception("Rezept wurde gelöscht")) + return@launch + } + recipeSelection.recipe = recipe val calulatedMap = calculatedIngredientAmounts.calculateAmountsForRecipe( mutableMapOf(), recipeSelection, @@ -49,6 +54,10 @@ class RecipeOverviewViewModel( _recipeState.value = ResultState.Loading viewModelScope.launch { val recipe = eventRepository.getRecipeById(recipeId) + if (recipe == null) { + _recipeState.value = ResultState.Error(Exception("Rezept wurde gelöscht")) + return@launch + } val recipeSelection = RecipeSelection().apply { recipeRef = recipeId this.recipe = recipe From 6da844f01e45db0a241dba9b4698c9b28425a017 Mon Sep 17 00:00:00 2001 From: Frederik Kischewski Date: Sun, 15 Mar 2026 14:07:41 +0100 Subject: [PATCH 05/12] Implement iOS CSV file picker and improve recipe form UX - Implement real UIDocumentPickerViewController for iOS CSV import instead of placeholder/sample data - Add keyboard dismiss on tap outside for recipe overview, recipe form dialog, recipe management screen, and ingredient picker dialog - Fix recipe form header layout to wrap title on narrow screens - Use FocusRequester for reliable Next keyboard action in recipe form - Improve ingredient section layout with full-width add button Co-Authored-By: Claude Opus 4.6 --- .../view/admin/recipes/RecipeFormDialog.kt | 72 +++++++--------- .../recipes/RecipeIngredientPickerDialog.kt | 20 ++++- .../admin/recipes/RecipeManagementScreen.kt | 7 ++ .../RecepieOverview.kt | 8 ++ .../iosMain/kotlin/data/csv/FilePicker.ios.kt | 86 +++++++++++++++---- .../csv_import/PlatformFilePicker.ios.kt | 56 +++--------- 6 files changed, 144 insertions(+), 105 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/view/admin/recipes/RecipeFormDialog.kt b/composeApp/src/commonMain/kotlin/view/admin/recipes/RecipeFormDialog.kt index 68027905..a3ba4e4b 100644 --- a/composeApp/src/commonMain/kotlin/view/admin/recipes/RecipeFormDialog.kt +++ b/composeApp/src/commonMain/kotlin/view/admin/recipes/RecipeFormDialog.kt @@ -1,30 +1,26 @@ package view.admin.recipes +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties -import co.touchlab.kermit.Logger import model.* import org.koin.compose.koinInject -import view.admin.new_participant.IngredientPickerDialog import view.event.categorized_shopping_list.IngredientViewModel @OptIn(ExperimentalMaterial3Api::class) @@ -41,6 +37,7 @@ fun RecipeFormDialog( var showIngredientPicker by remember { mutableStateOf(false) } var editingIngredientIndex by remember { mutableStateOf(-1) } + val descriptionFocusRequester = remember { FocusRequester() } // Recipe form state var name by remember { mutableStateOf(recipe?.name ?: "") } @@ -95,14 +92,13 @@ fun RecipeFormDialog( ) { Text( text = if (recipe == null) "Neues Rezept" else "Rezept bearbeiten", - style = MaterialTheme.typography.headlineSmall + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.weight(1f, fill = false) ) - - Row { + Row(verticalAlignment = Alignment.CenterVertically) { TextButton(onClick = onDismiss) { Text("Abbrechen") } - Button( onClick = { val newRecipe = Recipe().apply { @@ -114,9 +110,8 @@ fun RecipeFormDialog( this.dietaryHabit = dietaryHabit this.shoppingIngredients = shoppingIngredients this.materials = materials - // Preserve existing values for fields user cannot edit this.pageInCookbook = recipe?.pageInCookbook ?: 0 - this.source = recipe?.source ?: "" // Will be set by repository + this.source = recipe?.source ?: "" this.price = price this.season = season this.foodIntolerances = foodIntolerances.toList() @@ -133,13 +128,16 @@ fun RecipeFormDialog( } } - Divider() + HorizontalDivider() // Form content LazyColumn( modifier = Modifier .fillMaxSize() - .padding(16.dp), + .padding(16.dp) + .pointerInput(Unit) { + detectTapGestures(onTap = { focusManager.clearFocus() }) + }, verticalArrangement = Arrangement.spacedBy(16.dp) ) { // Basic Information Section @@ -166,7 +164,7 @@ fun RecipeFormDialog( imeAction = ImeAction.Next ), keyboardActions = KeyboardActions( - onNext = { focusManager.moveFocus(FocusDirection.Down) } + onNext = { descriptionFocusRequester.requestFocus() } ), singleLine = true ) @@ -175,15 +173,10 @@ fun RecipeFormDialog( value = description, onValueChange = { description = it }, label = { Text("Beschreibung") }, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth() + .focusRequester(descriptionFocusRequester), minLines = 2, - maxLines = 4, - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.Done - ), - keyboardActions = KeyboardActions( - onDone = { focusManager.clearFocus() } - ) + maxLines = 4 ) } } @@ -203,26 +196,23 @@ fun RecipeFormDialog( } ) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { + if (shoppingIngredients.isNotEmpty()) { Text( - text = if (shoppingIngredients.isEmpty()) "Keine Zutaten hinzugefügt" else "${shoppingIngredients.size} Zutaten", + text = "${shoppingIngredients.size} Zutaten", style = MaterialTheme.typography.bodyMedium ) + } - Button( - onClick = { - editingIngredientIndex = -1 - showIngredientPicker = true - } - ) { - Icon(Icons.Default.Add, contentDescription = null) - Spacer(Modifier.width(8.dp)) - Text("Zutat hinzufügen") - } + Button( + onClick = { + editingIngredientIndex = -1 + showIngredientPicker = true + }, + modifier = Modifier.fillMaxWidth() + ) { + Icon(Icons.Default.Add, contentDescription = null) + Spacer(Modifier.width(8.dp)) + Text("Zutat hinzufügen") } shoppingIngredients.forEachIndexed { index, ingredient -> diff --git a/composeApp/src/commonMain/kotlin/view/admin/recipes/RecipeIngredientPickerDialog.kt b/composeApp/src/commonMain/kotlin/view/admin/recipes/RecipeIngredientPickerDialog.kt index 6b90c4b5..c2bcd608 100644 --- a/composeApp/src/commonMain/kotlin/view/admin/recipes/RecipeIngredientPickerDialog.kt +++ b/composeApp/src/commonMain/kotlin/view/admin/recipes/RecipeIngredientPickerDialog.kt @@ -1,11 +1,16 @@ package view.admin.recipes +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog @@ -41,6 +46,8 @@ fun RecipeIngredientPickerDialog( amount.toDoubleOrNull() != null && amount.toDoubleOrNull()!! > 0 + val focusManager = LocalFocusManager.current + Dialog(onDismissRequest = onDismiss) { Card( modifier = modifier @@ -51,7 +58,10 @@ fun RecipeIngredientPickerDialog( Column( modifier = Modifier .fillMaxWidth() - .padding(24.dp), + .padding(24.dp) + .pointerInput(Unit) { + detectTapGestures(onTap = { focusManager.clearFocus() }) + }, verticalArrangement = Arrangement.spacedBy(16.dp) ) { // Title @@ -95,7 +105,13 @@ fun RecipeIngredientPickerDialog( value = amount, onValueChange = { amount = it }, label = { Text("Menge") }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Decimal, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { focusManager.clearFocus() } + ), modifier = Modifier.fillMaxWidth(), isError = amount.isNotBlank() && (amount.toDoubleOrNull() == null || amount.toDoubleOrNull()!! <= 0), supportingText = { diff --git a/composeApp/src/commonMain/kotlin/view/admin/recipes/RecipeManagementScreen.kt b/composeApp/src/commonMain/kotlin/view/admin/recipes/RecipeManagementScreen.kt index e7dbb287..d663e78b 100644 --- a/composeApp/src/commonMain/kotlin/view/admin/recipes/RecipeManagementScreen.kt +++ b/composeApp/src/commonMain/kotlin/view/admin/recipes/RecipeManagementScreen.kt @@ -1,5 +1,6 @@ package view.admin.recipes +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -9,6 +10,8 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import model.Recipe @@ -27,8 +30,12 @@ fun RecipeManagementScreen( var showCreateDialog by remember { mutableStateOf(false) } var editingRecipe by remember { mutableStateOf(null) } + val focusManager = LocalFocusManager.current Scaffold( + modifier = Modifier.pointerInput(Unit) { + detectTapGestures(onTap = { focusManager.clearFocus() }) + }, topBar = { TopAppBar( title = { Text("Rezepte verwalten") }, diff --git a/composeApp/src/commonMain/kotlin/view/event/recepie_overview_screen/RecepieOverview.kt b/composeApp/src/commonMain/kotlin/view/event/recepie_overview_screen/RecepieOverview.kt index ab2716c7..af21a1b7 100644 --- a/composeApp/src/commonMain/kotlin/view/event/recepie_overview_screen/RecepieOverview.kt +++ b/composeApp/src/commonMain/kotlin/view/event/recepie_overview_screen/RecepieOverview.kt @@ -28,7 +28,10 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.unit.sp +import androidx.compose.foundation.gestures.detectTapGestures import androidx.navigation.NavHostController import futterbock_app.composeapp.generated.resources.Res import futterbock_app.composeapp.generated.resources.schwierigkeit1 @@ -76,7 +79,12 @@ fun RecipeOverview( state: ResultState, onAction: (BaseAction) -> Unit ) { + val focusManager = LocalFocusManager.current + Scaffold( + modifier = Modifier.pointerInput(Unit) { + detectTapGestures(onTap = { focusManager.clearFocus() }) + }, topBar = { TopAppBar( title = { Text(text = "Rezeptübersicht") }, diff --git a/composeApp/src/iosMain/kotlin/data/csv/FilePicker.ios.kt b/composeApp/src/iosMain/kotlin/data/csv/FilePicker.ios.kt index 6594e936..1afc8eff 100644 --- a/composeApp/src/iosMain/kotlin/data/csv/FilePicker.ios.kt +++ b/composeApp/src/iosMain/kotlin/data/csv/FilePicker.ios.kt @@ -1,32 +1,84 @@ package data.csv -import platform.Foundation.NSData +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.coroutines.suspendCancellableCoroutine import platform.Foundation.NSString +import platform.Foundation.NSURL import platform.Foundation.NSUTF8StringEncoding -import platform.Foundation.dataUsingEncoding +import platform.Foundation.stringWithContentsOfURL +import platform.UIKit.UIApplication +import platform.UIKit.UIDocumentPickerDelegateProtocol import platform.UIKit.UIDocumentPickerViewController -import platform.UIKit.UIDocumentPickerMode import platform.UniformTypeIdentifiers.UTTypeCommaSeparatedText import platform.UniformTypeIdentifiers.UTTypePlainText -import kotlinx.coroutines.suspendCancellableCoroutine +import platform.UniformTypeIdentifiers.UTTypeText +import platform.darwin.NSObject import kotlin.coroutines.resume +@OptIn(ExperimentalForeignApi::class) actual class FilePicker { actual suspend fun pickCsvFile(): FilePickerResult { return suspendCancellableCoroutine { continuation -> - try { - // Placeholder implementation for iOS - // Actual implementation would require UIDocumentPickerViewController - continuation.resume( - FilePickerResult( - error = "iOS file picker implementation required" - ) - ) - } catch (e: Exception) { - continuation.resume( - FilePickerResult(error = "Error picking file: ${e.message}") - ) + val contentTypes = listOf( + UTTypeCommaSeparatedText, + UTTypePlainText, + UTTypeText + ) + val picker = UIDocumentPickerViewController(forOpeningContentTypes = contentTypes) + picker.allowsMultipleSelection = false + + val delegate = object : NSObject(), UIDocumentPickerDelegateProtocol { + override fun documentPicker( + controller: UIDocumentPickerViewController, + didPickDocumentsAtURLs: List<*> + ) { + val url = didPickDocumentsAtURLs.firstOrNull() as? NSURL + if (url == null) { + continuation.resume(FilePickerResult(error = "Keine Datei ausgewählt")) + return + } + + val accessing = url.startAccessingSecurityScopedResource() + try { + @Suppress("CAST_NEVER_SUCCEEDS") + val content = NSString.stringWithContentsOfURL( + url = url, + encoding = NSUTF8StringEncoding, + error = null + ) as? String + + if (content != null) { + continuation.resume( + FilePickerResult( + content = content, + fileName = url.lastPathComponent ?: "unknown.csv" + ) + ) + } else { + continuation.resume( + FilePickerResult(error = "Datei konnte nicht gelesen werden") + ) + } + } finally { + if (accessing) { + url.stopAccessingSecurityScopedResource() + } + } + } + + override fun documentPickerWasCancelled(controller: UIDocumentPickerViewController) { + continuation.resume(FilePickerResult(error = "Dateiauswahl abgebrochen")) + } + } + + picker.delegate = delegate + + val rootViewController = UIApplication.sharedApplication.keyWindow?.rootViewController + if (rootViewController != null) { + rootViewController.presentViewController(picker, animated = true, completion = null) + } else { + continuation.resume(FilePickerResult(error = "Kein ViewController verfügbar")) } } } -} \ No newline at end of file +} diff --git a/composeApp/src/iosMain/kotlin/view/admin/csv_import/PlatformFilePicker.ios.kt b/composeApp/src/iosMain/kotlin/view/admin/csv_import/PlatformFilePicker.ios.kt index 6bf99f39..2765d29c 100644 --- a/composeApp/src/iosMain/kotlin/view/admin/csv_import/PlatformFilePicker.ios.kt +++ b/composeApp/src/iosMain/kotlin/view/admin/csv_import/PlatformFilePicker.ios.kt @@ -3,63 +3,29 @@ package view.admin.csv_import import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -import androidx.compose.ui.interop.UIKitView +import data.csv.FilePicker import data.csv.FilePickerResult -import kotlinx.cinterop.ExperimentalForeignApi -import platform.Foundation.NSData -import platform.Foundation.NSString -import platform.Foundation.NSUTF8StringEncoding -import platform.Foundation.dataUsingEncoding -import platform.UIKit.UIDocumentPickerViewController -import platform.UIKit.UIDocumentPickerMode -import platform.UIKit.UIViewController -import platform.UniformTypeIdentifiers.UTTypeCommaSeparatedText -import platform.UniformTypeIdentifiers.UTTypePlainText +import kotlinx.coroutines.launch -@OptIn(ExperimentalForeignApi::class) @Composable actual fun PlatformFilePicker( onFileSelected: (FilePickerResult) -> Unit, modifier: Modifier ) { + val scope = rememberCoroutineScope() + val filePicker = FilePicker() + Button( onClick = { - // Create document picker for CSV and text files - val documentTypes = listOf( - UTTypeCommaSeparatedText.identifier, - UTTypePlainText.identifier, - "public.text" - ) - - try { - // This is a simplified implementation - // In a real app, you'd need to present the UIDocumentPickerViewController - // and handle the delegate methods properly - - // For now, provide a sample CSV for testing - val sampleCsv = """ - Vorname,Nachname,Geburtsdatum - Max,Mustermann,01.01.1990 - Anna,Schmidt,15.05.1985 - Peter,Wagner,22.12.1992 - """.trimIndent() - - onFileSelected( - FilePickerResult( - content = sampleCsv, - fileName = "sample.csv" - ) - ) - } catch (e: Exception) { - onFileSelected( - FilePickerResult(error = "iOS file picker error: ${e.message}") - ) + scope.launch { + val result = filePicker.pickCsvFile() + onFileSelected(result) } }, modifier = modifier ) { - Text("Datei auswählen (iOS)") + Text("Datei auswählen") } -} \ No newline at end of file +} From 4ebaaacc7e8d112c23b260ee6a2141faf51b8be0 Mon Sep 17 00:00:00 2001 From: Frederik Kischewski Date: Sun, 15 Mar 2026 14:32:58 +0100 Subject: [PATCH 06/12] Remove signup delay workaround, custom firebase-java-sdk, and add CSV import back button - Remove 5-second delay workaround in Register.kt signup flow - Remove custom dev.gitlive:firebase-java-sdk:0.6.2 dependency (now bundled in gitlive 2.4.0) - Add NavigationIconButton to CSV import wizard top bar Co-Authored-By: Claude Opus 4.6 --- composeApp/build.gradle.kts | 6 ++---- .../kotlin/view/admin/csv_import/CsvImportWizard.kt | 5 ++--- composeApp/src/commonMain/kotlin/view/login/Register.kt | 1 - 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 70660768..7bb597b8 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -14,7 +14,6 @@ plugins { alias(libs.plugins.kotlinxSerialization) alias(libs.plugins.google.services) alias(libs.plugins.buildkonfig) - alias(libs.plugins.hotreload) } composeCompiler { @@ -80,6 +79,7 @@ kotlin { compilerOptions { freeCompilerArgs.add("-Xexpect-actual-classes") + freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime") } androidTarget { compilerOptions { @@ -106,7 +106,6 @@ kotlin { androidMain.dependencies { - implementation(libs.compose.ui.tooling.preview) implementation(compose.preview) implementation(libs.androidx.activity.compose) implementation(libs.koin.android) @@ -124,6 +123,7 @@ kotlin { implementation(libs.kotlinx.datetime) implementation(libs.kotlinx.couroutines.core) implementation(libs.androidx.lifecycle.viewmodel) + implementation(libs.androidx.lifecycle.runtime.compose) implementation(compose.materialIconsExtended) implementation(libs.androidx.navigation) implementation(libs.kotlinx.serialization.json) @@ -135,8 +135,6 @@ kotlin { implementation(libs.kermit) } desktopMain.dependencies { - // TODO delete when this pr is merged: https://github.com/GitLiveApp/firebase-java-sdk/pull/33 - implementation("dev.gitlive:firebase-java-sdk:0.6.2") implementation(compose.desktop.currentOs) implementation(libs.kotlinx.coroutines.swing) implementation(libs.pdfbox) diff --git a/composeApp/src/commonMain/kotlin/view/admin/csv_import/CsvImportWizard.kt b/composeApp/src/commonMain/kotlin/view/admin/csv_import/CsvImportWizard.kt index ea61851f..d6aede63 100644 --- a/composeApp/src/commonMain/kotlin/view/admin/csv_import/CsvImportWizard.kt +++ b/composeApp/src/commonMain/kotlin/view/admin/csv_import/CsvImportWizard.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import data.csv.* +import view.shared.NavigationIconButton enum class ImportStep { FILE_SELECTION, @@ -64,9 +65,7 @@ fun CsvImportWizard( TopAppBar( title = { Text("CSV Import") }, navigationIcon = { - IconButton(onClick = onClose) { - Icon(Icons.Default.FileUpload, contentDescription = "Schließen") - } + NavigationIconButton(onLeave = onClose) } ) } diff --git a/composeApp/src/commonMain/kotlin/view/login/Register.kt b/composeApp/src/commonMain/kotlin/view/login/Register.kt index 09a7bcd1..4be55d6d 100644 --- a/composeApp/src/commonMain/kotlin/view/login/Register.kt +++ b/composeApp/src/commonMain/kotlin/view/login/Register.kt @@ -81,7 +81,6 @@ fun Register( fun onSubmit() { scope.launch { loading = true - delay(5000) if (password != passwordConfirm) { registerError = "Passwörter stimmen nicht überein" loading = false From f7260c39c538b63b0a7d8f92826471d46a12d2cf Mon Sep 17 00:00:00 2001 From: Frederik Kischewski Date: Sun, 15 Mar 2026 14:36:45 +0100 Subject: [PATCH 07/12] Upgrade dependencies, migrate Clock, and clean up project config - Upgrade Compose Multiplatform 1.7.1 -> 1.10.2, Navigation 2.8.0-alpha10 -> 2.9.2, kotlinx-datetime 0.6.2 -> 0.7.1 - Migrate kotlinx.datetime.Clock to kotlin.time.Clock across all sources - Remove compose hot-reload plugin and DevelopmentEntryPoint wrapper - Remove unused imports (viewModel, compose-ui-tooling-preview) - Enable Android predictive back gesture (enableOnBackInvokedCallback) - Hardcode iOS bundle identifier to org.futterbock.app - Add lifecycle-runtime-compose dependency Co-Authored-By: Claude Opus 4.6 --- composeApp/src/androidMain/AndroidManifest.xml | 1 + .../services/pdfService/PdfService.android.kt | 2 +- .../commonMain/kotlin/data/FireBaseRepository.kt | 2 +- composeApp/src/commonMain/kotlin/model/Event.kt | 2 +- .../commonMain/kotlin/modules/ViewModelModules.kt | 2 -- .../commonMain/kotlin/services/event/IsEditable.kt | 2 +- .../services/shoppingList/CalculateShoppingList.kt | 2 +- .../kotlin/view/event/SharedEventViewModel.kt | 2 +- .../CategorizedShoppingListViewModel.kt | 1 - .../CookingGroupIngredientsViewModel.kt | 2 +- .../event/materiallist/MaterialListViewModel.kt | 1 - .../view/event/new_event/SimpleDateRangePicker.kt | 2 +- .../RecipeOverviewViewModel.kt | 4 ++-- .../kotlin/view/shared/HelperFunctions.kt | 2 +- .../kotlin/view/shared/date/DatePickerDialog.kt | 2 +- .../shared/date/FutureOrPresentSelectableDates.kt | 2 +- .../shared/date/PastOrPresentSelectableDates.kt | 2 +- .../shared/date/SelectedAbleDatesWithFromTo.kt | 2 +- .../commonTest/kotlin/data/FakeEventRepository.kt | 2 +- .../CookingGroupIngredientServiceTest.kt | 2 +- .../services/event/ParticipantCanEatRecipeTest.kt | 2 +- .../materiallist/CalculateMaterialListTest.kt | 2 +- .../shoppingList/CalculateShoppingListTest.kt | 2 +- composeApp/src/desktopMain/kotlin/main.kt | 5 +---- gradle/libs.versions.toml | 14 +++++--------- iosApp/iosApp.xcodeproj/project.pbxproj | 4 ++-- 26 files changed, 29 insertions(+), 39 deletions(-) diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml index 0703b7f6..955ae37e 100644 --- a/composeApp/src/androidMain/AndroidManifest.xml +++ b/composeApp/src/androidMain/AndroidManifest.xml @@ -6,6 +6,7 @@ Date: Sun, 15 Mar 2026 14:49:10 +0100 Subject: [PATCH 08/12] Improve register error handling and sync iOS project config from main - Add FirebaseAuthWeakPasswordException for weak password errors - Remove irrelevant FirebaseAuthInvalidUserException catch - Unify error messages to du-form, clearer and more concise - Sync iOS project.pbxproj bundle ID and signing from main branch Co-Authored-By: Claude Opus 4.6 --- .../commonMain/kotlin/view/login/Register.kt | 30 ++++++++----------- iosApp/iosApp.xcodeproj/project.pbxproj | 10 ++++--- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/view/login/Register.kt b/composeApp/src/commonMain/kotlin/view/login/Register.kt index 4be55d6d..ebbbac7d 100644 --- a/composeApp/src/commonMain/kotlin/view/login/Register.kt +++ b/composeApp/src/commonMain/kotlin/view/login/Register.kt @@ -41,8 +41,8 @@ import androidx.compose.ui.unit.dp import co.touchlab.kermit.Logger import dev.gitlive.firebase.auth.FirebaseAuthEmailException import dev.gitlive.firebase.auth.FirebaseAuthInvalidCredentialsException -import dev.gitlive.firebase.auth.FirebaseAuthInvalidUserException import dev.gitlive.firebase.auth.FirebaseAuthUserCollisionException +import dev.gitlive.firebase.auth.FirebaseAuthWeakPasswordException import futterbock_app.composeapp.generated.resources.Res import futterbock_app.composeapp.generated.resources.login_submit import futterbock_app.composeapp.generated.resources.logo @@ -82,18 +82,17 @@ fun Register( scope.launch { loading = true if (password != passwordConfirm) { - registerError = "Passwörter stimmen nicht überein" + registerError = "Die Passwörter stimmen nicht überein." loading = false return@launch } if (password.length < 6) { - registerError = "Passwort muss mindestens 6 Zeichen lang sein" + registerError = "Das Passwort muss mindestens 6 Zeichen lang sein." loading = false return@launch } if (group.equals("Futterbock", ignoreCase = true)) { - registerError = - "Der Gruppenname 'Futterbock' ist reserviert und kann nicht verwendet werden" + registerError = "Der Gruppenname 'Futterbock' ist reserviert." loading = false return@launch } @@ -101,22 +100,17 @@ fun Register( currentApp.register(email = email, password = password, group = group) delay(100) onRegisterNavigation() + } catch (e: FirebaseAuthWeakPasswordException) { + registerError = "Das Passwort ist zu schwach. Bitte wähle ein stärkeres Passwort." + } catch (e: FirebaseAuthEmailException) { + registerError = "Bitte gib eine gültige E-Mail-Adresse ein." } catch (e: FirebaseAuthInvalidCredentialsException) { - registerError = - "Fehler beim Registrieren: Bitte geben Sie eine valide Email-Adresse an." + registerError = "Ungültige Anmeldedaten. Bitte überprüfe deine Eingaben." } catch (e: FirebaseAuthUserCollisionException) { - registerError = - "Fehler beim Registrieren: Der Username existiert bereits. Bitte verwenden Sie einen anderen Usernamen." - } catch (e: FirebaseAuthInvalidUserException) { - registerError = - "Fehler beim Registrieren: Der Username existiert bereits. Bitte verwenden Sie einen anderen Usernamen." - } catch (e: FirebaseAuthEmailException){ - registerError = "Bitte geben Sie eine valide Email-Adresse an." - } - catch (e: Exception) { + registerError = "Diese E-Mail-Adresse wird bereits verwendet. Bitte melde dich an oder verwende eine andere E-Mail." + } catch (e: Exception) { Logger.e(e.stackTraceToString()) - registerError = - "Ein unbekannter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut: " + registerError = "Ein unbekannter Fehler ist aufgetreten. Bitte versuche es später erneut." } finally { loading = false } diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index e38f8a7e..fac49e67 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -227,6 +227,7 @@ baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + BUNDLE_ID = com.futterbock.app; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -290,6 +291,7 @@ baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + BUNDLE_ID = com.futterbock.app; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -349,7 +351,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; - DEVELOPMENT_TEAM = 6KWU799LRP; + DEVELOPMENT_TEAM = 4U5J277GB9; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -361,7 +363,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = org.futterbock.app; + PRODUCT_BUNDLE_IDENTIFIER = com.futterbock.app; PRODUCT_NAME = "${APP_NAME}"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; @@ -376,7 +378,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; - DEVELOPMENT_TEAM = 6KWU799LRP; + DEVELOPMENT_TEAM = 4U5J277GB9; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -388,7 +390,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = org.futterbock.app; + PRODUCT_BUNDLE_IDENTIFIER = com.futterbock.app; PRODUCT_NAME = "${APP_NAME}"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; From 7c93a7d99e27569b3ef44bed5959faebdc3b0d9c Mon Sep 17 00:00:00 2001 From: Frederik Kischewski Date: Sun, 15 Mar 2026 16:39:09 +0100 Subject: [PATCH 09/12] Fix iOS delegate retention, test compilation, and hardcoded padding - Retain iOS FilePicker delegate as class property to prevent GC before UIKit weak delegate callback fires - Add invokeOnCancellation to dismiss picker on coroutine cancel - Fix FakeEventRepository.getRecipeById return type to match nullable interface (Recipe?) - Remove delay(100) after registration navigation - Replace hardcoded 165.dp bottom padding with dynamic values: Scaffold's calculateBottomPadding() in NewMealPage, onSizeChanged-measured FAB height in ParticipantSearchBar Co-Authored-By: Claude Opus 4.6 --- .../kotlin/view/event/new_meal_screen/NewMealPage.kt | 2 +- .../view/event/participants/ParticipantSearchBar.kt | 8 +++++++- .../src/commonMain/kotlin/view/login/Register.kt | 2 -- .../commonTest/kotlin/data/FakeEventRepository.kt | 2 +- .../src/iosMain/kotlin/data/csv/FilePicker.ios.kt | 12 ++++++++++++ 5 files changed, 21 insertions(+), 5 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/view/event/new_meal_screen/NewMealPage.kt b/composeApp/src/commonMain/kotlin/view/event/new_meal_screen/NewMealPage.kt index 52bb0564..c7522b55 100644 --- a/composeApp/src/commonMain/kotlin/view/event/new_meal_screen/NewMealPage.kt +++ b/composeApp/src/commonMain/kotlin/view/event/new_meal_screen/NewMealPage.kt @@ -169,7 +169,7 @@ fun NewMealPage( Column( modifier = Modifier.padding(top = it.calculateTopPadding()).padding(8.dp) .verticalScroll(rememberScrollState()).fillMaxHeight() - .padding(bottom = 165.dp) + .padding(bottom = it.calculateBottomPadding()) ) { when (state) { is ResultState.Success -> { diff --git a/composeApp/src/commonMain/kotlin/view/event/participants/ParticipantSearchBar.kt b/composeApp/src/commonMain/kotlin/view/event/participants/ParticipantSearchBar.kt index 9b9a1f2a..ca3e743a 100644 --- a/composeApp/src/commonMain/kotlin/view/event/participants/ParticipantSearchBar.kt +++ b/composeApp/src/commonMain/kotlin/view/event/participants/ParticipantSearchBar.kt @@ -40,12 +40,15 @@ import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -103,6 +106,8 @@ fun ParticipantSearchBar( var active by remember { mutableStateOf(true) } var participantsAddedInThisStep by remember { mutableStateOf(listOf()) } val keyboardController = LocalSoftwareKeyboardController.current + var fabHeightPx by remember { mutableIntStateOf(0) } + val density = LocalDensity.current Scaffold( @@ -205,7 +210,7 @@ fun ParticipantSearchBar( Box(modifier = Modifier.fillMaxSize().navigationBarsPadding().imePadding()) { Column( modifier = Modifier.verticalScroll(rememberScrollState()) - .padding(bottom = 165.dp) + .padding(bottom = with(density) { fabHeightPx.toDp() } + 32.dp) ) { when (allParticipants) { is ResultState.Success -> { @@ -240,6 +245,7 @@ fun ParticipantSearchBar( Box( modifier = Modifier.align(Alignment.BottomEnd) .padding(16.dp) + .onSizeChanged { fabHeightPx = it.height } ) { ParticipantActionButtons(onAction = onAction) } diff --git a/composeApp/src/commonMain/kotlin/view/login/Register.kt b/composeApp/src/commonMain/kotlin/view/login/Register.kt index ebbbac7d..769346d5 100644 --- a/composeApp/src/commonMain/kotlin/view/login/Register.kt +++ b/composeApp/src/commonMain/kotlin/view/login/Register.kt @@ -48,7 +48,6 @@ import futterbock_app.composeapp.generated.resources.login_submit import futterbock_app.composeapp.generated.resources.logo import futterbock_app.composeapp.generated.resources.stamm import futterbock_app.composeapp.generated.resources.startPage -import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.painterResource @@ -98,7 +97,6 @@ fun Register( } try { currentApp.register(email = email, password = password, group = group) - delay(100) onRegisterNavigation() } catch (e: FirebaseAuthWeakPasswordException) { registerError = "Das Passwort ist zu schwach. Bitte wähle ein stärkeres Passwort." diff --git a/composeApp/src/commonTest/kotlin/data/FakeEventRepository.kt b/composeApp/src/commonTest/kotlin/data/FakeEventRepository.kt index cd686beb..35901478 100644 --- a/composeApp/src/commonTest/kotlin/data/FakeEventRepository.kt +++ b/composeApp/src/commonTest/kotlin/data/FakeEventRepository.kt @@ -114,7 +114,7 @@ class FakeEventRepository : EventRepository { TODO("Not yet implemented") } - override suspend fun getRecipeById(recipeId: String): Recipe { + override suspend fun getRecipeById(recipeId: String): Recipe? { TODO("Not yet implemented") } diff --git a/composeApp/src/iosMain/kotlin/data/csv/FilePicker.ios.kt b/composeApp/src/iosMain/kotlin/data/csv/FilePicker.ios.kt index 1afc8eff..3b88ded8 100644 --- a/composeApp/src/iosMain/kotlin/data/csv/FilePicker.ios.kt +++ b/composeApp/src/iosMain/kotlin/data/csv/FilePicker.ios.kt @@ -17,6 +17,9 @@ import kotlin.coroutines.resume @OptIn(ExperimentalForeignApi::class) actual class FilePicker { + // Strong reference to prevent GC before UIKit weak delegate callback fires + private var currentDelegate: NSObject? = null + actual suspend fun pickCsvFile(): FilePickerResult { return suspendCancellableCoroutine { continuation -> val contentTypes = listOf( @@ -32,6 +35,7 @@ actual class FilePicker { controller: UIDocumentPickerViewController, didPickDocumentsAtURLs: List<*> ) { + currentDelegate = null val url = didPickDocumentsAtURLs.firstOrNull() as? NSURL if (url == null) { continuation.resume(FilePickerResult(error = "Keine Datei ausgewählt")) @@ -67,16 +71,24 @@ actual class FilePicker { } override fun documentPickerWasCancelled(controller: UIDocumentPickerViewController) { + currentDelegate = null continuation.resume(FilePickerResult(error = "Dateiauswahl abgebrochen")) } } + currentDelegate = delegate picker.delegate = delegate + continuation.invokeOnCancellation { + currentDelegate = null + picker.dismissViewControllerAnimated(true, completion = null) + } + val rootViewController = UIApplication.sharedApplication.keyWindow?.rootViewController if (rootViewController != null) { rootViewController.presentViewController(picker, animated = true, completion = null) } else { + currentDelegate = null continuation.resume(FilePickerResult(error = "Kein ViewController verfügbar")) } } From 2bd306ac424ed5f987e91be464d4b5cdb0a227f6 Mon Sep 17 00:00:00 2001 From: Frederik Kischewski Date: Sun, 15 Mar 2026 19:26:12 +0100 Subject: [PATCH 10/12] Center buttons vertically in event page FlowRows and clean up date button - Add itemVerticalAlignment = CenterVertically to participant/cooking groups FlowRow and date picker FlowRow - Remove unnecessary IntrinsicSize.Min, clip, and background from date button (use standard Button styling) Co-Authored-By: Claude Opus 4.6 --- .../kotlin/view/event/new_event/NewEventPage.kt | 1 + .../view/event/new_event/SimpleDateRangePicker.kt | 12 ++---------- iosApp/iosApp.xcodeproj/project.pbxproj | 8 ++++---- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/view/event/new_event/NewEventPage.kt b/composeApp/src/commonMain/kotlin/view/event/new_event/NewEventPage.kt index 3a67e424..72c07e7e 100644 --- a/composeApp/src/commonMain/kotlin/view/event/new_event/NewEventPage.kt +++ b/composeApp/src/commonMain/kotlin/view/event/new_event/NewEventPage.kt @@ -158,6 +158,7 @@ fun NewEventPage( FlowRow( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp), + itemVerticalAlignment = Alignment.CenterVertically, ) { ParticipantNumberTextField(sharedState, onAction) CookingGroupsButton(onAction) diff --git a/composeApp/src/commonMain/kotlin/view/event/new_event/SimpleDateRangePicker.kt b/composeApp/src/commonMain/kotlin/view/event/new_event/SimpleDateRangePicker.kt index 1f562527..85c0bf0e 100644 --- a/composeApp/src/commonMain/kotlin/view/event/new_event/SimpleDateRangePicker.kt +++ b/composeApp/src/commonMain/kotlin/view/event/new_event/SimpleDateRangePicker.kt @@ -1,17 +1,13 @@ package view.event.new_event -import androidx.compose.foundation.background import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.DateRange import androidx.compose.material3.Button @@ -26,7 +22,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import kotlin.time.Clock import kotlinx.datetime.Instant @@ -53,6 +48,7 @@ fun SimpleDateRangePickerInDatePickerDialog( FlowRow( horizontalArrangement = Arrangement.Start, + itemVerticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth(), ) { DateInputField( @@ -75,11 +71,7 @@ fun SimpleDateRangePickerInDatePickerDialog( } }, modifier = Modifier - .padding(8.dp).height(IntrinsicSize.Min).align(Alignment.CenterVertically) - .clip(shape = RoundedCornerShape(75)) - .background( - MaterialTheme.colorScheme.primary - ), + .padding(8.dp).align(Alignment.CenterVertically), ) { Row { diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index fac49e67..a618076b 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -351,7 +351,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; - DEVELOPMENT_TEAM = 4U5J277GB9; + DEVELOPMENT_TEAM = BP92C82JT8; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -363,7 +363,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.futterbock.app; + PRODUCT_BUNDLE_IDENTIFIER = com.futterbock.app2; PRODUCT_NAME = "${APP_NAME}"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; @@ -378,7 +378,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; - DEVELOPMENT_TEAM = 4U5J277GB9; + DEVELOPMENT_TEAM = BP92C82JT8; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -390,7 +390,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.futterbock.app; + PRODUCT_BUNDLE_IDENTIFIER = com.futterbock.app2; PRODUCT_NAME = "${APP_NAME}"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; From 1a84860a5caac770f70300b42971882bc6cfb340 Mon Sep 17 00:00:00 2001 From: Frederik Kischewski Date: Sun, 15 Mar 2026 19:48:30 +0100 Subject: [PATCH 11/12] fixed padding --- .claude/settings.local.json | 29 +++++++++++++++++-- .../view/event/new_meal_screen/NewMealPage.kt | 2 +- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 38984c80..7dea7070 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,8 +1,33 @@ { "permissions": { "allow": [ - "Bash(rg:*)" + "Bash(rg:*)", + "Bash(./gradlew :composeApp:compileKotlinDesktop 2>&1 | tail -20)", + "Bash(./gradlew :composeApp:compileKotlinDesktop 2>&1 | grep -i \"error\\\\|Error\" | head -20)", + "Bash(./gradlew :composeApp:compileKotlinDesktop 2>&1 | grep -E \"\\\\.kt:\" | head -20)", + "Bash(./gradlew :composeApp:compileKotlinDesktop 2>&1 | tail -10)", + "Bash(./gradlew :composeApp:compileKotlinDesktop 2>&1 | tail -5)", + "Bash(git checkout:*)", + "Bash(git add:*)", + "Bash(git commit:*)", + "WebSearch", + "WebFetch(domain:github.com)", + "WebFetch(domain:central.sonatype.com)", + "WebFetch(domain:www.jetbrains.com)", + "WebFetch(domain:raw.githubusercontent.com)", + "WebFetch(domain:maven.pkg.jetbrains.space)", + "WebFetch(domain:plugins.gradle.org)", + "Bash(curl:*)", + "Bash(./gradlew build:*)", + "Bash(./gradlew :composeApp:compileCommonMainKotlinMetadata :composeApp:compileKotlinDesktop :composeApp:compileDebugKotlinAndroid 2>&1 | grep \"^e:\\\\|BUILD\")", + "Bash(./gradlew tasks:*)", + "mcp__ide__getDiagnostics", + "Bash(./gradlew :composeApp:testDebugUnitTest 2>&1)", + "WebFetch(domain:gitliveapp.github.io)", + "Bash(git fetch:*)", + "Bash(gh pr:*)", + "Bash(git push:*)" ], "deny": [] } -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/view/event/new_meal_screen/NewMealPage.kt b/composeApp/src/commonMain/kotlin/view/event/new_meal_screen/NewMealPage.kt index c7522b55..52bb0564 100644 --- a/composeApp/src/commonMain/kotlin/view/event/new_meal_screen/NewMealPage.kt +++ b/composeApp/src/commonMain/kotlin/view/event/new_meal_screen/NewMealPage.kt @@ -169,7 +169,7 @@ fun NewMealPage( Column( modifier = Modifier.padding(top = it.calculateTopPadding()).padding(8.dp) .verticalScroll(rememberScrollState()).fillMaxHeight() - .padding(bottom = it.calculateBottomPadding()) + .padding(bottom = 165.dp) ) { when (state) { is ResultState.Success -> { From 8b7aca510d2a8f5085a2f4a37e54542df499e1dc Mon Sep 17 00:00:00 2001 From: Frederik Kischewski Date: Sun, 15 Mar 2026 19:51:11 +0100 Subject: [PATCH 12/12] resovled conflicts --- iosApp/iosApp.xcodeproj/project.pbxproj | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index a618076b..8e65c9b5 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -227,7 +227,6 @@ baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - BUNDLE_ID = com.futterbock.app; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -291,7 +290,6 @@ baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - BUNDLE_ID = com.futterbock.app; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -351,7 +349,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; - DEVELOPMENT_TEAM = BP92C82JT8; + DEVELOPMENT_TEAM = 6KWU799LRP; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -363,7 +361,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.futterbock.app2; + PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}"; PRODUCT_NAME = "${APP_NAME}"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; @@ -378,7 +376,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; - DEVELOPMENT_TEAM = BP92C82JT8; + DEVELOPMENT_TEAM = 6KWU799LRP; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -390,7 +388,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.futterbock.app2; + PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}"; PRODUCT_NAME = "${APP_NAME}"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0;