Compare commits

..

12 Commits
2.2.4 ... 2.2.7

Author SHA1 Message Date
vabene1111
8572f338ad fixed ingredient insert focus error 2025-09-25 21:03:35 +02:00
vabene1111
920ec8e74b fixed missing pg extensions 2025-09-25 20:56:53 +02:00
vabene1111
2328bf2342 fixed mealie edgecases 2025-09-25 20:48:20 +02:00
vabene1111
85620a1431 Merge branch 'master' into develop 2025-09-25 12:33:43 +02:00
vabene1111
0037858885 fixed step editor layout 2025-09-25 12:33:39 +02:00
vabene1111
9df3ff0028 Merge branch 'develop' 2025-09-25 07:39:38 +02:00
vabene1111
0a43272126 fixed property editor page updateing with 0 values 2025-09-25 07:39:30 +02:00
vabene1111
ff96eb194f auto grow comment textarea 2025-09-25 07:37:40 +02:00
vabene1111
6b69c4184b fixed ingredients wihtout unit in steps overview 2025-09-25 07:31:45 +02:00
vabene1111
e90e21181c fixed sizing of ingredient input in recipe editor 2025-09-25 07:28:01 +02:00
vabene1111
5237228673 added patreon link 2025-09-24 19:54:36 +02:00
vabene1111
ecb3172085 increased storage token length 2025-09-24 19:24:33 +02:00
11 changed files with 114 additions and 60 deletions

View File

@@ -30,9 +30,11 @@
![Preview](docs/preview.png)
## Core Features
- 🥗 **Manage your recipes** - Manage your ever growing recipe collection
- 📆 **Plan** - multiple meals for each day
- 🛒 **Shopping lists** - via the meal plan or straight from recipes
- 🪄 **use AI** to recognize images, sort recipe steps, find nutrition facts and more
- 📚 **Cookbooks** - collect recipes into books
- 👪 **Share and collaborate** on recipes with friends and family
@@ -62,12 +64,13 @@ a public page.
Documentation can be found [here](https://docs.tandoor.dev/).
## Support our work
## ❤️ Support our work ❤️
Tandoor is developed by volunteers in their free time just because its fun. That said earning
some money with the project allows us to spend more time on it and thus make improvements we otherwise couldn't.
Because of that there are several ways you can support us
- **GitHub Sponsors** You can sponsor contributors of this project on GitHub: [vabene1111](https://github.com/sponsors/vabene1111)
- **Patron** You can sponsor contributors of this project on Patron: [vabene111](https://www.patreon.com/cw/vabene1111)
- **Host at Hetzner** We have been very happy customers of Hetzner for multiple years for all of our projects. If you want to get into self-hosting or are tired of the expensive big providers, their cloud servers are a great place to get started. When you sign up via our [referral link](https://hetzner.cloud/?ref=ISdlrLmr9kGj) you will get 20€ worth of cloud credits and we get a small kickback too.
- **Let us host for you** We are offering a [hosted version](https://app.tandoor.dev) where all profits support us and the development of tandoor (currently only available in germany).

View File

@@ -138,6 +138,16 @@ class Mealie1(Integration):
if s['recipe_id'] not in first_step_of_recipe_dict:
first_step_of_recipe_dict[s['recipe_id']] = step.pk
# it is possible for a recipe to not have steps but have ingredients, in that case create an empty step to add them to later
for r in recipes_dict.keys():
if r not in first_step_of_recipe_dict:
step = Step.objects.create(instruction='',
order=0,
name='',
space=self.request.space)
steps_relation.append(Recipe.steps.through(recipe_id=recipes_dict[r], step_id=step.pk))
first_step_of_recipe_dict[r] = step.pk
for n in mealie_database['notes']:
if n['recipe_id'] in recipes_dict:
step = Step.objects.create(instruction=n['text'],
@@ -169,7 +179,7 @@ class Mealie1(Integration):
unit_id=units_dict[i['unit_id']] if i['unit_id'] in units_dict else None,
original_text=i['original_text'],
order=i['position'],
amount=i['quantity'],
amount=i['quantity'] if i['quantity'] else 0,
note=i['note'],
space=self.request.space,
)

View File

@@ -16,7 +16,7 @@ import uuid
from django.conf import settings
from django.db import migrations, models
from cookbook.models import SearchFields
from django.contrib.postgres.operations import TrigramExtension, UnaccentExtension
def allSearchFields():
return list(SearchFields.objects.values_list('id', flat=True))
@@ -141,6 +141,8 @@ class Migration(migrations.Migration):
]
operations = [
TrigramExtension(),
UnaccentExtension(),
migrations.RunPython(create_default_groups),
migrations.CreateModel(
name='AiProvider',

View File

@@ -0,0 +1,26 @@
# Generated by Django 5.2.6 on 2025-09-24 17:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0228_space_space_setup_completed'),
]
operations = [
migrations.AlterModelOptions(
name='ailog',
options={'ordering': ('-created_at',)},
),
migrations.AlterModelOptions(
name='aiprovider',
options={'ordering': ('id',)},
),
migrations.AlterField(
model_name='storage',
name='token',
field=models.CharField(blank=True, max_length=4098, null=True),
),
]

View File

@@ -0,0 +1,15 @@
# Generated by Django 5.2.6 on 2025-09-25 18:56
from django.db import migrations
from django.contrib.postgres.operations import TrigramExtension, UnaccentExtension
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0229_alter_ailog_options_alter_aiprovider_options_and_more'),
]
operations = [
TrigramExtension(),
UnaccentExtension(),
]

View File

@@ -592,7 +592,7 @@ class Storage(models.Model, PermissionModelMixin):
)
username = models.CharField(max_length=128, blank=True, null=True)
password = models.CharField(max_length=128, blank=True, null=True)
token = models.CharField(max_length=512, blank=True, null=True)
token = models.CharField(max_length=4098, blank=True, null=True)
url = models.URLField(blank=True, null=True)
path = models.CharField(blank=True, default='', max_length=256)
created_by = models.ForeignKey(User, on_delete=models.PROTECT)

View File

@@ -1,7 +1,7 @@
<template>
<v-card class="mt-1 d-print-none" v-if="useUserPreferenceStore().isAuthenticated" :loading="loading">
<v-card-text>
<v-textarea :label="$t('Comment')" rows="2" v-model="newCookLog.comment"></v-textarea>
<v-textarea :label="$t('Comment')" rows="2" v-model="newCookLog.comment" auto-grow></v-textarea>
<v-row dense>
<v-col cols="12" md="4">
<v-label>{{ $t('Rating') }}</v-label>

View File

@@ -98,10 +98,10 @@ const mergedIngredients = computed(() => {
const groupedIngredients = new Map<string, Ingredient>();
allIngredients.forEach(ingredient => {
if (!ingredient.food || !ingredient.unit) return;
if (!ingredient.food) return;
// Create a unique key for food-unit combination
const key = `${ingredient.food.id}-${ingredient.unit.id}`;
const key = `${ingredient.food.id}-${(ingredient.unit ? ingredient.unit.id : 'no_unit')}`;
if (groupedIngredients.has(key)) {
// If this food-unit combination already exists, sum the amounts

View File

@@ -2,7 +2,7 @@
<!-- TODO label is not showing for some reason, for now in placeholder -->
<v-label class="mt-2" v-if="props.label">{{ props.label }}</v-label>
<v-input :hint="props.hint" persistent-hint :label="props.label" :hide-details="props.hideDetails">
<v-input :hint="props.hint" persistent-hint :label="props.label" :hide-details="props.hideDetails" :disabled="props.disabled">
<template #prepend v-if="$slots.prepend">
<slot name="prepend"></slot>
</template>
@@ -37,6 +37,7 @@
:classes="{
dropdown: 'multiselect-dropdown z-3000',
containerActive: '',
containerDisabled: 'text-disabled'
}"
>
<template #option="{ option }" v-if="props.allowCreate">

View File

@@ -60,61 +60,58 @@
<v-label>{{ $t('Ingredients') }}</v-label>
<div v-if="!mobile">
<vue-draggable v-model="step.ingredients" handle=".drag-handle" :on-sort="sortIngredients" :empty-insert-threshold="25" group="ingredients">
<v-row v-for="(ingredient, index) in step.ingredients" :key="ingredient.id" class="d-flex" dense>
<v-col cols="12" class="pa-0 ma-0 text-center text-disabled" v-if="ingredient.originalText">
<div v-for="(ingredient, index) in step.ingredients" :key="ingredient.id" dense>
<div class="pa-0 ma-0 text-center text-disabled" v-if="ingredient.originalText">
<v-icon icon="$import" size="x-small"></v-icon>
{{ ingredient.originalText }}
</v-col>
<div class="flex-col flex-grow-0 ma-1" style="width: 15vw" v-if="!ingredient.isHeader">
<v-input hide-details>
<template #prepend>
<v-icon icon="$dragHandle" class="drag-handle cursor-grab mt-2" v-if="ingredient.noAmount" density="compact"></v-icon>
</template>
</v-input>
<v-text-field :id="`id_input_amount_${step.id}_${index}`" :label="$t('Amount')" type="number" v-model="ingredient.amount" density="compact"
hide-details v-if="!ingredient.noAmount">
</div>
<div class="d-flex flex-nowrap">
<div class="flex-col flex-grow-0 ma-1" style="min-width: 15%" v-if="!ingredient.isHeader">
<v-text-field :id="`id_input_amount_${props.stepIndex}_${index}`" :label="$t('Amount')" type="number" v-model="ingredient.amount" density="compact"
hide-details :disabled="ingredient.noAmount">
<template #prepend>
<v-icon icon="$dragHandle" class="drag-handle cursor-grab"></v-icon>
</template>
</v-text-field>
</div>
<div class="flex-col flex-grow-0 ma-1" style="min-width: 20vw" v-if="!ingredient.isHeader ">
<model-select model="Unit" v-model="ingredient.unit" density="compact" allow-create hide-details v-if="!ingredient.noAmount"></model-select>
</div>
<div class="flex-col flex-grow-0 ma-1" style="min-width: 20vw" v-if="!ingredient.isHeader">
<model-select model="Food" v-model="ingredient.food" density="compact" allow-create hide-details></model-select>
</div>
<div class="flex-col flex-grow-1 ma-1" @keydown.tab="event => handleIngredientNoteTab(event, index)">
<v-text-field :label="(ingredient.isHeader) ? $t('Headline') : $t('Note')" v-model="ingredient.note" density="compact" hide-details>
<template #prepend v-if="ingredient.isHeader">
<v-icon icon="$dragHandle" class="drag-handle cursor-grab"></v-icon>
</template>
</v-text-field>
</div>
<div class="flex-col flex-grow-0 d-flex ma-1">
<div class="d-flex align-center justify-center">
<v-btn variant="plain" class="" density="compact" tabindex="-1" icon>
<v-icon icon="$menu"></v-icon>
<v-menu activator="parent">
<v-list>
<v-list-item link>
<v-switch v-model="step.ingredients[index].isHeader" :label="$t('Headline')" hide-details></v-switch>
</v-list-item>
<v-list-item link>
<v-switch v-model="step.ingredients[index].noAmount" :label="$t('Disable_Amount')" hide-details></v-switch>
</v-list-item>
<v-list-item @click="editingIngredientIndex = index; dialogIngredientSorter = true" prepend-icon="fa-solid fa-sort">
{{ $t('Move') }}
</v-list-item>
<v-list-item @click="step.ingredients.splice(index, 1)" prepend-icon="$delete">{{ $t('Delete') }}</v-list-item>
</v-list>
</v-menu>
</v-btn>
<template #prepend>
<v-icon icon="$dragHandle" class="drag-handle cursor-grab"></v-icon>
</template>
</v-text-field>
</div>
<div class="flex-col flex-grow-0 ma-1" style="min-width: 15%" v-if="!ingredient.isHeader ">
<model-select model="Unit" v-model="ingredient.unit" density="compact" allow-create hide-details :disabled="ingredient.noAmount"></model-select>
</div>
<div class="flex-col flex-grow-1 ma-1" style="min-width: 15%" v-if="!ingredient.isHeader">
<model-select model="Food" v-model="ingredient.food" density="compact" allow-create hide-details></model-select>
</div>
<div class="flex-col ma-1" style="min-width: 15%" :class="{'flex-grow-1': ingredient.isHeader, 'flex-grow-0': !ingredient.isHeader}"
@keydown.tab="event => handleIngredientNoteTab(event, index)">
<v-text-field :label="(ingredient.isHeader) ? $t('Headline') : $t('Note')" v-model="ingredient.note" density="compact" hide-details>
<template #prepend v-if="ingredient.isHeader">
<v-icon icon="$dragHandle" class="drag-handle cursor-grab"></v-icon>
</template>
</v-text-field>
</div>
<div class="flex-col flex-grow-0 d-flex ma-1">
<div class="d-flex align-center justify-center">
<v-btn variant="plain" class="" density="compact" tabindex="-1" icon>
<v-icon icon="$menu"></v-icon>
<v-menu activator="parent">
<v-list>
<v-list-item link>
<v-switch v-model="step.ingredients[index].isHeader" :label="$t('Headline')" hide-details></v-switch>
</v-list-item>
<v-list-item link>
<v-switch v-model="step.ingredients[index].noAmount" :label="$t('Disable_Amount')" hide-details></v-switch>
</v-list-item>
<v-list-item @click="editingIngredientIndex = index; dialogIngredientSorter = true" prepend-icon="fa-solid fa-sort">
{{ $t('Move') }}
</v-list-item>
<v-list-item @click="step.ingredients.splice(index, 1)" prepend-icon="$delete">{{ $t('Delete') }}</v-list-item>
</v-list>
</v-menu>
</v-btn>
</div>
</div>
</div>
</v-row>
</div>
</vue-draggable>
</div>
@@ -352,7 +349,7 @@ function insertAndFocusIngredient() {
editingIngredientIndex.value = step.value.ingredients.length - 1
dialogIngredientEditor.value = true
} else {
document.getElementById(`id_input_amount_${step.value.id}_${step.value.ingredients.length - 1}`).select()
document.getElementById(`id_input_amount_${props.stepIndex}_${step.value.ingredients.length - 1}`).select()
}
})
}

View File

@@ -95,7 +95,7 @@
</v-number-input>
<v-btn variant="outlined" color="create" block v-if="p.propertyAmount == null" @click="p.propertyAmount = 0">
<v-btn variant="outlined" color="create" block v-if="p.propertyAmount == null" @click="p.propertyAmount = 0; updateFood(ingredient)">
<v-icon icon="$create"></v-icon>
</v-btn>
</td>