Merge branch 'master' of git.reim.ar:ReiMerc/easyeat

This commit is contained in:
Jeas0001 2025-05-14 09:04:35 +02:00
commit bd486fa9f7
29 changed files with 560 additions and 240 deletions

View File

@ -45,7 +45,7 @@
android:exported="false" /> android:exported="false" />
<activity <activity
android:name=".ui.dishes.CreateDishAIActivity" android:name=".ui.dishes.GenerateRecipeActivity"
android:exported="false" /> android:exported="false" />
<activity <activity

View File

@ -3,11 +3,8 @@ package tech.mercantec.easyeat.helpers
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import tech.mercantec.easyeat.models.CreateDirection
import tech.mercantec.easyeat.models.CreateIngredient
import tech.mercantec.easyeat.models.CreateRecipe
import tech.mercantec.easyeat.models.Direction
import tech.mercantec.easyeat.models.Ingredient import tech.mercantec.easyeat.models.Ingredient
import tech.mercantec.easyeat.models.Recipe
@Serializable @Serializable
data class LoginRequest(val emailUsr: String, val password: String) data class LoginRequest(val emailUsr: String, val password: String)
@ -20,7 +17,7 @@ fun login(ctx: Context, email: String, password: String) {
val response = requestJson<LoginRequest, LoginResponse>(ctx, "POST", "/api/User/login", request) val response = requestJson<LoginRequest, LoginResponse>(ctx, "POST", "/api/User/login", request)
with (ctx.getSharedPreferences("easyeat", Context.MODE_PRIVATE).edit()) { with(ctx.getSharedPreferences("easyeat", Context.MODE_PRIVATE).edit()) {
putInt("user-id", response.id) putInt("user-id", response.id)
putString("username", response.userName) putString("username", response.userName)
putString("auth-token", response.token) putString("auth-token", response.token)
@ -30,7 +27,7 @@ fun login(ctx: Context, email: String, password: String) {
} }
fun logout(ctx: Context) { fun logout(ctx: Context) {
with (ctx.getSharedPreferences("easyeat", Context.MODE_PRIVATE).edit()) { with(ctx.getSharedPreferences("easyeat", Context.MODE_PRIVATE).edit()) {
remove("user-id") remove("user-id")
remove("username") remove("username")
remove("auth-token") remove("auth-token")
@ -116,24 +113,10 @@ fun changePassword(ctx: Context, oldPassword: String, newPassword: String) {
} }
@Serializable @Serializable
data class CreateRecipeRequest(val name: String, val description: String, val directions: List<CreateDirection>, val ingredients: List<CreateIngredient>) data class ShoppingListAddRecipeRequest(val id: Int, val multiplier: Int)
fun createRecipe(ctx: Context, recipe: CreateRecipe) { fun AddRecipeToShoppingList(ctx: Context, id: Int, multiplier: Int) {
val request = CreateRecipeRequest(recipe.name, recipe.description, recipe.directions, recipe.ingredients) val request = ShoppingListAddRecipeRequest(id, multiplier)
requestJson<CreateRecipeRequest, Boolean>(ctx, "POST", "/api/recipe/create", request) return requestJson<ShoppingListAddRecipeRequest, Unit>(ctx, "POST", "/api/ShoppingList/recipeadd", request)
}
@Serializable
data class GetAllRecipesResponse(val id: Int, val name: String, val description: String)
fun getAllRecipies(ctx: Context): List<GetAllRecipesResponse> {
return requestJson<Unit, List<GetAllRecipesResponse>>(ctx, "GET", "/api/Recipe/getall", null)
}
@Serializable
data class RecipeDetailsResponse(val id: Int, val name: String, val description: String, val directions: List<Direction>, val ingredients: List<Ingredient>)
fun getRecipeDetails(ctx: Context, id: Int): RecipeDetailsResponse {
return requestJson<Unit, RecipeDetailsResponse>(ctx, "GET", "/api/Recipe/get/$id", null)
} }

View File

@ -2,6 +2,7 @@ package tech.mercantec.easyeat.helpers
import android.content.Context import android.content.Context
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import tech.mercantec.easyeat.models.Ingredient
import tech.mercantec.easyeat.models.Recipe import tech.mercantec.easyeat.models.Recipe
@Serializable @Serializable
@ -10,3 +11,22 @@ data class GenerateRecipeRequest(val dish: String, val language: String, val num
fun generateRecipeWithAI(ctx: Context, title: String, language: String): Recipe { fun generateRecipeWithAI(ctx: Context, title: String, language: String): Recipe {
return requestJson<GenerateRecipeRequest, Recipe>(ctx, "POST", "/api/Recipe/chatbot", GenerateRecipeRequest(title, language, 1, arrayOf())) return requestJson<GenerateRecipeRequest, Recipe>(ctx, "POST", "/api/Recipe/chatbot", GenerateRecipeRequest(title, language, 1, arrayOf()))
} }
fun createRecipe(ctx: Context, recipe: Recipe) {
requestJson<Recipe, Boolean>(ctx, "POST", "/api/recipe/create", recipe)
}
@Serializable
data class GetAllRecipesResponse(val id: Int, val name: String, val description: String)
fun getAllRecipes(ctx: Context): List<GetAllRecipesResponse> {
return requestJson<Unit, List<GetAllRecipesResponse>>(ctx, "GET", "/api/Recipe/getall", null)
}
@Serializable
data class RecipeDetailsResponse(val id: Int, val name: String, val description: String, val directions: List<String>, val ingredients: List<Ingredient>)
fun getRecipeDetails(ctx: Context, id: Int): RecipeDetailsResponse {
return requestJson<Unit, RecipeDetailsResponse>(ctx, "GET", "/api/Recipe/get/$id", null)
}

View File

@ -8,13 +8,12 @@ fun getShoppingList(ctx: Context): Array<ShoppingListItem> {
return requestJson<Unit, Array<ShoppingListItem>>(ctx, "GET", "/api/ShoppingList/get", null) return requestJson<Unit, Array<ShoppingListItem>>(ctx, "GET", "/api/ShoppingList/get", null)
} }
@Serializable fun addShoppingItem(ctx: Context, item: ShoppingListItem): ShoppingListItem {
data class AddShoppingItemRequest(val name: String, val amount: Double?, val unit: String?, val checked: Boolean) return requestJson<ShoppingListItem, ShoppingListItem>(ctx, "POST", "/api/ShoppingList/add", item)
}
fun addShoppingItem(ctx: Context, name: String, amount: Double?, unit: String?): ShoppingListItem { fun editShoppingItem(ctx: Context, old: ShoppingListItem, new: ShoppingListItem) {
val request = AddShoppingItemRequest(name, amount, unit, false) requestJson<ShoppingListItem, Boolean>(ctx, "PUT", "/api/ShoppingList/update?itemId=${old.id}", new)
return requestJson<AddShoppingItemRequest, ShoppingListItem>(ctx, "POST", "/api/ShoppingList/add", request)
} }
fun toggleShoppingItemChecked(ctx: Context, item: ShoppingListItem) { fun toggleShoppingItemChecked(ctx: Context, item: ShoppingListItem) {

View File

@ -1,24 +0,0 @@
package tech.mercantec.easyeat.models
import kotlinx.serialization.Serializable
@Serializable
data class CreateRecipe(
val name: String,
val description: String,
val directions: List<CreateDirection>,
val ingredients: List<CreateIngredient>
)
@Serializable
data class CreateDirection(
val instructions: String
)
@Serializable
data class CreateIngredient(
val amount: Double?,
val unit: String?,
val name: String
)

View File

@ -3,4 +3,10 @@ package tech.mercantec.easyeat.models
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class ShoppingListItem(val id: Int, var name: String, var amount: Double?, var unit: String?, var checked: Boolean) data class ShoppingListItem(
var id: Int? = null,
var name: String,
var amount: Double?,
var unit: String?,
var checked: Boolean = false,
)

View File

@ -2,10 +2,9 @@ package tech.mercantec.easyeat.models
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class Recipe( data class Recipe(
val id: Int, val id: Int? = null,
val name: String, val name: String,
val description: String, val description: String,
val directions: List<String>, val directions: List<String>,
@ -14,7 +13,7 @@ data class Recipe(
@Serializable @Serializable
data class Ingredient( data class Ingredient(
val id: Int, val id: Int? = null,
val amount: Double?, val amount: Double?,
val unit: String?, val unit: String?,
val name: String val name: String

View File

@ -2,9 +2,7 @@ package tech.mercantec.easyeat.ui.dishes
import android.app.Activity import android.app.Activity
import android.app.ProgressDialog import android.app.ProgressDialog
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.Button import android.widget.Button
@ -14,15 +12,11 @@ import android.widget.LinearLayout
import android.widget.Spinner import android.widget.Spinner
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContentProviderCompat.requireContext
import tech.mercantec.easyeat.R import tech.mercantec.easyeat.R
import tech.mercantec.easyeat.helpers.ApiRequestException import tech.mercantec.easyeat.helpers.ApiRequestException
import tech.mercantec.easyeat.helpers.changePassword
import tech.mercantec.easyeat.helpers.createRecipe import tech.mercantec.easyeat.helpers.createRecipe
import tech.mercantec.easyeat.helpers.request import tech.mercantec.easyeat.models.Ingredient
import tech.mercantec.easyeat.models.CreateDirection import tech.mercantec.easyeat.models.Recipe
import tech.mercantec.easyeat.models.CreateIngredient
import tech.mercantec.easyeat.models.CreateRecipe
import kotlin.concurrent.thread import kotlin.concurrent.thread
class CreateDishActivity : AppCompatActivity() { class CreateDishActivity : AppCompatActivity() {
@ -53,7 +47,7 @@ class CreateDishActivity : AppCompatActivity() {
.map { line -> line.trim() } .map { line -> line.trim() }
.filter { it.isNotEmpty() } .filter { it.isNotEmpty() }
val recipe = CreateRecipe( val recipe = Recipe(
name = findViewById<EditText>(R.id.dishName).text.toString().trim(), name = findViewById<EditText>(R.id.dishName).text.toString().trim(),
description = findViewById<EditText>(R.id.dishDescription).text.toString().trim(), description = findViewById<EditText>(R.id.dishDescription).text.toString().trim(),
directions = directions, directions = directions,
@ -107,8 +101,8 @@ class CreateDishActivity : AppCompatActivity() {
ingredientContainer.addView(ingredientRow) ingredientContainer.addView(ingredientRow)
} }
private fun collectIngredients(): List<CreateIngredient> { private fun collectIngredients(): List<Ingredient> {
val ingredients = mutableListOf<CreateIngredient>() val ingredients = mutableListOf<Ingredient>()
for (i in 0 until ingredientContainer.childCount) { for (i in 0 until ingredientContainer.childCount) {
val ingredientView = ingredientContainer.getChildAt(i) val ingredientView = ingredientContainer.getChildAt(i)
@ -125,7 +119,7 @@ class CreateDishActivity : AppCompatActivity() {
// Optional: Only add non-empty rows // Optional: Only add non-empty rows
if (element.isNotEmpty() && amount.isNotEmpty()) { if (element.isNotEmpty() && amount.isNotEmpty()) {
ingredients.add(CreateIngredient(name = element, amount = amount.toDouble(), unit = unit)) ingredients.add(Ingredient(name = element, amount = amount.toDouble(), unit = unit))
} }
} }

View File

@ -1,7 +1,14 @@
package tech.mercantec.easyeat.ui.dishes package tech.mercantec.easyeat.ui.dishes
import android.app.Activity
import android.app.ProgressDialog
import android.os.Bundle import android.os.Bundle
import android.text.Html
import android.util.Log import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.LinearLayout
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import tech.mercantec.easyeat.R import tech.mercantec.easyeat.R
@ -9,14 +16,21 @@ import tech.mercantec.easyeat.helpers.ApiRequestException
import tech.mercantec.easyeat.helpers.RecipeDetailsResponse import tech.mercantec.easyeat.helpers.RecipeDetailsResponse
import tech.mercantec.easyeat.helpers.getRecipeDetails import tech.mercantec.easyeat.helpers.getRecipeDetails
import kotlin.concurrent.thread import kotlin.concurrent.thread
import android.widget.ListView import android.widget.EditText
import android.widget.ImageButton
import android.widget.TextView import android.widget.TextView
import androidx.core.widget.doAfterTextChanged
import tech.mercantec.easyeat.helpers.AddRecipeToShoppingList
import tech.mercantec.easyeat.models.Ingredient
class DishDetailsActivity : AppCompatActivity() { class DishDetailsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_dish_details) setContentView(R.layout.activity_dish_details)
val ingredientsContainer = findViewById<LinearLayout>(R.id.ingredients)
val multiplierEditText = findViewById<EditText>(R.id.ingredient_multiplier)
val dishId = intent.getIntExtra("dish_id", -1) val dishId = intent.getIntExtra("dish_id", -1)
if (dishId == -1) { if (dishId == -1) {
Toast.makeText(this, "No dish ID provided", Toast.LENGTH_SHORT).show() Toast.makeText(this, "No dish ID provided", Toast.LENGTH_SHORT).show()
@ -35,20 +49,108 @@ class DishDetailsActivity : AppCompatActivity() {
return@thread return@thread
} }
Log.i("DISH", recipe.ingredients.toString()) Log.i("DISH", recipe.ingredients.toString())
val instructionsLayout = findViewById<LinearLayout>(R.id.instructions)
// Example data: recipe.ingredients and recipe.directions
runOnUiThread { runOnUiThread {
// Set title and description
findViewById<TextView>(R.id.dishDetailName).text = recipe.name findViewById<TextView>(R.id.dishDetailName).text = recipe.name
findViewById<TextView>(R.id.dishDetailDescription).text = recipe.description findViewById<TextView>(R.id.dishDetailDescription).text = recipe.description
// Set up the ingredient list // Populate Instructions (if directions are strings)
val ingredientListView = findViewById<ListView>(R.id.dishDetailIngredients) recipe.directions.forEachIndexed { index, direction ->
val ingredientAdapter = IngredientAdapter(this, recipe.ingredients) val textView = TextView(this).apply {
ingredientListView.adapter = ingredientAdapter text = "${index + 1}. $direction"
textSize = 18f
val instructionsListView = findViewById<ListView>(R.id.dishDetailInstructions) setPadding(0, 8, 0, 8)
val instructionsAdapter = InstructionsAdapter(this, recipe.directions)
instructionsListView.adapter = instructionsAdapter
} }
instructionsLayout.addView(textView)
}
}
fun displayIngredients(ingredients: List<Ingredient>, multiplier: Int, container: LinearLayout) {
container.removeAllViews() // clear previous views
for (ingredient in ingredients) {
val row = TextView(this)
val amount = (ingredient.amount ?: 0.0) * multiplier
val amountStr = amount.toBigDecimal().stripTrailingZeros().toPlainString()
row.text = Html.fromHtml("&#8226; ${ingredient.name}: $amountStr ${ingredient.unit ?: ""}", Html.FROM_HTML_MODE_LEGACY)
row.textSize = 18f
row.setPadding(0, 8, 0, 8)
container.addView(row)
}
}
runOnUiThread {
val nameView = findViewById<TextView>(R.id.dishDetailName)
val descView = findViewById<TextView>(R.id.dishDetailDescription)
nameView.text = recipe.name
descView.text = recipe.description
// Default multiplier
var multiplier = 1
// Initial display
displayIngredients(recipe.ingredients, multiplier, ingredientsContainer)
// Listen for user input changes
multiplierEditText.doAfterTextChanged {
multiplier = it.toString().toIntOrNull() ?: 1
displayIngredients(recipe.ingredients, multiplier, ingredientsContainer)
}
findViewById<ImageButton>(R.id.increment_multiplier).setOnClickListener {
multiplier++
multiplierEditText.setText(multiplier.toString(), TextView.BufferType.EDITABLE)
displayIngredients(recipe.ingredients, multiplier, ingredientsContainer)
}
findViewById<ImageButton>(R.id.decrement_multiplier).setOnClickListener {
if (multiplier <= 1) return@setOnClickListener
multiplier--
multiplierEditText.setText(multiplier.toString(), TextView.BufferType.EDITABLE)
displayIngredients(recipe.ingredients, multiplier, ingredientsContainer)
}
// You can do the same for directions if needed
}
val saveButton: Button = findViewById(R.id.addDishToShoppingList)
saveButton.setOnClickListener {
val progressDialog = ProgressDialog(this)
progressDialog.setMessage("Loading...")
progressDialog.show()
thread {
try {
val multiplierEditText = findViewById<EditText>(R.id.ingredient_multiplier)
val multiplierText = multiplierEditText.text.toString()
AddRecipeToShoppingList(this, dishId, multiplierText.toIntOrNull() ?: 1)
} catch (e: ApiRequestException) {
runOnUiThread {
Toast.makeText(this, e.message, Toast.LENGTH_LONG).show()
}
return@thread
} finally {
runOnUiThread {
progressDialog.hide()
}
}
runOnUiThread {
Toast.makeText(this, "Password changed successfully", Toast.LENGTH_LONG).show()
}
setResult(Activity.RESULT_OK)
finish()
}
}
} }
} }
} }

View File

@ -6,7 +6,7 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button import android.widget.PopupMenu
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
@ -15,8 +15,9 @@ import tech.mercantec.easyeat.R
import tech.mercantec.easyeat.databinding.FragmentDishesBinding import tech.mercantec.easyeat.databinding.FragmentDishesBinding
import tech.mercantec.easyeat.helpers.ApiRequestException import tech.mercantec.easyeat.helpers.ApiRequestException
import tech.mercantec.easyeat.helpers.GetAllRecipesResponse import tech.mercantec.easyeat.helpers.GetAllRecipesResponse
import tech.mercantec.easyeat.helpers.getAllRecipies import tech.mercantec.easyeat.helpers.getAllRecipes
import tech.mercantec.easyeat.models.DishListItem import tech.mercantec.easyeat.models.DishListItem
import tech.mercantec.easyeat.models.Recipe
import kotlin.concurrent.thread import kotlin.concurrent.thread
class DishesFragment : Fragment() { class DishesFragment : Fragment() {
@ -36,28 +37,31 @@ class DishesFragment : Fragment() {
loadRecipes() loadRecipes()
binding.addDish.setOnClickListener { binding.addDish.setOnClickListener {
val dialogView = LayoutInflater.from(requireContext()).inflate(R.layout.create_dish_modal_dialog, null) val popup = PopupMenu(requireActivity(), it)
val dialog = android.app.AlertDialog.Builder(requireContext()) popup.apply {
.setView(dialogView) menuInflater.inflate(R.menu.create_dish_menu, menu)
.setCancelable(true) // tap outside to dismiss
.create()
dialog.window?.setBackgroundDrawableResource(android.R.color.transparent) setOnMenuItemClickListener {
when (it.itemId) {
dialogView.findViewById<Button>(R.id.createManualBtn).setOnClickListener { R.id.create_manually -> {
val intent = Intent(requireContext(), CreateDishActivity::class.java) val intent = Intent(requireContext(), CreateDishActivity::class.java)
createDishLauncher.launch(intent) createDishLauncher.launch(intent)
dialog.dismiss()
}
dialogView.findViewById<Button>(R.id.createAIBtn).setOnClickListener { true
val intent = Intent(requireContext(), CreateDishAIActivity::class.java) }
R.id.create_with_ai -> {
val intent = Intent(requireContext(), GenerateRecipeActivity::class.java)
createDishLauncher.launch(intent) createDishLauncher.launch(intent)
dialog.dismiss()
true
}
else -> false
}
} }
dialog.show() show()
}
} }
@ -68,7 +72,7 @@ class DishesFragment : Fragment() {
thread { thread {
val recipes: List<GetAllRecipesResponse> val recipes: List<GetAllRecipesResponse>
try { try {
recipes = getAllRecipies(requireContext()) recipes = getAllRecipes(requireContext())
} catch (e: ApiRequestException) { } catch (e: ApiRequestException) {
activity?.runOnUiThread { activity?.runOnUiThread {
Toast.makeText(requireContext(), e.message, Toast.LENGTH_LONG).show() Toast.makeText(requireContext(), e.message, Toast.LENGTH_LONG).show()

View File

@ -4,20 +4,20 @@ import android.app.ProgressDialog
import android.os.Bundle import android.os.Bundle
import android.widget.Button import android.widget.Button
import android.widget.EditText import android.widget.EditText
import android.widget.LinearLayout
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import tech.mercantec.easyeat.R import tech.mercantec.easyeat.R
import tech.mercantec.easyeat.helpers.ApiRequestException import tech.mercantec.easyeat.helpers.ApiRequestException
import tech.mercantec.easyeat.helpers.GenerateRecipeResponse import tech.mercantec.easyeat.helpers.createRecipe
import tech.mercantec.easyeat.helpers.generateRecipeWithAI import tech.mercantec.easyeat.helpers.generateRecipeWithAI
import tech.mercantec.easyeat.models.Recipe
import java.util.Locale import java.util.Locale
import kotlin.concurrent.thread import kotlin.concurrent.thread
class CreateDishAIActivity : AppCompatActivity() { class GenerateRecipeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.create_dish_ai_form) setContentView(R.layout.activity_generate_recipe)
findViewById<Button>(R.id.generate).setOnClickListener { findViewById<Button>(R.id.generate).setOnClickListener {
val name = findViewById<EditText>(R.id.dish_title).text.toString() val name = findViewById<EditText>(R.id.dish_title).text.toString()
@ -27,21 +27,25 @@ class CreateDishAIActivity : AppCompatActivity() {
progressDialog.show() progressDialog.show()
thread { thread {
val response: GenerateRecipeResponse // val response: GenerateRecipeResponse
try { try {
response = generateRecipeWithAI(this, name, Locale.getDefault().displayLanguage) val recipe = generateRecipeWithAI(this, name, Locale.getDefault().displayLanguage)
runOnUiThread {
progressDialog.setMessage("Saving...")
}
createRecipe(this, recipe)
finish()
} catch (e: ApiRequestException) { } catch (e: ApiRequestException) {
runOnUiThread { runOnUiThread {
Toast.makeText(this, e.message, Toast.LENGTH_LONG).show() Toast.makeText(this, e.message, Toast.LENGTH_LONG).show()
} }
return@thread
} finally { } finally {
runOnUiThread {
progressDialog.hide() progressDialog.hide()
} }
runOnUiThread {
Toast.makeText(this, response.toString(), Toast.LENGTH_LONG).show()
} }
} }
} }

View File

@ -7,19 +7,18 @@ import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.TextView import android.widget.TextView
import tech.mercantec.easyeat.R import tech.mercantec.easyeat.R
import tech.mercantec.easyeat.models.Direction
class InstructionsAdapter (context: Context, instructions: List<Direction>) class InstructionsAdapter (context: Context, instructions: List<String>)
: ArrayAdapter<Direction>(context, 0, instructions) { : ArrayAdapter<String>(context, R.layout.activity_dish_details_instructions_list_item, instructions) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val ingredient = getItem(position) val instruction = getItem(position)
val view = convertView ?: LayoutInflater.from(context) val view = convertView ?: LayoutInflater.from(context)
.inflate(R.layout.activity_dish_details_instructions_list_item, parent, false) .inflate(R.layout.activity_dish_details_instructions_list_item, parent, false)
val nameView = view.findViewById<TextView>(R.id.instructionText) val nameView = view.findViewById<TextView>(R.id.instructionText)
nameView.text = ingredient?.instruktions ?: "" nameView.text = instruction
return view return view
} }

View File

@ -0,0 +1,54 @@
package tech.mercantec.easyeat.ui.dishes
import android.os.Bundle
import android.text.Html
import android.text.Html.FROM_HTML_MODE_LEGACY
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import kotlinx.serialization.json.Json
import tech.mercantec.easyeat.R
import tech.mercantec.easyeat.models.Recipe
class RecipeFragment : Fragment() {
private var recipe: Recipe? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let { args ->
recipe = args.getString("RECIPE")?.let { Json.decodeFromString<Recipe>(it) }
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
val binding = inflater.inflate(R.layout.fragment_recipe, container, false)
recipe?.let { recipe ->
binding.findViewById<TextView>(R.id.title).text = recipe.name
binding.findViewById<TextView>(R.id.ingredients).text =
Html.fromHtml(
"<ul>" +
recipe.ingredients.map { "<li>${it.amount} ${it.unit} ${it.name}</li>" } +
"</ul>",
FROM_HTML_MODE_LEGACY
)
binding.findViewById<TextView>(R.id.directions).text =
Html.fromHtml(
"<ul>" +
recipe.directions.map { "<li>${it}</li>" } +
"</ul>",
FROM_HTML_MODE_LEGACY
)
}
return binding
}
}

View File

@ -9,6 +9,7 @@ import android.widget.ArrayAdapter
import android.widget.EditText import android.widget.EditText
import android.widget.PopupMenu import android.widget.PopupMenu
import android.widget.Spinner import android.widget.Spinner
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import tech.mercantec.easyeat.R import tech.mercantec.easyeat.R
@ -16,6 +17,7 @@ import tech.mercantec.easyeat.databinding.FragmentShoppingListBinding
import tech.mercantec.easyeat.helpers.ApiRequestException import tech.mercantec.easyeat.helpers.ApiRequestException
import tech.mercantec.easyeat.helpers.addShoppingItem import tech.mercantec.easyeat.helpers.addShoppingItem
import tech.mercantec.easyeat.helpers.deleteShoppingItem import tech.mercantec.easyeat.helpers.deleteShoppingItem
import tech.mercantec.easyeat.helpers.editShoppingItem
import tech.mercantec.easyeat.helpers.getShoppingList import tech.mercantec.easyeat.helpers.getShoppingList
import tech.mercantec.easyeat.helpers.toggleShoppingItemChecked import tech.mercantec.easyeat.helpers.toggleShoppingItemChecked
import tech.mercantec.easyeat.models.ShoppingListItem import tech.mercantec.easyeat.models.ShoppingListItem
@ -87,8 +89,35 @@ class ShoppingListFragment : Fragment() {
val popup = PopupMenu(requireActivity(), view) val popup = PopupMenu(requireActivity(), view)
popup.apply { popup.apply {
menuInflater.inflate(R.menu.shopping_item_context_menu, menu) menuInflater.inflate(R.menu.shopping_item_context_menu, menu)
setOnMenuItemClickListener { setOnMenuItemClickListener {
when (it.itemId) { when (it.itemId) {
R.id.edit_shopping_item -> {
showEditDialog(item) { dialog, newItem ->
thread {
try {
editShoppingItem(requireContext(), item, newItem)
} catch (e: ApiRequestException) {
activity?.runOnUiThread {
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
}
return@thread
} finally {
dialog.dismiss()
}
activity?.runOnUiThread {
val adapter = binding.shoppingList.adapter as ShoppingItemAdapter
adapter.remove(item)
adapter.insert(newItem, position)
}
}
}
true
}
R.id.remove_shopping_item -> { R.id.remove_shopping_item -> {
(parent.adapter as ShoppingItemAdapter).remove(item) (parent.adapter as ShoppingItemAdapter).remove(item)
@ -107,6 +136,7 @@ class ShoppingListFragment : Fragment() {
else -> false else -> false
} }
} }
show() show()
} }
@ -116,24 +146,11 @@ class ShoppingListFragment : Fragment() {
// Show new item dialog when clicking add // Show new item dialog when clicking add
binding.addToShoppingList.setOnClickListener { binding.addToShoppingList.setOnClickListener {
val view = requireActivity().layoutInflater.inflate(R.layout.dialog_add_to_shopping_list, null) showEditDialog(null) { dialog, item ->
val dialog = AlertDialog.Builder(activity)
.setView(view)
.setPositiveButton(R.string.add_label) { dialog, id ->
val dialog = dialog as AlertDialog
val amount = view.findViewById<EditText>(R.id.amount).text.toString().toDouble()
val unit = view.findViewById<Spinner>(R.id.unit_selector).selectedItem.toString().ifEmpty { null }
val name = view.findViewById<EditText>(R.id.name).text.toString()
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
dialog.getButton(AlertDialog.BUTTON_NEGATIVE).isEnabled = false
thread { thread {
val item: ShoppingListItem val newItem: ShoppingListItem
try { try {
item = addShoppingItem(requireContext(), name, amount, unit) newItem = addShoppingItem(requireContext(), item)
} catch (e: ApiRequestException) { } catch (e: ApiRequestException) {
activity?.runOnUiThread { activity?.runOnUiThread {
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show() Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
@ -149,19 +166,44 @@ class ShoppingListFragment : Fragment() {
activity?.runOnUiThread { activity?.runOnUiThread {
val adapter = binding.shoppingList.adapter as ShoppingItemAdapter val adapter = binding.shoppingList.adapter as ShoppingItemAdapter
for (i in 0 ..< adapter.count) { for (i in 0..<adapter.count) {
if (adapter.getItem(i)?.id == item.id) { if (adapter.getItem(i)?.id == newItem.id) {
adapter.remove(adapter.getItem(i)) adapter.remove(adapter.getItem(i))
adapter.insert(item, i) adapter.insert(newItem, i)
return@runOnUiThread return@runOnUiThread
} }
} }
adapter.insert(item, adapter.count) adapter.insert(newItem, adapter.count)
} }
} }
} }
}
return binding.root
}
private fun showEditDialog(item: ShoppingListItem?, onSave: (dialog: AlertDialog, item: ShoppingListItem) -> Unit) {
val view = requireActivity().layoutInflater.inflate(R.layout.dialog_edit_shopping_item, null)
val dialog = AlertDialog.Builder(activity)
.setView(view)
.setPositiveButton(R.string.add_label) { dialog, id ->
val dialog = dialog as AlertDialog
val amount = view.findViewById<EditText>(R.id.amount).text.toString().toDouble()
val unit = view.findViewById<Spinner>(R.id.unit_selector).selectedItem.toString()
.ifEmpty { null }
val name = view.findViewById<EditText>(R.id.name).text.toString()
onSave(dialog, ShoppingListItem(item?.id, name, amount, unit, item?.checked ?: false))
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
dialog.getButton(AlertDialog.BUTTON_NEGATIVE).isEnabled = false
}
.setNegativeButton(R.string.cancel_label) { dialog, _ -> .setNegativeButton(R.string.cancel_label) { dialog, _ ->
dialog.cancel() dialog.cancel()
} }
@ -169,12 +211,23 @@ class ShoppingListFragment : Fragment() {
dialog.show() dialog.show()
val adapter = ArrayAdapter.createFromResource(requireContext(), R.array.units, android.R.layout.simple_spinner_item) val adapter = ArrayAdapter.createFromResource(
requireContext(),
R.array.units,
android.R.layout.simple_spinner_item
)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
dialog.findViewById<Spinner>(R.id.unit_selector).adapter = adapter dialog.findViewById<Spinner>(R.id.unit_selector).adapter = adapter
}
return binding.root // Pre-fill dialog inputs with current item if applicable
item?.let { item ->
view.findViewById<EditText>(R.id.amount).setText(item.amount?.toBigDecimal()?.stripTrailingZeros()?.toPlainString(), TextView.BufferType.EDITABLE)
view.findViewById<Spinner>(R.id.unit_selector).setSelection(adapter.getPosition(item.unit))
view.findViewById<EditText>(R.id.name).setText(item.name, TextView.BufferType.EDITABLE)
view.findViewById<TextView>(R.id.title).text = resources.getString(R.string.edit_shopping_item_label)
dialog.getButton(AlertDialog.BUTTON_POSITIVE).text = resources.getString(R.string.save_label)
}
} }
} }

View File

@ -6,5 +6,5 @@
android:tint="?attr/colorControlNormal"> android:tint="?attr/colorControlNormal">
<path <path
android:fillColor="@android:color/white" android:fillColor="@android:color/white"
android:pathData="M280,840Q247,840 223.5,816.5Q200,793 200,760L200,240L160,240L160,160L360,160L360,120L600,120L600,160L800,160L800,240L760,240L760,760Q760,793 736.5,816.5Q713,840 680,840L280,840ZM360,680L440,680L440,320L360,320L360,680ZM520,680L600,680L600,320L520,320L520,680Z"/> android:pathData="M480,600L280,400L680,400L480,600Z"/>
</vector> </vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M280,560L480,360L680,560L280,560Z"/>
</vector>

View File

@ -43,7 +43,9 @@
<EditText <EditText
android:id="@+id/ingredientAmountEditText" android:id="@+id/ingredientAmountEditText"
android:layout_width="173dp" android:layout_width="173dp"
android:layout_height="48dp" /> android:layout_height="48dp"
android:inputType="numberDecimal"
/>
</LinearLayout> </LinearLayout>
<!-- Measurement field --> <!-- Measurement field -->

View File

@ -1,58 +1,125 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<ScrollView <ScrollView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_width="409dp" android:layout_height="match_parent">
android:layout_height="729dp"
tools:layout_editor_absoluteX="1dp"
tools:layout_editor_absoluteY="1dp">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:padding="30sp"
> android:orientation="vertical">
<TextView <TextView
android:id="@+id/dishDetailName" android:id="@+id/dishDetailName"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="30sp" android:textSize="30sp"
android:textAlignment="center"
android:layout_marginBottom="10dp" android:layout_marginBottom="10dp"
android:textStyle="bold" /> android:textStyle="bold"
style="@style/HighContrastText"
/>
<TextView <TextView
android:id="@+id/dishDetailDescription" android:id="@+id/dishDetailDescription"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="20sp" android:textSize="20sp"
android:textAlignment="center" />
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginVertical="20dp"
android:textSize="25sp"
android:textAlignment="center"
android:textStyle="bold"
android:text="Ingredients"/>
<ListView
android:id="@+id/dishDetailIngredients"
android:layout_width="match_parent"
android:layout_height="match_parent"
/> />
<LinearLayout
android:layout_marginTop="20dp"
android:layout_marginHorizontal="10dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textSize="16sp"
android:paddingTop="8dp"
android:labelFor="@id/ingredient_multiplier"
android:text="@string/portions_label"
/>
<EditText
android:id="@+id/ingredient_multiplier"
android:layout_marginStart="20dp"
android:layout_width="50dp"
android:layout_height="wrap_content"
android:importantForAutofill="no"
android:inputType="number"
android:text="1"
/>
<ImageButton
android:id="@+id/decrement_multiplier"
android:layout_marginStart="20dp"
android:layout_width="32dp"
android:layout_height="40dp"
android:scaleType="fitXY"
android:src="@drawable/ic_arrow_drop_down_24px"
android:contentDescription="@string/decrement_multiplier_desc"
android:background="?android:attr/selectableItemBackgroundBorderless"
/>
<ImageButton
android:id="@+id/increment_multiplier"
android:layout_marginStart="5dp"
android:layout_width="32dp"
android:layout_height="40dp"
android:scaleType="fitXY"
android:src="@drawable/ic_arrow_drop_up_24px"
android:contentDescription="@string/increment_multiplier_desc"
android:background="?android:attr/selectableItemBackgroundBorderless"
/>
</LinearLayout>
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginVertical="20dp" android:layout_marginVertical="20dp"
android:textSize="25sp"
android:textAlignment="center"
android:textStyle="bold" android:textStyle="bold"
android:text="Instructions"/> android:textSize="18sp"
android:text="@string/ingredients_label"
style="@style/HighContrastText"
/>
<ListView <LinearLayout
android:id="@+id/dishDetailInstructions" android:id="@+id/ingredients"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" />
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginVertical="20dp"
android:textStyle="bold"
android:textSize="18sp"
android:text="@string/instructions_label"
style="@style/HighContrastText"
/>
<LinearLayout
android:id="@+id/instructions"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
/>
<Button
android:id="@+id/addDishToShoppingList"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="16dp"
android:backgroundTint="@color/cyan"
android:text="@string/add_ingredients_to_shopping_list"
android:tint="@android:color/white"
/> />
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>

View File

@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/dialog_root"
android:layout_width="250dp"
android:layout_height="400dp"
android:orientation="vertical"
android:background="@drawable/rounded_background"
android:padding="0dp"
android:gravity="center">
<TextView
android:layout_width="match_parent"
android:layout_height="40dp"
android:text="@string/create_dish"
android:textAlignment="center"
android:textSize="30sp"/>
<Button
android:id="@+id/createManualBtn"
android:layout_width="200dp"
android:layout_height="0dp"
android:layout_weight="1"
android:text="@string/manually" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#CCCCCC" />
<Button
android:id="@+id/createAIBtn"
android:layout_width="200dp"
android:layout_height="0dp"
android:layout_weight="1"
android:text="@string/search_for_dishes" />
</LinearLayout>

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:padding="30dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/title"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:textSize="24sp"
/>
<TextView
android:layout_marginTop="20dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textStyle="bold"
android:text="@string/ingredients_label"
style="@style/HighContrastText"
/>
<TextView
android:id="@+id/ingredients"
android:layout_marginTop="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<TextView
android:layout_marginTop="20dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textStyle="bold"
android:text="@string/directions_label"
style="@style/HighContrastText"
/>
<TextView
android:id="@+id/directions"
android:layout_marginTop="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</LinearLayout>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/create_manually"
android:title="@string/create_manually_label"
/>
<item
android:id="@+id/create_with_ai"
android:title="@string/create_ai_label"
/>
</menu>

View File

@ -1,8 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/edit_shopping_item"
android:title="@string/edit_label"
/>
<item <item
android:id="@+id/remove_shopping_item" android:id="@+id/remove_shopping_item"
android:icon="@drawable/ic_delete_24px"
android:title="@string/delete_label" android:title="@string/delete_label"
/> />
</menu> </menu>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="HighContrastText" parent="TextAppearance.AppCompat">
<item name="android:textColor">@color/white</item>
</style>
</resources>

View File

@ -8,5 +8,6 @@
<item name="colorSurface">@color/dark_cyan</item> <item name="colorSurface">@color/dark_cyan</item>
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item> <item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<item name="android:colorBackground">@color/black</item> <item name="android:colorBackground">@color/black</item>
<item name="android:windowBackground">@color/black</item>
</style> </style>
</resources> </resources>

View File

@ -32,6 +32,7 @@
<string name="Instructions">Instructions</string> <string name="Instructions">Instructions</string>
<string name="cancel_label">Cancel</string> <string name="cancel_label">Cancel</string>
<string name="add_shopping_item_label">Add shopping item</string> <string name="add_shopping_item_label">Add shopping item</string>
<string name="edit_shopping_item_label">Edit shopping item</string>
<string name="ingredient_amount_label">Amount</string> <string name="ingredient_amount_label">Amount</string>
<string name="ingredient_amount_hint">500</string> <string name="ingredient_amount_hint">500</string>
<string name="ingredient_name_label">Name</string> <string name="ingredient_name_label">Name</string>
@ -40,13 +41,21 @@
<string name="logout_label">Log out</string> <string name="logout_label">Log out</string>
<string name="checked_desc">Checked</string> <string name="checked_desc">Checked</string>
<string name="delete_label">Delete</string> <string name="delete_label">Delete</string>
<string name="edit_label">Edit</string>
<string name="empty_shopping_list">Your shopping list is empty</string> <string name="empty_shopping_list">Your shopping list is empty</string>
<string name="search_for_dishes">Search For Dishes</string>
<string name="manually">Manually</string>
<string name="create_dish">Create Dish</string> <string name="create_dish">Create Dish</string>
<string name="ai_generate_recipe_title">Generate recipe with AI</string> <string name="ai_generate_recipe_title">Generate recipe with AI</string>
<string name="dish_title_label">Name of dish</string> <string name="dish_title_label">Name of dish</string>
<string name="generate_recipe_label">Generate</string> <string name="generate_recipe_label">Generate</string>
<string name="add_ingredients_to_shopping_list">Add ingredients to Shopping List</string>
<string name="ingredients_label">Ingredients</string>
<string name="directions_label">Directions</string>
<string name="create_manually_label">Create manually</string>
<string name="create_ai_label">Generate recipe using AI</string>
<string name="portions_label">Portions</string>
<string name="instructions_label">Instructions</string>
<string name="increment_multiplier_desc">Increment portion count</string>
<string name="decrement_multiplier_desc">Decrement portion size</string>
<string-array name="units"> <string-array name="units">
<item></item> <item></item>
<item>g</item> <item>g</item>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="HighContrastText" parent="TextAppearance.AppCompat">
<item name="android:textColor">@color/black</item>
</style>
</resources>