From 4e3c83a2ccd416400120fe6694a888ca3776b078 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 1 Apr 2024 14:43:54 +0200 Subject: [PATCH] add recipe editing and start on settings --- .../io/github/jan/einkaufszettel/Utils.kt | 13 +- .../cards/data/local/CardsDataSource.kt | 8 +- .../profile/data/local/ProfileDataSource.kt | 7 + .../recipes/data/local/RecipeDataSource.kt | 7 + .../recipes/data/remote/RecipeApi.kt | 32 ++--- .../ui/{create => }/components/RecipeList.kt | 10 +- .../recipes/ui/create/RecipeCreateS2Screen.kt | 56 -------- .../recipes/ui/create/RecipeCreateScreen.kt | 9 +- .../ui/create/RecipeCreateScreenModel.kt | 52 +------ .../ui/create/RecipeCreateStepScreen.kt | 15 -- .../recipes/ui/edit/RecipeEditScreen.kt | 132 ++++++++++++++++++ .../recipes/ui/edit/RecipeEditScreenModel.kt | 63 +++++++++ .../recipes/ui/main/RecipeScreen.kt | 7 +- .../RecipeModifyS1Screen.kt} | 19 ++- .../recipes/ui/steps/RecipeModifyS2Screen.kt | 88 ++++++++++++ .../RecipeModifyS3Screen.kt} | 19 ++- .../ui/steps/RecipeModifyScreenModel.kt | 59 ++++++++ .../ui/steps/RecipeModifyStepScreen.kt | 15 ++ .../root/di/models/recipeModels.kt | 4 + .../root/di/models/settingsModels.kt | 3 + .../settings/ui/SettingsScreenModel.kt | 12 +- .../einkaufszettel/settings/ui/SettingsTab.kt | 89 +++++++++++- .../shops/data/local/ShopDataSource.kt | 5 +- .../commonMain/libres/strings/strings_en.xml | 8 ++ .../sqldelight/einkaufszettel/cardTable.sq | 7 - .../sqldelight/einkaufszettel/recipeTable.sq | 7 - .../sqldelight/einkaufszettel/shopTable.sq | 4 - 27 files changed, 567 insertions(+), 183 deletions(-) rename composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/{create => }/components/RecipeList.kt (93%) delete mode 100644 composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/create/RecipeCreateS2Screen.kt delete mode 100644 composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/create/RecipeCreateStepScreen.kt create mode 100644 composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/edit/RecipeEditScreen.kt create mode 100644 composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/edit/RecipeEditScreenModel.kt rename composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/{create/RecipeCreateS1Screen.kt => steps/RecipeModifyS1Screen.kt} (64%) create mode 100644 composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/steps/RecipeModifyS2Screen.kt rename composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/{create/RecipeCreateS3Screen.kt => steps/RecipeModifyS3Screen.kt} (84%) create mode 100644 composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/steps/RecipeModifyScreenModel.kt create mode 100644 composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/steps/RecipeModifyStepScreen.kt diff --git a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/Utils.kt b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/Utils.kt index 56bf345..ebf8e7c 100644 --- a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/Utils.kt +++ b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/Utils.kt @@ -3,8 +3,10 @@ package io.github.jan.einkaufszettel import androidx.compose.runtime.Composable import androidx.compose.runtime.State import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.core.model.rememberNavigatorScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.navigator.Navigator import io.github.jan.supabase.gotrue.Auth import kotlinx.coroutines.flow.StateFlow import org.koin.compose.getKoin @@ -25,4 +27,13 @@ public inline fun Screen.getScreenModel( ): T { val koin = getKoin() return rememberScreenModel(tag = tag) { koin.get(null, parameters) } -} \ No newline at end of file +} + +@Composable +public inline fun Navigator.getNavigatorScreenModelT( + tag: String? = null, + noinline parameters: ParametersDefinition? = null +): T { + val koin = getKoin() + return rememberNavigatorScreenModel(tag = tag) { koin.get(null, parameters) } +} diff --git a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/cards/data/local/CardsDataSource.kt b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/cards/data/local/CardsDataSource.kt index 028ef32..fbe870a 100644 --- a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/cards/data/local/CardsDataSource.kt +++ b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/cards/data/local/CardsDataSource.kt @@ -1,11 +1,9 @@ package io.github.jan.einkaufszettel.cards.data.local import app.cash.sqldelight.async.coroutines.awaitAsList -import app.cash.sqldelight.async.coroutines.awaitAsOne import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToList import einkaufszettel.GetAllCards -import einkaufszettel.GetCardById import io.github.jan.einkaufszettel.cards.data.remote.CardDto import io.github.jan.einkaufszettel.root.data.local.db.DatabaseProvider import kotlinx.coroutines.Dispatchers @@ -26,7 +24,7 @@ interface CardsDataSource { fun getCardById( id: Long - ): Flow + ): Flow suspend fun retrieveAllCards(): List @@ -69,8 +67,8 @@ internal class CardsDataSourceImpl( return queries.getAllCards().asFlow().mapToList(Dispatchers.Default) } - override fun getCardById(id: Long): Flow { - return queries.getCardById(id).asFlow().map { it.awaitAsOne() } + override fun getCardById(id: Long): Flow = getAllCards().map { cards -> + cards.find { it.id == id } } override suspend fun retrieveAllCards(): List { diff --git a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/profile/data/local/ProfileDataSource.kt b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/profile/data/local/ProfileDataSource.kt index bcc0dfd..aa9e4a8 100644 --- a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/profile/data/local/ProfileDataSource.kt +++ b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/profile/data/local/ProfileDataSource.kt @@ -1,6 +1,7 @@ package io.github.jan.einkaufszettel.profile.data.local import app.cash.sqldelight.async.coroutines.awaitAsList +import app.cash.sqldelight.async.coroutines.awaitAsOne import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToList @@ -16,6 +17,8 @@ interface ProfileDataSource { fun getProfiles(): Flow> + fun getOwnProfile(): Flow + suspend fun retrieveAllProfiles(): List suspend fun insertProfile(profile: ProfileDto) = insertProfiles(listOf(profile)) @@ -56,6 +59,10 @@ internal class ProfileDataSourceImpl( override suspend fun retrieveProfile(uid: String): ProfileDto? = queries.getProfileById(uid).awaitAsOneOrNull()?.toProfile() + override fun getOwnProfile(): Flow { + return auth.currentUserOrNull()?.id!!.let { id -> queries.getProfileById(id).asFlow().map { it.awaitAsOne().toProfile() } } + } + override suspend fun retrieveOwnProfile(): ProfileDto? = auth.currentUserOrNull()?.id?.let { retrieveProfile(it) } override suspend fun clear() { diff --git a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/data/local/RecipeDataSource.kt b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/data/local/RecipeDataSource.kt index a9edec8..4390833 100644 --- a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/data/local/RecipeDataSource.kt +++ b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/data/local/RecipeDataSource.kt @@ -8,11 +8,14 @@ import io.github.jan.einkaufszettel.recipes.data.remote.RecipeDto import io.github.jan.einkaufszettel.root.data.local.db.DatabaseProvider import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map interface RecipeDataSource { fun getAllRecipes(): Flow> + fun getRecipeById(recipeId: Long): Flow + suspend fun retrieveAllRecipes(): List suspend fun insertRecipe(recipe: RecipeDto) @@ -41,6 +44,10 @@ internal class RecipeDataSourceImpl( return queries.getAllRecipes().asFlow().mapToList(Dispatchers.Default) } + override fun getRecipeById(recipeId: Long): Flow = getAllRecipes().map { recipes -> + recipes.find { it.id == recipeId } + } + override suspend fun retrieveAllRecipes(): List { return queries.getAllRecipes().awaitAsList() } diff --git a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/data/remote/RecipeApi.kt b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/data/remote/RecipeApi.kt index 154accd..398cf5d 100644 --- a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/data/remote/RecipeApi.kt +++ b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/data/remote/RecipeApi.kt @@ -40,12 +40,11 @@ interface RecipeApi { suspend fun editRecipe( id: Long, - name: String, + name: String?, imagePath: String?, - ingredients: List, - steps: String, - private: Boolean, - ) + ingredients: List?, + steps: String?, + ): RecipeDto suspend fun retrieveRecipes(): List @@ -106,23 +105,22 @@ internal class RecipeApiImpl( override suspend fun editRecipe( id: Long, - name: String, + name: String?, imagePath: String?, - ingredients: List, - steps: String, - private: Boolean - ) { - table.update({ - RecipeDto::name setTo name - RecipeDto::imagePath setTo imagePath - RecipeDto::ingredients setTo ingredients - RecipeDto::steps setTo steps - RecipeDto::private setTo private + ingredients: List?, + steps: String?, + ): RecipeDto { + return table.update({ + name?.let { RecipeDto::name setTo it } + imagePath?.let { RecipeDto::imagePath setTo it } + ingredients?.let { RecipeDto::ingredients setTo it } + steps?.let { RecipeDto::steps setTo it } }) { + select() filter { RecipeDto::id eq id } - } + }.decodeSingle() } override suspend fun uploadImage(imagePath: String, image: ByteArray) { diff --git a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/create/components/RecipeList.kt b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/components/RecipeList.kt similarity index 93% rename from composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/create/components/RecipeList.kt rename to composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/components/RecipeList.kt index b586c96..cff201b 100644 --- a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/create/components/RecipeList.kt +++ b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/components/RecipeList.kt @@ -1,4 +1,4 @@ -package io.github.jan.einkaufszettel.recipes.ui.create.components +package io.github.jan.einkaufszettel.recipes.ui.components import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -24,9 +24,8 @@ import cafe.adriel.voyager.navigator.Navigator import io.github.jan.einkaufszettel.Res import io.github.jan.einkaufszettel.app.ui.components.DeleteDialog import io.github.jan.einkaufszettel.collectAsStateWithLifecycle -import io.github.jan.einkaufszettel.recipes.ui.components.RecipeCard -import io.github.jan.einkaufszettel.recipes.ui.components.RecipeCardDefaults import io.github.jan.einkaufszettel.recipes.ui.detail.RecipeDetailScreen +import io.github.jan.einkaufszettel.recipes.ui.edit.RecipeEditScreen import io.github.jan.einkaufszettel.recipes.ui.main.RecipeScreenModel @OptIn(ExperimentalMaterial3Api::class) @@ -68,7 +67,10 @@ fun RecipeList( modifier = Modifier.width(RecipeCardDefaults.WIDTH).height( RecipeCardDefaults.HEIGHT).padding(RecipeCardDefaults.PADDING), onClick = { - navigator.push(RecipeDetailScreen(it.id)) + navigator.push(RecipeDetailScreen(it.id)) //TODO: JS + }, + onEdit = { + navigator.push(RecipeEditScreen(it.id)) }, onDelete = { screenModel.onShowDeleteDialogChanged(it) diff --git a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/create/RecipeCreateS2Screen.kt b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/create/RecipeCreateS2Screen.kt deleted file mode 100644 index fda27cf..0000000 --- a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/create/RecipeCreateS2Screen.kt +++ /dev/null @@ -1,56 +0,0 @@ -package io.github.jan.einkaufszettel.recipes.ui.create - -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.koin.getNavigatorScreenModel -import cafe.adriel.voyager.navigator.LocalNavigator -import cafe.adriel.voyager.navigator.currentOrThrow -import com.darkrockstudios.libraries.mpfilepicker.FilePicker -import io.github.jan.einkaufszettel.collectAsStateWithLifecycle -import io.github.jan.einkaufszettel.root.ui.component.LocalImage - -object RecipeCreateS2Screen : RecipeCreateStepScreen { - - override val index: Int = 1 - - override val nextStep: RecipeCreateStepScreen = RecipeCreateS3Screen - - @Composable - override fun Content() { - val navigator = LocalNavigator.currentOrThrow - val model = navigator.parent!!.getNavigatorScreenModel() - val imageData by model.imageData.collectAsStateWithLifecycle() - var showImageDialog by remember { mutableStateOf(false) } - Box( - modifier = Modifier.padding(8.dp), - ) { - LocalImage( - imageData, - modifier = Modifier - .fillMaxSize() - .border(2.dp, MaterialTheme.colorScheme.onSurface) - .clickable { showImageDialog = true } - ) - } - FilePicker( - show = showImageDialog, - fileExtensions = listOf("png", "jpg", "jpeg"), - onFileSelected = { - showImageDialog = false - it?.platformFile?.let { file -> model.importNativeFile(file) } - } - ) - } - -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/create/RecipeCreateScreen.kt b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/create/RecipeCreateScreen.kt index c8af68a..b16cddf 100644 --- a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/create/RecipeCreateScreen.kt +++ b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/create/RecipeCreateScreen.kt @@ -2,6 +2,7 @@ package io.github.jan.einkaufszettel.recipes.ui.create import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material.icons.filled.Done @@ -24,6 +25,8 @@ import io.github.jan.einkaufszettel.app.ui.AppState import io.github.jan.einkaufszettel.app.ui.AppStateScreen import io.github.jan.einkaufszettel.app.ui.components.ChildTopBar import io.github.jan.einkaufszettel.recipes.ui.detail.RecipeDetailScreen +import io.github.jan.einkaufszettel.recipes.ui.steps.RecipeModifyS1Screen +import io.github.jan.einkaufszettel.recipes.ui.steps.RecipeModifyStepScreen import io.github.jan.einkaufszettel.root.ui.dialog.LoadingDialog object RecipeCreateScreen: AppStateScreen { @@ -37,8 +40,8 @@ object RecipeCreateScreen: AppStateScreen { @Composable override fun Content(screenModel: RecipeCreateScreenModel, state: AppState) { val pNavigator = LocalNavigator.currentOrThrow - Navigator(RecipeCreateS1Screen) { navigator -> - val currentStep = navigator.lastItem as RecipeCreateStepScreen + Navigator(RecipeModifyS1Screen(null)) { navigator -> + val currentStep = navigator.lastItem as RecipeModifyStepScreen Scaffold( modifier = Modifier.fillMaxSize(), floatingActionButton = { @@ -64,7 +67,7 @@ object RecipeCreateScreen: AppStateScreen { ChildTopBar(Res.string.create_recipe, pNavigator) } ) { - SlideTransition(navigator) { + SlideTransition(navigator, Modifier.padding(it)) { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally diff --git a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/create/RecipeCreateScreenModel.kt b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/create/RecipeCreateScreenModel.kt index 952c015..fa6c6ca 100644 --- a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/create/RecipeCreateScreenModel.kt +++ b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/create/RecipeCreateScreenModel.kt @@ -1,41 +1,28 @@ package io.github.jan.einkaufszettel.recipes.ui.create -import androidx.compose.runtime.mutableStateListOf import cafe.adriel.voyager.core.model.screenModelScope -import com.mohamedrejeb.richeditor.model.RichTextState import io.github.jan.einkaufszettel.app.ui.AppState -import io.github.jan.einkaufszettel.app.ui.AppStateModel import io.github.jan.einkaufszettel.recipes.data.local.RecipeDataSource import io.github.jan.einkaufszettel.recipes.data.remote.RecipeApi +import io.github.jan.einkaufszettel.recipes.ui.steps.RecipeModifyScreenModel import io.github.jan.einkaufszettel.root.data.local.image.LocalImageData import io.github.jan.einkaufszettel.root.data.local.image.LocalImageReader import io.github.jan.supabase.exceptions.RestException import io.github.jan.supabase.gotrue.Auth -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import kotlinx.datetime.Clock class RecipeCreateScreenModel( - private val recipeApi: RecipeApi, - private val recipeDataSource: RecipeDataSource, - private val localImageReader: LocalImageReader, - private val auth: Auth, -): AppStateModel() { + recipeApi: RecipeApi, + recipeDataSource: RecipeDataSource, + localImageReader: LocalImageReader, + auth: Auth, +): RecipeModifyScreenModel(recipeApi, recipeDataSource, localImageReader, auth) { sealed interface State: AppState { data class Success(val id: Long) : State } - private val _imageData = MutableStateFlow(null) - private val _name = MutableStateFlow("") - private val _showIngredientsDialog = MutableStateFlow(false) - val ingredients = mutableStateListOf() - val name = _name.asStateFlow() - val imageData = _imageData.asStateFlow() - val instructionState = RichTextState() - val showIngredientsDialog = _showIngredientsDialog.asStateFlow() - fun createRecipe( name: String, imageData: LocalImageData?, @@ -65,31 +52,4 @@ class RecipeCreateScreenModel( } } - fun importNativeFile(file: Any) { - screenModelScope.launch { - runCatching { - localImageReader.platformFileToLocalImage(file) - }.onSuccess { - _imageData.value = it - }.onFailure { - it.printStackTrace() - } - } - } - - fun setName(name: String) { - _name.value = name - } - - fun setShowIngredientsDialog(show: Boolean) { - _showIngredientsDialog.value = show - } - - fun resetContent() { - _name.value = "" - ingredients.clear() - instructionState.clear() - _showIngredientsDialog.value = false - } - } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/create/RecipeCreateStepScreen.kt b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/create/RecipeCreateStepScreen.kt deleted file mode 100644 index 27430cf..0000000 --- a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/create/RecipeCreateStepScreen.kt +++ /dev/null @@ -1,15 +0,0 @@ -package io.github.jan.einkaufszettel.recipes.ui.create - -import cafe.adriel.voyager.core.screen.Screen - -interface RecipeCreateStepScreen : Screen { - - val index: Int - - val nextStep: RecipeCreateStepScreen? - - companion object { - const val MAX_INDEX = 1 - } - -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/edit/RecipeEditScreen.kt b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/edit/RecipeEditScreen.kt new file mode 100644 index 0000000..b739fb4 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/edit/RecipeEditScreen.kt @@ -0,0 +1,132 @@ +package io.github.jan.einkaufszettel.recipes.ui.edit + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowForward +import androidx.compose.material.icons.filled.Done +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.navigator.currentOrThrow +import cafe.adriel.voyager.transitions.SlideTransition +import io.github.jan.einkaufszettel.Res +import io.github.jan.einkaufszettel.app.ui.AppState +import io.github.jan.einkaufszettel.app.ui.AppStateScreen +import io.github.jan.einkaufszettel.app.ui.components.ChildTopBar +import io.github.jan.einkaufszettel.collectAsStateWithLifecycle +import io.github.jan.einkaufszettel.getNavigatorScreenModelT +import io.github.jan.einkaufszettel.recipes.ui.detail.RecipeDetailScreen +import io.github.jan.einkaufszettel.recipes.ui.steps.RecipeModifyS1Screen +import io.github.jan.einkaufszettel.recipes.ui.steps.RecipeModifyStepScreen +import io.github.jan.einkaufszettel.root.ui.component.LoadingCircle +import io.github.jan.einkaufszettel.root.ui.dialog.LoadingDialog +import org.koin.core.parameter.parametersOf + +class RecipeEditScreen( + private val recipeId: Long +): AppStateScreen { + + @Composable + override fun createScreenModel(): RecipeEditScreenModel { + val pNavigator = LocalNavigator.currentOrThrow + return pNavigator.getNavigatorScreenModelT(tag = recipeId.toString(), parameters = { parametersOf(recipeId) }) + } + + @Composable + override fun Content(screenModel: RecipeEditScreenModel, state: AppState) { + val pNavigator = LocalNavigator.currentOrThrow + val recipe by screenModel.recipe.collectAsStateWithLifecycle() + if(recipe == null) { + LoadingCircle() + return + } + LaunchedEffect(Unit) { + screenModel.setName(recipe!!.name) + screenModel.ingredients.addAll(recipe!!.ingredients) + recipe!!.steps?.let { screenModel.instructionState.setHtml(it) } + } + Navigator(RecipeModifyS1Screen(recipe!!.id, recipe!!.imagePath)) { navigator -> + val currentStep = navigator.lastItem as RecipeModifyStepScreen + Scaffold( + modifier = Modifier.fillMaxSize(), + floatingActionButton = { + FloatingActionButton( + onClick = { + if(currentStep.nextStep != null) { + navigator.push(currentStep.nextStep!!) + } else { + screenModel.editRecipe( + name = screenModel.name.value, + steps = screenModel.instructionState.toHtml(), + ingredients = screenModel.ingredients.toList(), + imageData = screenModel.imageData.value, + oldImagePath = recipe!!.imagePath + ) + } + }, + ) { + Icon(if(currentStep.nextStep != null) Icons.AutoMirrored.Filled.ArrowForward else Icons.Filled.Done, contentDescription = Res.string.create) + } + }, + topBar = { + ChildTopBar(Res.string.create_recipe, pNavigator) + } + ) { + SlideTransition(navigator, Modifier.padding(it)) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + it.Content() + } + } + } + } + + when(state) { + is AppState.Loading -> { + LoadingDialog() + } + is RecipeEditScreenModel.State.Success -> { + RecipeEditedDialog { + screenModel.resetState() + screenModel.resetContent() + pNavigator.replace(RecipeDetailScreen(state.id)) + } + } + } + + } + + @Composable + private fun RecipeEditedDialog( + onClose: () -> Unit + ) { + AlertDialog( + onDismissRequest = onClose, + title = { Text(Res.string.recipe_edited) }, + text = { Text(Res.string.recipe_edited_message) }, + confirmButton = { + TextButton( + onClick = onClose + ) { + Text("Ok") + } + } + ) + } + + +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/edit/RecipeEditScreenModel.kt b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/edit/RecipeEditScreenModel.kt new file mode 100644 index 0000000..e02ca75 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/edit/RecipeEditScreenModel.kt @@ -0,0 +1,63 @@ +package io.github.jan.einkaufszettel.recipes.ui.edit + +import cafe.adriel.voyager.core.model.screenModelScope +import io.github.jan.einkaufszettel.app.ui.AppState +import io.github.jan.einkaufszettel.recipes.data.local.RecipeDataSource +import io.github.jan.einkaufszettel.recipes.data.remote.RecipeApi +import io.github.jan.einkaufszettel.recipes.ui.steps.RecipeModifyScreenModel +import io.github.jan.einkaufszettel.root.data.local.image.LocalImageData +import io.github.jan.einkaufszettel.root.data.local.image.LocalImageReader +import io.github.jan.supabase.exceptions.RestException +import io.github.jan.supabase.gotrue.Auth +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.datetime.Clock + +class RecipeEditScreenModel( + private val recipeId: Long, + recipeApi: RecipeApi, + recipeDataSource: RecipeDataSource, + localImageReader: LocalImageReader, + auth: Auth, +): RecipeModifyScreenModel(recipeApi, recipeDataSource, localImageReader, auth) { + + sealed interface State: AppState { + data class Success(val id: Long) : State + } + + val recipe = recipeDataSource.getRecipeById(recipeId).stateIn(screenModelScope, SharingStarted.Eagerly, null) + + fun editRecipe( + name: String?, + imageData: LocalImageData?, + oldImagePath: String?, + steps: String?, + ingredients: List?, + ) { + mutableState.value = AppState.Loading + screenModelScope.launch { + runCatching { + val imagePath = imageData?.let { + val imagePath = "${Clock.System.now().toEpochMilliseconds()}.${imageData.extension}" + recipeApi.uploadImage(imagePath, imageData.data) + imagePath + } + if(oldImagePath != null && imageData != null) { + recipeApi.deleteImage(oldImagePath) + } + recipeApi.editRecipe(recipeId, name, imagePath, ingredients, steps) + }.onSuccess { + recipeDataSource.insertRecipe(it) + mutableState.value = State.Success(it.id) + }.onFailure { + it.printStackTrace() + when(it) { + is RestException -> mutableState.value = AppState.Error(it.message ?: "") + else -> mutableState.value = AppState.NetworkError + } + } + } + } + +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/main/RecipeScreen.kt b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/main/RecipeScreen.kt index c161e21..f860221 100644 --- a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/main/RecipeScreen.kt +++ b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/main/RecipeScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.material3.AlertDialog import androidx.compose.material3.Scaffold @@ -25,8 +26,8 @@ import io.github.jan.einkaufszettel.app.ui.AppStateScreen import io.github.jan.einkaufszettel.app.ui.BlankScreen import io.github.jan.einkaufszettel.app.ui.components.CreateButton import io.github.jan.einkaufszettel.app.ui.pullrefresh.RefreshScope +import io.github.jan.einkaufszettel.recipes.ui.components.RecipeList import io.github.jan.einkaufszettel.recipes.ui.create.RecipeCreateScreen -import io.github.jan.einkaufszettel.recipes.ui.create.components.RecipeList import io.github.jan.einkaufszettel.recipes.ui.detail.RecipeDetailScreen import io.github.jan.einkaufszettel.root.ui.dialog.LoadingDialog import io.github.jan.einkaufszettel.shops.ui.components.VerticalDivider @@ -62,7 +63,9 @@ object RecipeScreen : AppStateScreen { } }, ) { - RecipeList(screenModel, navigator.parent!!, listState) + Box(Modifier.fillMaxSize().padding(it)) { + RecipeList(screenModel, navigator.parent!!, listState) + } } AnimatedVisibility( diff --git a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/create/RecipeCreateS1Screen.kt b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/steps/RecipeModifyS1Screen.kt similarity index 64% rename from composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/create/RecipeCreateS1Screen.kt rename to composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/steps/RecipeModifyS1Screen.kt index c652407..4c10207 100644 --- a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/create/RecipeCreateS1Screen.kt +++ b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/steps/RecipeModifyS1Screen.kt @@ -1,4 +1,4 @@ -package io.github.jan.einkaufszettel.recipes.ui.create +package io.github.jan.einkaufszettel.recipes.ui.steps import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.padding @@ -15,19 +15,30 @@ import cafe.adriel.voyager.navigator.currentOrThrow import com.mohamedrejeb.richeditor.ui.material3.OutlinedRichTextEditor import io.github.jan.einkaufszettel.Res import io.github.jan.einkaufszettel.collectAsStateWithLifecycle +import io.github.jan.einkaufszettel.getNavigatorScreenModelT +import io.github.jan.einkaufszettel.recipes.ui.create.RecipeCreateScreenModel import io.github.jan.einkaufszettel.recipes.ui.create.components.RichTextStyleRow +import io.github.jan.einkaufszettel.recipes.ui.edit.RecipeEditScreenModel +import org.koin.core.parameter.parametersOf -object RecipeCreateS1Screen: RecipeCreateStepScreen { +class RecipeModifyS1Screen( + private val oldRecipeId: Long? = null, + oldRecipeImage: String? = null +): RecipeModifyStepScreen { override val index: Int = 0 - override val nextStep: RecipeCreateStepScreen = RecipeCreateS2Screen + override val nextStep: RecipeModifyStepScreen = RecipeModifyS2Screen(oldRecipeId = oldRecipeId, oldRecipeImage = oldRecipeImage) @OptIn(ExperimentalMaterial3Api::class) @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow - val model = navigator.parent!!.getNavigatorScreenModel() + val model = if(oldRecipeId != null) { + navigator.parent!!.getNavigatorScreenModelT(tag = oldRecipeId.toString(), parameters = { parametersOf(oldRecipeId) }) + } else { + navigator.parent!!.getNavigatorScreenModel() + } val name by model.name.collectAsStateWithLifecycle() OutlinedTextField( value = name, diff --git a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/steps/RecipeModifyS2Screen.kt b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/steps/RecipeModifyS2Screen.kt new file mode 100644 index 0000000..280aab2 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/steps/RecipeModifyS2Screen.kt @@ -0,0 +1,88 @@ +package io.github.jan.einkaufszettel.recipes.ui.steps + +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import cafe.adriel.voyager.koin.getNavigatorScreenModel +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import coil3.compose.AsyncImage +import coil3.compose.LocalPlatformContext +import coil3.request.ImageRequest +import coil3.request.crossfade +import com.darkrockstudios.libraries.mpfilepicker.FilePicker +import io.github.jan.einkaufszettel.collectAsStateWithLifecycle +import io.github.jan.einkaufszettel.getNavigatorScreenModelT +import io.github.jan.einkaufszettel.recipes.data.remote.RecipeApi +import io.github.jan.einkaufszettel.recipes.ui.create.RecipeCreateScreenModel +import io.github.jan.einkaufszettel.recipes.ui.edit.RecipeEditScreenModel +import io.github.jan.einkaufszettel.root.ui.component.LocalImage +import io.github.jan.supabase.storage.publicStorageItem +import org.koin.core.parameter.parametersOf + +class RecipeModifyS2Screen( + private val oldRecipeId: Long? = null, + private val oldRecipeImage: String? = null +): RecipeModifyStepScreen { + + override val index: Int = 1 + + override val nextStep: RecipeModifyStepScreen = RecipeModifyS3Screen(oldRecipeId = oldRecipeId, oldRecipeImage = oldRecipeImage) + + @Composable + override fun Content() { + val navigator = LocalNavigator.currentOrThrow + val model = if(oldRecipeId != null) { + navigator.parent!!.getNavigatorScreenModelT(tag = oldRecipeId.toString(), parameters = { parametersOf(oldRecipeId) }) + } else { + navigator.parent!!.getNavigatorScreenModel() + } + val imageData by model.imageData.collectAsStateWithLifecycle() + var showImageDialog by remember { mutableStateOf(false) } + Box( + modifier = Modifier.padding(8.dp), + ) { + if(imageData == null && oldRecipeImage != null) { + val request = ImageRequest.Builder(LocalPlatformContext.current) + .data(publicStorageItem(RecipeApi.BUCKET_ID, oldRecipeImage)) + .crossfade(true) + .build() + AsyncImage( + model = request, + contentDescription = null, + modifier = Modifier + .fillMaxSize() + .border(2.dp, MaterialTheme.colorScheme.onSurface) + .clickable { showImageDialog = true } + ) + } else { + LocalImage( + imageData, + modifier = Modifier + .fillMaxSize() + .border(2.dp, MaterialTheme.colorScheme.onSurface) + .clickable { showImageDialog = true } + ) + } + } + FilePicker( + show = showImageDialog, + fileExtensions = listOf("png", "jpg", "jpeg"), + onFileSelected = { + showImageDialog = false + it?.platformFile?.let { file -> model.importNativeFile(file) } + } + ) + } + +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/create/RecipeCreateS3Screen.kt b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/steps/RecipeModifyS3Screen.kt similarity index 84% rename from composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/create/RecipeCreateS3Screen.kt rename to composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/steps/RecipeModifyS3Screen.kt index 3757c9a..115e7ee 100644 --- a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/create/RecipeCreateS3Screen.kt +++ b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/steps/RecipeModifyS3Screen.kt @@ -1,4 +1,4 @@ -package io.github.jan.einkaufszettel.recipes.ui.create +package io.github.jan.einkaufszettel.recipes.ui.steps import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -32,16 +32,27 @@ import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import io.github.jan.einkaufszettel.Res import io.github.jan.einkaufszettel.collectAsStateWithLifecycle +import io.github.jan.einkaufszettel.getNavigatorScreenModelT +import io.github.jan.einkaufszettel.recipes.ui.create.RecipeCreateScreenModel +import io.github.jan.einkaufszettel.recipes.ui.edit.RecipeEditScreenModel +import org.koin.core.parameter.parametersOf -object RecipeCreateS3Screen: RecipeCreateStepScreen { +class RecipeModifyS3Screen( + private val oldRecipeId: Long? = null, + private val oldRecipeImage: String? = null +): RecipeModifyStepScreen { override val index: Int = 2 - override val nextStep: RecipeCreateStepScreen? = null + override val nextStep: RecipeModifyStepScreen? = null @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow - val model = navigator.parent!!.getNavigatorScreenModel() + val model = if(oldRecipeId != null) { + navigator.parent!!.getNavigatorScreenModelT(tag = oldRecipeId.toString(), parameters = { parametersOf(oldRecipeId) }) + } else { + navigator.parent!!.getNavigatorScreenModel() + } val showIngredientDialog by model.showIngredientsDialog.collectAsStateWithLifecycle() Box( modifier = Modifier.fillMaxWidth() diff --git a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/steps/RecipeModifyScreenModel.kt b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/steps/RecipeModifyScreenModel.kt new file mode 100644 index 0000000..0a27b8f --- /dev/null +++ b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/steps/RecipeModifyScreenModel.kt @@ -0,0 +1,59 @@ +package io.github.jan.einkaufszettel.recipes.ui.steps + +import androidx.compose.runtime.mutableStateListOf +import cafe.adriel.voyager.core.model.screenModelScope +import com.mohamedrejeb.richeditor.model.RichTextState +import io.github.jan.einkaufszettel.app.ui.AppStateModel +import io.github.jan.einkaufszettel.recipes.data.local.RecipeDataSource +import io.github.jan.einkaufszettel.recipes.data.remote.RecipeApi +import io.github.jan.einkaufszettel.root.data.local.image.LocalImageData +import io.github.jan.einkaufszettel.root.data.local.image.LocalImageReader +import io.github.jan.supabase.gotrue.Auth +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +open class RecipeModifyScreenModel( + protected val recipeApi: RecipeApi, + protected val recipeDataSource: RecipeDataSource, + private val localImageReader: LocalImageReader, + protected val auth: Auth, +): AppStateModel() { + + private val _imageData = MutableStateFlow(null) + private val _name = MutableStateFlow("") + private val _showIngredientsDialog = MutableStateFlow(false) + val ingredients = mutableStateListOf() + val name = _name.asStateFlow() + val imageData = _imageData.asStateFlow() + val instructionState = RichTextState() + val showIngredientsDialog = _showIngredientsDialog.asStateFlow() + + fun importNativeFile(file: Any) { + screenModelScope.launch { + runCatching { + localImageReader.platformFileToLocalImage(file) + }.onSuccess { + _imageData.value = it + }.onFailure { + it.printStackTrace() + } + } + } + + fun setName(name: String) { + _name.value = name + } + + fun setShowIngredientsDialog(show: Boolean) { + _showIngredientsDialog.value = show + } + + fun resetContent() { + _name.value = "" + ingredients.clear() + instructionState.clear() + _showIngredientsDialog.value = false + } + +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/steps/RecipeModifyStepScreen.kt b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/steps/RecipeModifyStepScreen.kt new file mode 100644 index 0000000..0728a27 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/recipes/ui/steps/RecipeModifyStepScreen.kt @@ -0,0 +1,15 @@ +package io.github.jan.einkaufszettel.recipes.ui.steps + +import cafe.adriel.voyager.core.screen.Screen + +interface RecipeModifyStepScreen : Screen { + + val index: Int + + val nextStep: RecipeModifyStepScreen? + + companion object { + const val MAX_INDEX = 1 + } + +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/root/di/models/recipeModels.kt b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/root/di/models/recipeModels.kt index 68f53a6..1c3bf40 100644 --- a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/root/di/models/recipeModels.kt +++ b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/root/di/models/recipeModels.kt @@ -2,6 +2,7 @@ package io.github.jan.einkaufszettel.root.di.models import io.github.jan.einkaufszettel.recipes.ui.create.RecipeCreateScreenModel import io.github.jan.einkaufszettel.recipes.ui.detail.RecipeDetailScreenModel +import io.github.jan.einkaufszettel.recipes.ui.edit.RecipeEditScreenModel import io.github.jan.einkaufszettel.recipes.ui.main.RecipeScreenModel import org.koin.dsl.module @@ -12,6 +13,9 @@ val recipeModels = module { factory { parameters -> RecipeDetailScreenModel(parameters.get(), get()) } + factory { parameters -> + RecipeEditScreenModel(parameters.get(), get(), get(), get(), get()) + } factory { RecipeCreateScreenModel(get(), get(), get(), get()) } diff --git a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/root/di/models/settingsModels.kt b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/root/di/models/settingsModels.kt index 5cf078e..9422820 100644 --- a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/root/di/models/settingsModels.kt +++ b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/root/di/models/settingsModels.kt @@ -6,6 +6,9 @@ import org.koin.dsl.module val settingsModels = module { factory { SettingsScreenModel( + get(), + get(), + get(), get(), get(), get(), diff --git a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/settings/ui/SettingsScreenModel.kt b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/settings/ui/SettingsScreenModel.kt index b2e320a..3740f1f 100644 --- a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/settings/ui/SettingsScreenModel.kt +++ b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/settings/ui/SettingsScreenModel.kt @@ -3,18 +3,28 @@ package io.github.jan.einkaufszettel.settings.ui import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.screenModelScope import io.github.jan.einkaufszettel.auth.data.remote.AuthenticationApi +import io.github.jan.einkaufszettel.profile.data.local.ProfileDataSource +import io.github.jan.einkaufszettel.profile.data.remote.ProfileApi import io.github.jan.einkaufszettel.recipes.data.local.RecipeDataSource import io.github.jan.einkaufszettel.shops.data.local.ProductDataSource import io.github.jan.einkaufszettel.shops.data.local.ShopDataSource +import io.github.jan.supabase.gotrue.Auth +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch class SettingsScreenModel( private val authenticationApi: AuthenticationApi, private val recipeDataSource: RecipeDataSource, private val productDataSource: ProductDataSource, - private val shopDataSource: ShopDataSource + private val shopDataSource: ShopDataSource, + private val profileDataSource: ProfileDataSource, + private val profileApi: ProfileApi, + private val auth: Auth ): ScreenModel { + val userProfile = profileDataSource.getOwnProfile().stateIn(screenModelScope, SharingStarted.Eagerly, null) + fun logout() { screenModelScope.launch { runCatching { diff --git a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/settings/ui/SettingsTab.kt b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/settings/ui/SettingsTab.kt index 7de9a67..1bf36dc 100644 --- a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/settings/ui/SettingsTab.kt +++ b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/settings/ui/SettingsTab.kt @@ -1,16 +1,36 @@ package io.github.jan.einkaufszettel.settings.ui +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Logout import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.TabOptions import io.github.jan.einkaufszettel.Res +import io.github.jan.einkaufszettel.collectAsStateWithLifecycle import io.github.jan.einkaufszettel.getScreenModel +import io.github.jan.einkaufszettel.root.ui.component.LoadingCircle data object SettingsTab: Tab { @@ -26,13 +46,74 @@ data object SettingsTab: Tab { @Composable override fun Content() { val screenModel = getScreenModel() - Button( - onClick = { - screenModel.logout() + var showLogoutDialog by remember { mutableStateOf(false) } + val profile by screenModel.userProfile.collectAsStateWithLifecycle() + if(profile == null) { + LoadingCircle() + return + } + Scaffold( + floatingActionButton = { + ExtendedFloatingActionButton( + onClick = { showLogoutDialog = true }, + text = { Text(Res.string.logout) }, + icon = { Icon(Icons.AutoMirrored.Filled.Logout, null) }, + containerColor = MaterialTheme.colorScheme.errorContainer + ) } ) { - Text("Logout") + Column( + modifier = Modifier.fillMaxSize().padding(it).padding(start = 6.dp) + ) { + Text(Res.string.account, style = MaterialTheme.typography.headlineMedium) + Row(Modifier.fillMaxWidth(), verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) { + Text("${Res.string.name}: ", fontWeight = FontWeight.Bold) + Text(profile!!.username) + TextButton(onClick = { /*TODO*/ }) { + Text(Res.string.change) + } + } + Button(onClick = { /*TODO*/ }) { + Text(Res.string.change_password) + } + } + } + + if(showLogoutDialog) { + LogoutDialog( + onDismiss = { showLogoutDialog = false }, + onLogout = screenModel::logout + ) } } + @Composable + fun LogoutDialog( + onDismiss: () -> Unit, + onLogout: () -> Unit + ) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(Res.string.logout) }, + text = { Text(Res.string.logout_confirmation) }, + confirmButton = { + TextButton( + onClick = { + onLogout() + onDismiss() + } + ) { + Text(Res.string.yes) + } + }, + dismissButton = { + TextButton( + onClick = onDismiss + ) { + Text(Res.string.cancel) + } + } + ) + } + } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/shops/data/local/ShopDataSource.kt b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/shops/data/local/ShopDataSource.kt index 7505fa8..2073d7c 100644 --- a/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/shops/data/local/ShopDataSource.kt +++ b/composeApp/src/commonMain/kotlin/io/github/jan/einkaufszettel/shops/data/local/ShopDataSource.kt @@ -1,7 +1,6 @@ package io.github.jan.einkaufszettel.shops.data.local import app.cash.sqldelight.async.coroutines.awaitAsList -import app.cash.sqldelight.async.coroutines.awaitAsOne import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToList import einkaufszettel.ShopTable @@ -94,8 +93,8 @@ internal class ShopDataSourceImpl( queries.changePinned(if (pinned) 1L else 0L, id) } - override fun getShopById(id: Long): Flow { - return queries.getShopById(id).asFlow().map { it.awaitAsOne().toShopDto() } + override fun getShopById(id: Long): Flow = getAllShops().map { shops -> + shops.find { it.id == id }!! } } diff --git a/composeApp/src/commonMain/libres/strings/strings_en.xml b/composeApp/src/commonMain/libres/strings/strings_en.xml index a11df66..e704797 100644 --- a/composeApp/src/commonMain/libres/strings/strings_en.xml +++ b/composeApp/src/commonMain/libres/strings/strings_en.xml @@ -54,6 +54,8 @@ Delete Recipe Are you sure you want to delete this recipe? Recipe deleted + Recipe edited + Your recipe has been edited successfully. Your recipe has been deleted successfully. Create Card @@ -79,6 +81,12 @@ Delete Cancel + Account + Change + Change Password + Logout + Yes + Are you sure you want to log out? Error Unknown error: ${message} Network error. Please check your internet connection. diff --git a/composeApp/src/commonMain/sqldelight/einkaufszettel/cardTable.sq b/composeApp/src/commonMain/sqldelight/einkaufszettel/cardTable.sq index cf33d23..94ad0c2 100644 --- a/composeApp/src/commonMain/sqldelight/einkaufszettel/cardTable.sq +++ b/composeApp/src/commonMain/sqldelight/einkaufszettel/cardTable.sq @@ -18,13 +18,6 @@ FROM cardTable AS cardEntry LEFT JOIN profileTable ownerEntry ON cardEntry.ownerId = ownerEntry.id; -getCardById: -SELECT cardEntry.id, description, createdAt, authorizedUsers, imagePath, ownerEntry.username AS owner, ownerId -FROM cardTable AS cardEntry -LEFT JOIN profileTable ownerEntry -ON cardEntry.ownerId = ownerEntry.id -WHERE cardEntry.id = :id; - insertCard: INSERT OR REPLACE INTO cardTable diff --git a/composeApp/src/commonMain/sqldelight/einkaufszettel/recipeTable.sq b/composeApp/src/commonMain/sqldelight/einkaufszettel/recipeTable.sq index 84fdd6c..0b143ff 100644 --- a/composeApp/src/commonMain/sqldelight/einkaufszettel/recipeTable.sq +++ b/composeApp/src/commonMain/sqldelight/einkaufszettel/recipeTable.sq @@ -20,13 +20,6 @@ FROM recipeTable AS recipe LEFT JOIN profileTable creator ON recipe.creatorId = creator.id; -getRecipeById: -SELECT recipe.id, recipe.createdAt, recipe.name, recipe.imagePath, recipe.ingredients, recipe.steps, recipe.isPrivate, creator.username AS creator, recipe.creatorId AS creatorId -FROM recipeTable AS recipe -LEFT JOIN profileTable creator -ON recipe.creatorId = creator.id -WHERE recipe.id = ?; - insertRecipe: INSERT OR REPLACE INTO recipeTable VALUES(?, ?, ?, ?, ?, ?, ?, ?); diff --git a/composeApp/src/commonMain/sqldelight/einkaufszettel/shopTable.sq b/composeApp/src/commonMain/sqldelight/einkaufszettel/shopTable.sq index 338fa7f..5e6973d 100644 --- a/composeApp/src/commonMain/sqldelight/einkaufszettel/shopTable.sq +++ b/composeApp/src/commonMain/sqldelight/einkaufszettel/shopTable.sq @@ -17,10 +17,6 @@ CREATE TABLE shopTable ( getAllShops: SELECT * FROM shopTable; -getShopById: -SELECT * FROM shopTable -WHERE id = :id; - insertShop: INSERT OR REPLACE INTO shopTable