Compare commits

...

12 Commits
2.3.4 ... 2.3.5

Author SHA1 Message Date
vabene1111
1cc0806729 remove empty default ingredient when using paste improt 2025-11-19 21:50:47 +01:00
vabene1111
09487a0e94 improved ical export 2025-11-19 21:47:23 +01:00
vabene1111
89e58edcad fixed SLR's without entries 2025-11-19 21:30:20 +01:00
vabene1111
887d7fe9f0 no more unaccent on sqlite 2025-11-19 21:18:23 +01:00
vabene1111
14696e3ce8 fixed times cooked in saved serarch and improved frontend query binding 2025-11-19 21:02:16 +01:00
vabene1111
dd56bb4b35 fixed property detail dialog serving scaling 2025-11-19 20:17:39 +01:00
vabene1111
8ec0ba9541 fixed help view not navigatbale on mobile 2025-11-19 20:06:39 +01:00
vabene1111
c105c9190e ignore defunct websites in url list import mode 2025-11-18 16:16:39 +01:00
vabene1111
9c1700adb9 fixed copying recipes would link data 2025-11-18 16:07:36 +01:00
vabene1111
b43a87a7e3 improved mealie importer 2025-11-18 15:59:15 +01:00
vabene1111
01e78baecf fixed property helper error 2025-11-18 15:48:39 +01:00
vabene1111
0ee241524d some more print mode tweaks 2025-11-18 15:42:19 +01:00
49 changed files with 9626 additions and 9456 deletions

View File

@@ -56,7 +56,7 @@ class FoodPropertyHelper:
if p.property_type == pt and p.property_amount is not None:
has_property_value = True
for c in conversions:
if c.unit == i.food.properties_food_unit:
if c.unit == i.food.properties_food_unit and i.food.properties_food_amount != 0:
found_property = True
computed_properties[pt.id]['total_value'] += (c.amount / i.food.properties_food_amount) * p.property_amount
computed_properties[pt.id]['food_values'] = self.add_or_create(

View File

@@ -324,9 +324,9 @@ class RecipeSearch():
self._queryset = self._queryset.annotate(recent=Coalesce(Max(Case(When(pk__in=num_recent_recipes.values('recipe'), then='viewlog__pk'))), Value(0)))
def _favorite_recipes(self):
if self._sort_includes('favorite') or self._timescooked or self._timescooked_gte or self._timescooked_lte:
if self._sort_includes('favorite') or self._timescooked is not None or self._timescooked_gte is not None or self._timescooked_lte is not None:
less_than = self._timescooked_lte and not self._sort_includes('-favorite')
if less_than or self._timescooked == 0:
if less_than:
default = 1000
else:
default = 0
@@ -338,11 +338,11 @@ class RecipeSearch():
)
self._queryset = self._queryset.annotate(favorite=Coalesce(Subquery(favorite_recipes), default))
if self._timescooked:
if self._timescooked is not None:
self._queryset = self._queryset.filter(favorite=self._timescooked)
elif self._timescooked_lte:
elif self._timescooked_lte is not None:
self._queryset = self._queryset.filter(favorite__lte=int(self._timescooked_lte)).exclude(favorite=0)
elif self._timescooked_gte:
elif self._timescooked_gte is not None:
self._queryset = self._queryset.filter(favorite__gte=int(self._timescooked_gte))
def keyword_filters(self, **kwargs):

View File

@@ -75,7 +75,8 @@ class RecipeShoppingEditor():
@staticmethod
def get_shopping_list_recipe(id, user, space):
return ShoppingListRecipe.objects.filter(id=id).filter(entries__space=space).filter(
# TODO this sucks since it wont find SLR's that no longer have any entries
return ShoppingListRecipe.objects.filter(id=id, space=space).filter(
Q(entries__created_by=user)
| Q(entries__created_by__in=list(user.get_shopping_share()))
).prefetch_related('entries').first()
@@ -136,7 +137,8 @@ class RecipeShoppingEditor():
self.servings = servings
self._delete_ingredients(ingredients=ingredients)
if self.servings != self._shopping_list_recipe.servings:
# need to check if there is a SLR because its possible it cant be found if all entries are deleted
if self._shopping_list_recipe and self.servings != self._shopping_list_recipe.servings:
self.edit_servings()
self._add_ingredients(ingredients=ingredients)
return True

View File

@@ -96,13 +96,19 @@ class Mealie1(Integration):
self.import_log.msg += f"Ignoring {r['name']} because a recipe with this name already exists.\n"
self.import_log.save()
else:
servings = 1
try:
servings = r['recipe_servings'] if r['recipe_servings'] and r['recipe_servings'] != 0 else 1
except KeyError:
pass
recipe = Recipe.objects.create(
waiting_time=parse_time(r['perform_time']),
working_time=parse_time(r['prep_time']),
description=r['description'][:512],
name=r['name'],
source_url=r['org_url'],
servings=r['recipe_servings'] if r['recipe_servings'] and r['recipe_servings'] != 0 else 1,
servings=servings,
servings_text=r['recipe_yield'].strip() if r['recipe_yield'] else "",
internal=True,
created_at=r['created_at'],
@@ -131,7 +137,7 @@ class Mealie1(Integration):
step_id_dict = {}
for s in mealie_database['recipe_instructions']:
if s['recipe_id'] in recipes_dict:
step = Step.objects.create(instruction=(s['text'] if s['text'] else "") + (f" \n {s['summary']}" if s['summary'] else ""),
step = Step.objects.create(instruction=(s['text'] if s['text'] else "") + (f" \n {s['summary']}" if 'summary' in s and s['summary'] else ""),
order=s['position'],
name=s['title'],
space=self.request.space)
@@ -243,7 +249,7 @@ class Mealie1(Integration):
for r in mealie_database['recipe_nutrition']:
if r['recipe_id'] in recipes_dict:
for key in property_types_dict:
if r[key]:
if key in r and r[key]:
properties_relation.append(
Property(property_type_id=property_types_dict[key].pk,
property_amount=Decimal(str(r[key])) / (

View File

@@ -307,7 +307,8 @@ class FuzzyFilterMixin(viewsets.ModelViewSet, ExtendedRecipeMixin):
filter = Q(name__icontains=query)
if self.request.user.is_authenticated:
if any([self.model.__name__.lower() in x for x in
self.request.user.searchpreference.unaccent.values_list('field', flat=True)]):
self.request.user.searchpreference.unaccent.values_list('field', flat=True)]) and (
settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql'):
filter |= Q(name__unaccent__icontains=query)
self.queryset = (
@@ -3056,11 +3057,20 @@ def meal_plans_to_ical(queryset, filename):
for p in queryset:
event = Event()
event['uid'] = p.id
event.add('dtstart', p.from_date)
start_date_time = p.from_date
end_date_time = p.from_date
if p.to_date:
event.add('dtend', p.to_date)
else:
event.add('dtend', p.from_date)
end_date_time = p.to_date
if p.meal_type.time:
start_date_time = datetime.datetime.combine(p.from_date, p.meal_type.time)
end_date_time = datetime.datetime.combine(p.to_date, p.meal_type.time) + datetime.timedelta(minutes=60)
event.add('dtstart', start_date_time)
event.add('dtend', end_date_time)
event['summary'] = f'{p.meal_type.name}: {p.get_label()}'
event['description'] = p.note
cal.add_component(event)

View File

@@ -675,4 +675,4 @@ DISABLE_EXTERNAL_CONNECTORS = extract_bool('DISABLE_EXTERNAL_CONNECTORS', False)
EXTERNAL_CONNECTORS_QUEUE_SIZE = int(os.getenv('EXTERNAL_CONNECTORS_QUEUE_SIZE', 100))
mimetypes.add_type("text/javascript", ".js", True)
mimetypes.add_type("text/javascript", ".mjs", True)
mimetypes.add_type("text/javascript", ".mjs", True)

View File

@@ -10,15 +10,15 @@
<!-- <v-text-field density="compact" variant="outlined" class="pt-2 pb-2" :label="$t('Search')" hide-details clearable></v-text-field>-->
<!-- </v-list-item>-->
<!-- <v-divider></v-divider>-->
<v-list-item link title="Start" @click="window = 'start'" prepend-icon="fa-solid fa-house"></v-list-item>
<v-list-item link title="Space" @click="window = 'space'" prepend-icon="fa-solid fa-database"></v-list-item>
<v-list-item link :title="$t('Start')" @click="window = 'start'" prepend-icon="fa-solid fa-house"></v-list-item>
<v-list-item link :title="$t('Space')" @click="window = 'space'" prepend-icon="fa-solid fa-database"></v-list-item>
<v-list-item link :title="$t('Recipes')" @click="window = 'recipes'" prepend-icon="$recipes"></v-list-item>
<v-list-item link :title="$t('Import')" @click="window = 'import'" prepend-icon="$import"></v-list-item>
<v-list-item link :title="$t('AI')" @click="window = 'ai'" prepend-icon="$ai"></v-list-item>
<v-list-item link :title="$t('Unit')" @click="window = 'unit'" prepend-icon="fa-solid fa-scale-balanced"></v-list-item>
<v-list-item link :title="$t('Food')" @click="window = 'food'" prepend-icon="fa-solid fa-carrot"></v-list-item>
<v-list-item link :title="$t('Keyword')" @click="window = 'keyword'" prepend-icon="fa-solid fa-tags"></v-list-item>
<v-list-item link title="Recipe Structure" @click="window = 'recipe_structure'" prepend-icon="fa-solid fa-diagram-project"></v-list-item>
<v-list-item link :title="$t('Recipe Structure')" @click="window = 'recipe_structure'" prepend-icon="fa-solid fa-diagram-project"></v-list-item>
<v-list-item link :title="$t('Properties')" @click="window = 'properties'" prepend-icon="fa-solid fa-database"></v-list-item>
<v-list-item link :title="$t('Search')" @click="window = 'recipe_search'" prepend-icon="$search"></v-list-item>
<v-list-item link :title="$t('SavedSearch')" @click="window = 'search_filter'" prepend-icon="fa-solid fa-sd-card"></v-list-item>
@@ -31,6 +31,8 @@
<v-main>
<v-container>
<v-select v-model="window" :items="mobileMenuItems" class="d-block d-lg-none"> </v-select>
<v-window v-model="window">
<v-window-item value="start">
<h2>Welcome to Tandoor 2</h2>
@@ -46,7 +48,8 @@
<v-btn class="mt-2 ms-2" color="info" href="https://github.com/TandoorRecipes/recipes" target="_blank" prepend-icon="fa-solid fa-code-branch">GitHub
</v-btn>
<v-alert class="mt-3" border="start" variant="tonal" color="success" v-if="(!useUserPreferenceStore().serverSettings.hosted && !useUserPreferenceStore().activeSpace.demo)">
<v-alert class="mt-3" border="start" variant="tonal" color="success"
v-if="(!useUserPreferenceStore().serverSettings.hosted && !useUserPreferenceStore().activeSpace.demo)">
<v-alert-title>Did you know?</v-alert-title>
Tandoor is Open Source and available to anyone for free to host on their own server. Thousands of hours have been spend
making Tandoor what it is today. You can help make Tandoor even better by contributing or helping financing the effort.
@@ -60,10 +63,12 @@
</v-window-item>
<v-window-item value="space">
<p class="mt-3">All your data is stored in a Space where you can invite other people to collaborate on your recipe database. Typcially the members of a space
<p class="mt-3">All your data is stored in a Space where you can invite other people to collaborate on your recipe database. Typcially the members of a
space
belong to one family/household/organization.</p>
<p class="mt-3">While everyone can access all recipes by default, Books, Shopping Lists and Mealplans are not shared by default. You can share them with other
<p class="mt-3">While everyone can access all recipes by default, Books, Shopping Lists and Mealplans are not shared by default. You can share them with
other
members of your space
using the settings.
</p>
@@ -77,19 +82,24 @@
</v-window-item>
<v-window-item value="recipes">
<p class="mt-3">Recipes are the foundation of your Tandoor space. A Recipe has one or more steps that contain ingredients, instructions and other information.
<p class="mt-3">Recipes are the foundation of your Tandoor space. A Recipe has one or more steps that contain ingredients, instructions and other
information.
Ingredients in turn consist of an amount, a unit and a food, allowing recipes to be scaled, nutrition's to be calculated and shopping to be organized.
</p>
<p class="mt-3">Besides manually creating them you can also import them from various different places.
</p>
<p class="mt-3">Recipes, by default, are visible to all members of your space. Setting them to private means only you can see it. After setting it to private you
<p class="mt-3">Recipes, by default, are visible to all members of your space. Setting them to private means only you can see it. After setting it to
private you
can manually specify the people who should be able to view the recipe.
You can also create a share link for the recipe to share it with everyone that has access to the link.
</p>
<p class="mt-3"></p>
<v-btn color="primary" variant="tonal" prepend-icon="$create" class="me-2" :to="{name: 'ModelEditPage', params: {model: 'Recipe'}}">{{ $t('Create') }}</v-btn>
<v-btn color="primary" variant="tonal" prepend-icon="$create" class="me-2" :to="{name: 'ModelEditPage', params: {model: 'Recipe'}}">{{
$t('Create')
}}
</v-btn>
<v-btn color="primary" variant="tonal" prepend-icon="$search" class="me-2" :to="{name: 'SearchPage'}">{{ $t('Search') }}</v-btn>
</v-window-item>
@@ -119,7 +129,8 @@
</p>
<p class="mt-3" v-if="useUserPreferenceStore().serverSettings.hosted">
To prevent accidental AI cost you can review your AI usage using the AI Log. The Server Administrator can also set AI usage limits for your space (either monthly or using a balance).
To prevent accidental AI cost you can review your AI usage using the AI Log. The Server Administrator can also set AI usage limits for your space
(either monthly or using a balance).
</p>
<p class="mt-3" v-if="!useUserPreferenceStore().serverSettings.hosted">
Depending on your subscription you will have different AI Credits available for your space every month. Additionally you might have a Credit balance
@@ -153,7 +164,8 @@
<v-btn color="primary" variant="tonal" prepend-icon="fa-solid fa-scale-balanced" class="me-2" :to="{name: 'ModelListPage', params: {model: 'Unit'}}">
{{ $t('Unit') }}
</v-btn>
<v-btn color="primary" variant="tonal" prepend-icon="fa-solid fa-exchange-alt" class="me-2" :to="{name: 'ModelListPage', params: {model: 'UnitConversion'}}">
<v-btn color="primary" variant="tonal" prepend-icon="fa-solid fa-exchange-alt" class="me-2"
:to="{name: 'ModelListPage', params: {model: 'UnitConversion'}}">
{{ $t('Conversion') }}
</v-btn>
@@ -223,7 +235,8 @@
calculate the property amount if a Food is given in a different unit (e.g. 1kg or 1 cup).
</p>
<v-btn color="primary" variant="tonal" prepend-icon="fa-solid fa-database" class="me-2 mt-2 mb-2" :to="{name: 'ModelListPage', params: {model: 'PropertyType'}}">
<v-btn color="primary" variant="tonal" prepend-icon="fa-solid fa-database" class="me-2 mt-2 mb-2"
:to="{name: 'ModelListPage', params: {model: 'PropertyType'}}">
{{ $t('Property') }}
</v-btn>
<h3>Editor</h3>
@@ -294,7 +307,8 @@
</p>
<p class="mt-3">
You can assign Supermarket Categories to your Foods, either trough the Food Editor or directly by clicking on a Shopping List Entry, to automatically sort the list
You can assign Supermarket Categories to your Foods, either trough the Food Editor or directly by clicking on a Shopping List Entry, to automatically
sort the list
according to the Category Order defined in the Supermarket.
</p>
@@ -333,7 +347,8 @@
<p class="mt-3">
When selecting a Recipe in a Meal Plan you can automatically add its ingredients to the shopping list. You can also manually add more entries trough the
shopping tab in the Meal Plan editor. When deleting a Meal Plan all Shopping List Entries associated with that Meal Plan are deleted as well. When changing the
shopping tab in the Meal Plan editor. When deleting a Meal Plan all Shopping List Entries associated with that Meal Plan are deleted as well. When
changing the
number of servings in a Meal Plan the Servings of the connected Recipe in the Shopping list are automatically changed as well.
</p>
@@ -368,10 +383,30 @@
import {ref} from "vue";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
import {useI18n} from "vue-i18n";
const {t} = useI18n()
const drawer = defineModel()
const window = ref('start')
const mobileMenuItems = ref([
{title: t('Start'), props: {prependIcon: 'fa-solid fa-house'}, value: 'start'},
{title: t('Space'), props: {prependIcon: 'fa-solid fa-database'}, value: 'space'},
{title: t('Recipes'), props: {prependIcon: '$recipes'}, value: 'recipes'},
{title: t('Import'), props: {prependIcon: '$import'}, value: 'import'},
{title: t('AI'), props: {prependIcon: '$ai'}, value: 'ai'},
{title: t('Unit'), props: {prependIcon: 'fa-solid fa-scale-balanced'}, value: 'unit'},
{title: t('Food'), props: {prependIcon: 'fa-solid fa-carrot'}, value: 'food'},
{title: t('Keyword'), props: {prependIcon: 'fa-solid fa-tags'}, value: 'keyword'},
{title: t('RecipeStructure'), props: {prependIcon: 'fa-solid fa-diagram-project'}, value: 'recipe_structure'},
{title: t('Properties'), props: {prependIcon: 'fa-solid fa-database'}, value: 'properties'},
{title: t('Search'), props: {prependIcon: '$search'}, value: 'recipe_search'},
{title: t('SavedSearch'), props: {prependIcon: 'fa-solid fa-sd-card'}, value: 'search_filter'},
{title: t('Books'), props: {prependIcon: '$books'}, value: 'books'},
{title: t('Shopping'), props: {prependIcon: '$shopping'}, value: 'shopping'},
{title: t('Meal_Plan'), props: {prependIcon: '$mealplan'}, value: 'meal_plan'}
])
</script>

View File

@@ -37,15 +37,15 @@
<v-dialog max-width="900px" v-model="dialog">
<v-card v-if="dialogProperty" :loading="loading">
<v-closable-card-title :title="`${dialogProperty.propertyAmountTotal} ${dialogProperty.unit} ${dialogProperty.name}`" :sub-title="$t('total')" icon="$properties"
<v-closable-card-title :title="`${dialogProperty.propertyAmountTotal} ${(dialogProperty.unit != null) ? dialogProperty.unit : ''} ${dialogProperty.name}`" :sub-title="$t('total')" icon="$properties"
v-model="dialog"></v-closable-card-title>
<v-card-text>
<v-list>
<v-list-item border v-for="fv in dialogProperty.foodValues" :key="`${dialogProperty.id}_${fv.id}`">
<template #prepend>
<v-progress-circular size="55" width="5" :model-value="(fv.value/dialogProperty.propertyAmountTotal)*100"
:color="colorScale((fv.value/dialogProperty.propertyAmountTotal)*100)" v-if="fv.value != null && dialogProperty.propertyAmountTotal > 0">
{{ Math.round((fv.value / dialogProperty.propertyAmountTotal) * 100) }}%
<v-progress-circular size="55" width="5" :model-value="(fv.value* props.ingredientFactor/dialogProperty.propertyAmountTotal)*100"
:color="colorScale((fv.value* props.ingredientFactor/dialogProperty.propertyAmountTotal)*100)" v-if="fv.value != null && dialogProperty.propertyAmountTotal > 0">
{{ Math.round((fv.value* props.ingredientFactor / dialogProperty.propertyAmountTotal) * 100) }}%
</v-progress-circular>
<v-progress-circular size="55" width="5" v-if="fv.value == null">?</v-progress-circular>
</template>
@@ -59,7 +59,7 @@
<model-edit-dialog model="UnitConversion" @create="refreshRecipe()"
:item-defaults="{baseAmount: 1, baseUnit: fv.missing_conversion.base_unit, convertedUnit: fv.missing_conversion.converted_unit, food: fv.food}"></model-edit-dialog>
</v-chip>
<v-chip v-else-if="fv.value != undefined">{{ $n(fv.value) }} {{ dialogProperty.unit }}</v-chip>
<v-chip v-else-if="fv.value != undefined">{{ $n(fv.value * props.ingredientFactor) }} {{ dialogProperty.unit }}</v-chip>
<v-chip color="warning" prepend-icon="$edit" class="cursor-pointer" :to="{name: 'ModelEditPage', params: {model: 'Recipe', id: recipe.id}}" v-else-if="fv.missing_unit">
{{ $t('NoUnit') }}
</v-chip>
@@ -101,7 +101,10 @@ type PropertyWrapper = {
}
const props = defineProps({
servings: {type: Number, required: true,},
ingredientFactor: {
type: Number,
required: true,
},
})
const recipe = defineModel<Recipe>({required: true})
@@ -143,7 +146,7 @@ const propertyList = computed(() => {
description: rp.propertyType.description,
foodValues: [],
propertyAmountPerServing: rp.propertyAmount,
propertyAmountTotal: rp.propertyAmount * recipe.value.servings * (props.servings / recipe.value.servings),
propertyAmountTotal: rp.propertyAmount * recipe.value.servings * props.ingredientFactor,
missingValue: false,
unit: rp.propertyType.unit,
type: rp.propertyType,
@@ -161,7 +164,7 @@ const propertyList = computed(() => {
icon: fp.icon,
foodValues: fp.food_values,
propertyAmountPerServing: fp.total_value / recipe.value.servings,
propertyAmountTotal: fp.total_value * (props.servings / recipe.value.servings),
propertyAmountTotal: fp.total_value * props.ingredientFactor,
missingValue: fp.missing_value,
unit: fp.unit,
type: fp,

View File

@@ -10,7 +10,7 @@
<template v-if="recipe.name != undefined">
<template class="d-block d-lg-none">
<template class="d-block d-lg-none d-print-none">
<!-- mobile layout -->
<v-card class="rounded-0">
@@ -61,7 +61,7 @@
</v-card>
</template>
<!-- Desktop horizontal layout -->
<template class="d-none d-lg-block">
<template class="d-none d-lg-block d-print-block">
<v-row dense>
<v-col cols="8">
<recipe-image
@@ -118,7 +118,7 @@
</v-row>
</template>
<template v-if="recipe.filePath">
<template v-if="recipe.filePath && !useUserPreferenceStore().isPrintMode">
<external-recipe-viewer class="mt-2" :recipe="recipe"></external-recipe-viewer>
<v-card :title="$t('AI')" prepend-icon="$ai" :loading="fileApiLoading || loading" :disabled="fileApiLoading || loading || !useUserPreferenceStore().activeSpace.aiEnabled"
@@ -144,7 +144,7 @@
<step-view v-model="recipe.steps[index]" :step-number="index+1" :ingredientFactor="ingredientFactor"></step-view>
</v-card>
<property-view v-model="recipe" :servings="servings"></property-view>
<property-view v-model="recipe" :ingredientFactor="ingredientFactor"></property-view>
<v-card class="mt-2">
<v-card-text>

View File

@@ -22,10 +22,10 @@
<timer :seconds="step.time != undefined ? step.time*60 : 0" @stop="timerRunning = false" v-if="timerRunning"></timer>
<v-card-text v-if="step.ingredients.length > 0 || step.instruction != ''">
<v-row>
<v-col cols="12" md="6" v-if="step.ingredients.length > 0 && (step.showIngredientsTable || step.show_ingredients_table)">
<v-col :cols="(useUserPreferenceStore().isPrintMode) ? 6 : 12" md="6" v-if="step.ingredients.length > 0 && (step.showIngredientsTable || step.show_ingredients_table)">
<ingredients-table v-model="step.ingredients" :ingredient-factor="ingredientFactor"></ingredients-table>
</v-col>
<v-col cols="12" md="6" class="markdown-body">
<v-col :cols="(useUserPreferenceStore().isPrintMode) ? 6 : 12" md="6" class="markdown-body">
<instructions :instructions_html="step.instructionsMarkdown" :ingredient_factor="ingredientFactor"
v-if="step.instructionsMarkdown != undefined"></instructions>
<!-- sub recipes dont have a correct schema, thus they use different variable naming -->
@@ -62,6 +62,7 @@ import {Step} from "@/openapi";
import Instructions from "@/components/display/Instructions.vue";
import Timer from "@/components/display/Timer.vue";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
const step = defineModel<Step>({required: true})

View File

@@ -47,8 +47,10 @@ import AddToShoppingDialog from "@/components/dialogs/AddToShoppingDialog.vue";
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore.ts";
import {useRouter} from "vue-router";
import {useFileApi} from "@/composables/useFileApi.ts";
import {useI18n} from "vue-i18n";
const router = useRouter()
const {t} = useI18n()
const {updateRecipeImage} = useFileApi()
const props = defineProps({
@@ -70,7 +72,27 @@ function duplicateRecipe() {
let api = new ApiApi()
duplicateLoading.value = true
api.apiRecipeRetrieve({id: props.recipe.id!}).then(originalRecipe => {
api.apiRecipeCreate({recipe: originalRecipe}).then(newRecipe => {
let recipe = {...originalRecipe, ...{id: undefined, name: originalRecipe.name + `(${t('Copy')})`}}
recipe.steps = recipe.steps.map((step) => {
return {
...step,
...{
id: undefined,
ingredients: step.ingredients.map((ingredient) => {
return {...ingredient, ...{id: undefined}}
}),
},
}
})
if (recipe.properties != null) {
recipe.properties = recipe.properties.map((p) => {
return {...p, ...{id: undefined}}
})
}
api.apiRecipeCreate({recipe: recipe}).then(newRecipe => {
if (originalRecipe.image) {
updateRecipeImage(newRecipe.id!, null, originalRecipe.image).then(r => {

View File

@@ -283,8 +283,9 @@ function parseAndInsertIngredients() {
}
})
Promise.allSettled(promises).then(r => {
step.value.ingredients = step.value.ingredients.filter(i => i.food != null || i.note != null || i.amount != 0)
r.forEach(i => {
console.log(i)
step.value.ingredients.push({
originalText: i.value.originalText,
amount: i.value.amount,

View File

@@ -267,6 +267,7 @@
"Ratings": "",
"Recently_Viewed": "",
"Recipe": "",
"RecipeStructure": "",
"Recipe_Book": "",
"Recipe_Image": "",
"Recipes": "",
@@ -314,6 +315,7 @@
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Start": "",
"Starting_Day": "",
"StartsWith": "",
"StartsWithHelp": "",

View File

@@ -260,6 +260,7 @@
"Ratings": "Рейтинги",
"Recently_Viewed": "Наскоро разгледани",
"Recipe": "Рецепта",
"RecipeStructure": "",
"Recipe_Book": "Книга с рецепти",
"Recipe_Image": "Изображение на рецептата",
"Recipes": "Рецепти",
@@ -307,6 +308,7 @@
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Start": "",
"Starting_Day": "Начален ден от седмицата",
"StartsWith": "",
"StartsWithHelp": "",

View File

@@ -337,6 +337,7 @@
"Ratings": "Avaluació",
"Recently_Viewed": "Vistos recentment",
"Recipe": "Recepta",
"RecipeStructure": "",
"Recipe_Book": "Llibre de receptes",
"Recipe_Image": "Imatge de la recepta",
"Recipes": "Receptes",
@@ -394,6 +395,7 @@
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "Un administrador de l'espai podria canviar algunes configuracions estètiques i tindrien prioritat sobre la configuració dels usuaris per a aquest espai.",
"Split_All_Steps": "Dividir totes les files en passos separats.",
"Start": "",
"StartDate": "Data d'inici",
"Starting_Day": "Dia d'inici de la setmana",
"StartsWith": "",

View File

@@ -334,6 +334,7 @@
"Ratings": "Hodnocení",
"Recently_Viewed": "Naposledy prohlížené",
"Recipe": "Recept",
"RecipeStructure": "",
"Recipe_Book": "Kuchařka",
"Recipe_Image": "Obrázek k receptu",
"Recipes": "Recepty",
@@ -389,6 +390,7 @@
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "Některá kosmetická nastavení mohou měnit správci prostoru a budou mít přednost před nastavením klienta pro daný prostor.",
"Split_All_Steps": "Rozdělit každý řádek do samostatného kroku.",
"Start": "",
"StartDate": "Počáteční datum",
"Starting_Day": "První den v týdnu",
"StartsWith": "",

View File

@@ -337,6 +337,7 @@
"Ratings": "Bedømmelser",
"Recently_Viewed": "Vist for nylig",
"Recipe": "Opskrift",
"RecipeStructure": "",
"Recipe_Book": "Opskriftsbog",
"Recipe_Image": "Opskriftsbillede",
"Recipes": "Opskrifter",
@@ -394,6 +395,7 @@
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "Visse kosmetiske indstillinger kan ændres af område-administratorer og vil overskrive klient-indstillinger for pågældende område.",
"Split_All_Steps": "Opdel rækker i separate trin.",
"Start": "",
"StartDate": "Startdato",
"Starting_Day": "Første dag på ugen",
"StartsWith": "",

File diff suppressed because it is too large Load Diff

View File

@@ -337,6 +337,7 @@
"Ratings": "Βαθμολογίες",
"Recently_Viewed": "Προβλήθηκαν πρόσφατα",
"Recipe": "Συνταγή",
"RecipeStructure": "",
"Recipe_Book": "Βιβλίο συνταγών",
"Recipe_Image": "Εικόνα συνταγής",
"Recipes": "Συνταγές",
@@ -394,6 +395,7 @@
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "Ορισμένες ρυθμίσεις εμφάνισης μπορούν να αλλάξουν από τους διαχειριστές του χώρου και θα παρακάμψουν τις ρυθμίσεις πελάτη για αυτόν τον χώρο.",
"Split_All_Steps": "Διαχωρισμός όλων των γραμμών σε χωριστά βήματα.",
"Start": "",
"StartDate": "Ημερομηνία Έναρξης",
"Starting_Day": "Πρώτη μέρα της εβδομάδας",
"StartsWith": "",

View File

@@ -452,6 +452,7 @@
"RecipeBookHelp": "Recipebooks contain recipe book entries or can be automatically populated by using saved search filters. ",
"RecipeHelp": "Recipes are the foundation of Tandoor and consist of general information and steps, made up of ingredients, instructions and more. ",
"RecipeStepsHelp": "Ingredients, Instructions and more can be edited in the tab Steps.",
"RecipeStructure": "Recipe Structure",
"Recipe_Book": "Recipe Book",
"Recipe_Image": "Recipe Image",
"Recipes": "Recipes",
@@ -539,6 +540,7 @@
"Space_Cosmetic_Settings": "Some cosmetic settings can be changed by space administrators and will override client settings for that space.",
"Split": "Split",
"Split_All_Steps": "Split all rows into separate steps.",
"Start": "Start",
"StartDate": "Start Date",
"Starting_Day": "Starting day of the week",
"StartsWith": "Starts with",

File diff suppressed because it is too large Load Diff

View File

@@ -326,6 +326,7 @@
"Ratings": "Luokitukset",
"Recently_Viewed": "Äskettäin katsotut",
"Recipe": "Resepti",
"RecipeStructure": "",
"Recipe_Book": "Keittokirja",
"Recipe_Image": "Reseptin Kuva",
"Recipes": "Reseptit",
@@ -381,6 +382,7 @@
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Split_All_Steps": "Jaa kaikki rivit erillisiin vaiheisiin.",
"Start": "",
"StartDate": "Aloituspäivä",
"Starting_Day": "Viikon aloituspäivä",
"StartsWith": "",

File diff suppressed because it is too large Load Diff

View File

@@ -337,6 +337,7 @@
"Ratings": "דירוג",
"Recently_Viewed": "נצפו לאחרונה",
"Recipe": "מתכון",
"RecipeStructure": "",
"Recipe_Book": "ספר מתכון",
"Recipe_Image": "תמונת מתכון",
"Recipes": "מתכונים",
@@ -394,6 +395,7 @@
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "חלק מהגדרות הקוסמטיות יכולות להיות מעודכנות על ידי מנהל המרחב וידרסו את הגדרות הקליינט עבור מרחב זה.",
"Split_All_Steps": "פצל את כל השורות לצעדים נפרדים.",
"Start": "",
"StartDate": "תאריך התחלה",
"Starting_Day": "יום תחילת השבוע",
"StartsWith": "",

View File

@@ -337,6 +337,7 @@
"Ratings": "Ocjene",
"Recently_Viewed": "Nedavno pogledano",
"Recipe": "Recept",
"RecipeStructure": "",
"Recipe_Book": "Knjiga recepata",
"Recipe_Image": "Slika recepta",
"Recipes": "Recepti",
@@ -394,6 +395,7 @@
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "Neke kozmetičke postavke mogu promijeniti administratori prostora i one će poništiti postavke klijenta za taj prostor.",
"Split_All_Steps": "Podijeli sve retke u zasebne korake.",
"Start": "",
"StartDate": "Početni datum",
"Starting_Day": "Početni dan u tjednu",
"StartsWith": "",

View File

@@ -310,6 +310,7 @@
"Ratings": "Értékelések",
"Recently_Viewed": "Nemrég megtekintett",
"Recipe": "Recept",
"RecipeStructure": "",
"Recipe_Book": "Szakácskönyv",
"Recipe_Image": "Receptkép",
"Recipes": "Receptek",
@@ -360,6 +361,7 @@
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Split_All_Steps": "Ossza fel az összes sort különálló lépésekbe.",
"Start": "",
"StartDate": "Kezdés dátuma",
"Starting_Day": "A hét kezdőnapja",
"StartsWith": "",

View File

@@ -143,6 +143,7 @@
"Rating": "",
"Recently_Viewed": "Վերջերս դիտած",
"Recipe": "Բաղադրատոմս",
"RecipeStructure": "",
"Recipe_Book": "Բաղադրատոմսերի գիրք",
"Recipe_Image": "Բաղադրատոմսի նկար",
"Recipes": "Բաղադրատոմսեր",
@@ -177,6 +178,7 @@
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Start": "",
"StartsWith": "",
"StartsWithHelp": "",
"Step": "",

View File

@@ -286,6 +286,7 @@
"Ratings": "",
"Recently_Viewed": "baru saja dilihat",
"Recipe": "",
"RecipeStructure": "",
"Recipe_Book": "",
"Recipe_Image": "Gambar Resep",
"Recipes": "Resep",
@@ -336,6 +337,7 @@
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Start": "",
"Starting_Day": "",
"StartsWith": "",
"StartsWithHelp": "",

View File

@@ -336,6 +336,7 @@
"Ratings": "",
"Recently_Viewed": "",
"Recipe": "",
"RecipeStructure": "",
"Recipe_Book": "",
"Recipe_Image": "",
"Recipes": "",
@@ -392,6 +393,7 @@
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "",
"Split_All_Steps": "",
"Start": "",
"StartDate": "",
"Starting_Day": "",
"StartsWith": "",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -314,6 +314,7 @@
"Ratings": "",
"Recently_Viewed": "Neseniai Žiūrėta",
"Recipe": "",
"RecipeStructure": "",
"Recipe_Book": "",
"Recipe_Image": "Recepto nuotrauka",
"Recipes": "",
@@ -365,6 +366,7 @@
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Split_All_Steps": "",
"Start": "",
"StartDate": "",
"Starting_Day": "",
"StartsWith": "",

View File

@@ -337,6 +337,7 @@
"Ratings": "",
"Recently_Viewed": "",
"Recipe": "",
"RecipeStructure": "",
"Recipe_Book": "",
"Recipe_Image": "",
"Recipes": "",
@@ -394,6 +395,7 @@
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "",
"Split_All_Steps": "",
"Start": "",
"StartDate": "",
"Starting_Day": "",
"StartsWith": "",

View File

@@ -321,6 +321,7 @@
"Ratings": "",
"Recently_Viewed": "Nylig vist",
"Recipe": "Oppskrift",
"RecipeStructure": "",
"Recipe_Book": "Oppskriftsbok",
"Recipe_Image": "Oppskriftsbilde",
"Recipes": "Oppskrift",
@@ -375,6 +376,7 @@
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Split_All_Steps": "",
"Start": "",
"StartDate": "Startdato",
"Starting_Day": "Dag uken skal state på",
"StartsWith": "",

File diff suppressed because it is too large Load Diff

View File

@@ -363,6 +363,7 @@
"Ratings": "Oceny",
"Recently_Viewed": "Ostatnio oglądane",
"Recipe": "Przepis",
"RecipeStructure": "",
"Recipe_Book": "Książka z przepisami",
"Recipe_Image": "Obrazek dla przepisu",
"Recipes": "Przepisy",
@@ -420,6 +421,7 @@
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "Administratorzy przestrzeni mogą zmienić niektóre ustawienia kosmetyczne, które zastąpią ustawienia klienta dla tej przestrzeni.",
"Split_All_Steps": "Traktuj każdy wiersz jako osobne kroki.",
"Start": "",
"StartDate": "Data początkowa",
"Starting_Day": "Dzień rozpoczęcia tygodnia",
"StartsWith": "",

View File

@@ -236,6 +236,7 @@
"Ratings": "Avaliações",
"Recently_Viewed": "Vistos Recentemente",
"Recipe": "Receita",
"RecipeStructure": "",
"Recipe_Book": "Livro de Receitas",
"Recipe_Image": "Imagem da Receita",
"Recipes": "Receitas",
@@ -271,6 +272,7 @@
"Show_as_header": "Mostrar como cabeçalho",
"Size": "Tamanho",
"Sort_by_new": "Ordenar por mais recente",
"Start": "",
"StartDate": "Data de início",
"Starting_Day": "Dia de início da semana",
"StartsWith": "",

File diff suppressed because it is too large Load Diff

View File

@@ -298,6 +298,7 @@
"Ratings": "Evaluări",
"Recently_Viewed": "Vizualizate recent",
"Recipe": "Rețetă",
"RecipeStructure": "",
"Recipe_Book": "Carte de rețete",
"Recipe_Image": "Imagine a rețetei",
"Recipes": "Rețete",
@@ -349,6 +350,7 @@
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"Split_All_Steps": "Împărțiți toate rândurile în pași separați.",
"Start": "",
"Starting_Day": "Ziua de început a săptămânii",
"StartsWith": "",
"StartsWithHelp": "",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -374,6 +374,7 @@
"Ratings": "Betyg",
"Recently_Viewed": "Nyligen visade",
"Recipe": "Recept",
"RecipeStructure": "",
"Recipe_Book": "Receptbok",
"Recipe_Image": "Receptbild",
"Recipes": "Recept",
@@ -431,6 +432,7 @@
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "Vissa kosmetiska inställningar kan ändras av hushålls-administratörer och skriver över klientinställningar för det hushållet.",
"Split_All_Steps": "Dela upp alla rader i separata steg.",
"Start": "",
"StartDate": "Startdatum",
"Starting_Day": "Startdag i veckan",
"StartsWith": "",

View File

@@ -337,6 +337,7 @@
"Ratings": "Derecelendirmeler",
"Recently_Viewed": "Son Görüntülenen",
"Recipe": "Tarif",
"RecipeStructure": "",
"Recipe_Book": "Yemek Tarifi Kitabı",
"Recipe_Image": "Tarif Resmi",
"Recipes": "Tarifler",
@@ -394,6 +395,7 @@
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "Bazı kozmetik ayarlar alan yöneticileri tarafından değiştirilebilir ve o alanın istemci ayarlarını geçersiz kılar.",
"Split_All_Steps": "Tüm satırları ayrı adımlara bölün.",
"Start": "",
"StartDate": "Başlangıç Tarihi",
"Starting_Day": "Haftanın başlangıç günü",
"StartsWith": "",

File diff suppressed because it is too large Load Diff

View File

@@ -337,6 +337,7 @@
"Ratings": "等级",
"Recently_Viewed": "最近浏览",
"Recipe": "食谱",
"RecipeStructure": "",
"Recipe_Book": "食谱书",
"Recipe_Image": "食谱图像",
"Recipes": "食谱",
@@ -394,6 +395,7 @@
"SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "空间管理员可以更改某些装饰设置,并将覆盖该空间的客户端设置。",
"Split_All_Steps": "将所有行拆分为单独的步骤。",
"Start": "",
"StartDate": "开始日期",
"Starting_Day": "一周中的第一天",
"StartsWith": "",

File diff suppressed because it is too large Load Diff

View File

@@ -606,7 +606,7 @@ function importFromUrlList() {
setTimeout(importFromUrlList, 500)
})
}).catch(err => {
setTimeout(importFromUrlList, 500)
}).finally(() => {
loading.value = false
})

View File

@@ -184,7 +184,7 @@ import RecipeCard from "@/components/display/RecipeCard.vue";
import {useDisplay} from "vuetify";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import {useRouteQuery} from "@vueuse/router";
import {routeQueryDateTransformer, stringToBool, toNumberArray} from "@/utils/utils";
import {numberOrUndefinedTransformer, routeQueryDateTransformer, stringToBool, toNumberArray} from "@/utils/utils";
import RandomIcon from "@/components/display/RandomIcon.vue";
import {VSelect, VTextField, VNumberInput} from "vuetify/components";
import RatingField from "@/components/inputs/RatingField.vue";
@@ -759,27 +759,30 @@ const filters = ref({
label: `${t('Rating')} (${t('exact')})`,
hint: '',
enabled: false,
clearable: true,
default: undefined,
is: RatingField,
modelValue: useRouteQuery('rating', undefined, {transform: Number}),
modelValue: useRouteQuery('rating', undefined, {transform: numberOrUndefinedTransformer}),
},
ratingGte: {
id: 'ratingGte',
label: `${t('Rating')} (>=)`,
hint: '',
enabled: false,
clearable: true,
default: undefined,
is: RatingField,
modelValue: useRouteQuery('ratingGte', undefined, {transform: Number}),
modelValue: useRouteQuery('ratingGte', undefined, {transform: numberOrUndefinedTransformer}),
},
ratingLte: {
id: 'ratingLte',
label: `${t('Rating')} (<=)`,
hint: '',
enabled: false,
clearable: true,
default: undefined,
is: RatingField,
modelValue: useRouteQuery('ratingLte', undefined, {transform: Number}),
modelValue: useRouteQuery('ratingLte', undefined, {transform: numberOrUndefinedTransformer}),
},
timescooked: {
id: 'timescooked',
@@ -787,26 +790,29 @@ const filters = ref({
hint: 'Recipes that were cooked at least X times',
enabled: false,
default: undefined,
clearable: true,
is: VNumberInput,
modelValue: useRouteQuery('timescooked', undefined, {transform: Number}),
modelValue: useRouteQuery('timescooked', undefined, {transform: numberOrUndefinedTransformer}),
},
timescookedGte: {
id: 'timescookedGte',
label: `${t('times_cooked')} (>=)`,
hint: '',
enabled: false,
clearable: true,
default: undefined,
is: VNumberInput,
modelValue: useRouteQuery('timescookedGte', undefined, {transform: Number}),
modelValue: useRouteQuery('timescookedGte', undefined, {transform: numberOrUndefinedTransformer}),
},
timescookedLte: {
id: 'timescookedLte',
label: `${t('times_cooked')} (<=)`,
hint: '',
enabled: false,
clearable: true,
default: undefined,
is: VNumberInput,
modelValue: useRouteQuery('timescookedLte', undefined, {transform: Number}),
modelValue: useRouteQuery('timescookedLte', undefined, {transform: numberOrUndefinedTransformer}),
},
makenow: {
id: 'makenow',

View File

@@ -79,4 +79,20 @@ export function stringToBool(param: string): boolean | undefined {
export const routeQueryDateTransformer = {
get: (value: string | null | Date) => ((value == null) ? null : (new Date(value))),
set: (value: string | null | Date) => ((value == null) ? null : (DateTime.fromJSDate(new Date(value)).toISODate()))
}
}
/**
* routeQueryParam transformer for boolean fields converting string bools to real bools
*/
export const boolOrUndefinedTransformer = {
get: (value: string | null | undefined) => ((value == null) ? undefined : value == 'true'),
set: (value: boolean | null | undefined) => ((value == null) ? undefined : value.toString())
}
/**
* routeQueryParam transformer for number fields converting string numbers to real numbers and allowing undefined for resettable parameters
*/
export const numberOrUndefinedTransformer = {
get: (value: string | null | undefined) => ((value == null) ? undefined : Number(value)),
set: (value: string | null | undefined) => ((value == null) ? undefined : value.toString())
}