Merge remote-tracking branch 'upstream/dev' into localization

This commit is contained in:
Florian Dupret 2021-04-29 08:52:13 +02:00
commit a7f7a742bb
73 changed files with 1330 additions and 1004 deletions

2
.gitignore vendored
View file

@ -9,6 +9,7 @@ mealie/temp/*
mealie/temp/api.html
.temp/
.secret
!*/components/Recipe/Parts
dev/data/backups/*
dev/data/debug/*
@ -159,3 +160,4 @@ scratch.py
dev/data/backups/dev_sample_data*.zip
dev/data/backups/dev_sample_data*.zip
!dev/data/backups/test*.zip
dev/data/recipes/*

View file

@ -16,6 +16,7 @@ const recipeURLs = {
delete: slug => prefix + slug,
recipeImage: slug => `${prefix}${slug}/image`,
updateImage: slug => `${prefix}${slug}/image`,
createAsset: slug => `${prefix}${slug}/asset`,
};
export const recipeAPI = {
@ -78,26 +79,16 @@ export const recipeAPI = {
);
},
updateImagebyURL(slug, url) {
return apiReq.post(
recipeURLs.updateImage(slug),
{ url: url },
function() { return i18n.t('general.image-upload-failed'); },
function() { return i18n.t('recipe.recipe-image-updated'); }
);
async updateImagebyURL(slug, url) {
const response = apiReq.post(recipeURLs.updateImage(slug), { url: url });
return response;
},
async update(data) {
let response = await apiReq.put(
recipeURLs.update(data.slug),
data,
function() { return i18n.t('recipe.recipe-update-failed'); },
function() { return i18n.t('recipe.recipe-updated'); }
);
if(response) {
console.log(data)
let response = await apiReq.put(recipeURLs.update(data.slug), data);
store.dispatch("patchRecipe", response.data);
return response.data.slug; // ! Temporary until I rewrite to refresh page without additional request
}
},
async patch(data) {

View file

@ -0,0 +1,160 @@
<template>
<div v-if="value.length > 0 || edit">
<v-card class="mt-2">
<v-card-title class="py-2">
{{ $t("recipe.assets") }}
</v-card-title>
<v-divider class="mx-2"></v-divider>
<v-list :flat="!edit" v-if="value.length > 0">
<v-list-item v-for="(item, i) in value" :key="i">
<v-list-item-icon class="ma-auto">
<v-icon v-text="item.icon"></v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title
class="pl-2"
v-text="item.name"
></v-list-item-title>
</v-list-item-content>
<v-list-item-action>
<v-btn
v-if="!edit"
color="primary"
icon
:href="`/api/recipes/${slug}/asset?file_name=${item.fileName}`"
target="_blank"
top
>
<v-icon> mdi-download</v-icon>
</v-btn>
<v-btn v-else color="error" icon @click="deleteAsset(i)" top>
<v-icon>mdi-delete</v-icon>
</v-btn>
</v-list-item-action>
</v-list-item>
</v-list>
</v-card>
<div class="d-flex ml-auto mt-2">
<v-spacer></v-spacer>
<base-dialog
@submit="addAsset"
title="New Asset"
:title-icon="newAsset.icon"
>
<template v-slot:open="{ open }">
<v-btn color="secondary" dark @click="open" v-if="edit">
<v-icon>mdi-plus</v-icon>
</v-btn>
</template>
<v-card-text class="pt-2">
<v-text-field
dense
v-model="newAsset.name"
:label="$t('general.name')"
></v-text-field>
<div class="d-flex justify-space-between">
<v-select
dense
:prepend-icon="newAsset.icon"
v-model="newAsset.icon"
:items="iconOptions"
class="mr-2"
>
<template v-slot:item="{ item }">
<v-list-item-avatar>
<v-icon class="mr-auto">
{{ item }}
</v-icon>
</v-list-item-avatar>
{{ item }}
</template>
</v-select>
<TheUploadBtn
@uploaded="setFileObject"
:post="false"
file-name="file"
:text-btn="false"
/>
</div>
{{ fileObject.name }}
</v-card-text>
</base-dialog>
</div>
</div>
</template>
<script>
import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn";
import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
import { api } from "@/api";
export default {
components: {
BaseDialog,
TheUploadBtn,
},
props: {
slug: String,
value: {
type: Array,
},
edit: {
type: Boolean,
default: true,
},
},
data() {
return {
fileObject: {},
newAsset: {
name: "",
icon: "mdi-file",
},
iconOptions: [
"mdi-file",
"mdi-file-pdf-box",
"mdi-file-image",
"mdi-code-json",
"mdi-silverware-fork-knife",
],
menu: [
{
title: "Link 1",
icon: "mdi-file",
action: "Do Something",
},
{
title: "Link 1",
icon: "mdi-file",
action: "Do Something",
},
{
title: "Link 1",
icon: "mdi-file",
action: "Do Something",
},
],
};
},
methods: {
setFileObject(obj) {
this.fileObject = obj;
},
async addAsset() {
const serverAsset = await api.recipes.createAsset(
this.slug,
this.fileObject,
this.newAsset.name,
this.newAsset.icon
);
this.value.push(serverAsset.data);
this.newAsset = { name: "", icon: "mdi-file" };
},
deleteAsset(index) {
this.value.splice(index, 1);
},
},
};
</script>
<style scoped>
</style>

View file

@ -3,7 +3,7 @@
<v-dialog v-model="dialog" width="600">
<template v-slot:activator="{ on, attrs }">
<v-btn
text
color="secondary lighten-2"
dark
v-bind="attrs"

View file

@ -3,13 +3,16 @@
<v-menu offset-y top nudge-top="6" :close-on-content-click="false">
<template v-slot:activator="{ on, attrs }">
<v-btn color="accent" dark v-bind="attrs" v-on="on">
{{$t('general.image')}}
<v-icon left>
mdi-image
</v-icon>
{{ $t("general.image") }}
</v-btn>
</template>
<v-card width="400">
<v-card-title class="headline flex mb-0">
<div>
{{$t('recipe.recipe-image')}}
{{ $t("recipe.recipe-image") }}
</div>
<TheUploadBtn
class="ml-auto"
@ -22,7 +25,12 @@
</v-card-title>
<v-card-text class="mt-n5">
<div>
<v-text-field :label="$t('general.url')" class="pt-5" clearable v-model="url">
<v-text-field
:label="$t('general.url')"
class="pt-5"
clearable
v-model="url"
>
<template v-slot:append-outer>
<v-btn
class="ml-2"
@ -30,7 +38,7 @@
@click="getImageFromURL"
:loading="loading"
>
{{$t('general.get')}}
{{ $t("general.get") }}
</v-btn>
</template>
</v-text-field>

View file

@ -0,0 +1,137 @@
<template>
<div>
<h2 class="mb-4">{{ $t("recipe.ingredients") }}</h2>
<div v-if="edit">
<draggable
:value="value"
@input="updateIndex"
@start="drag = true"
@end="drag = false"
handle=".handle"
>
<transition-group type="transition" :name="!drag ? 'flip-list' : null">
<div
v-for="(ingredient, index) in value"
:key="generateKey('ingredient', index)"
>
<v-row align="center">
<v-textarea
class="mr-2"
:label="$t('recipe.ingredient')"
v-model="value[index]"
mdi-move-resize
auto-grow
solo
dense
rows="1"
>
<template slot="append-outer">
<v-icon class="handle">mdi-arrow-up-down</v-icon>
</template>
<v-icon
class="mr-n1"
slot="prepend"
color="error"
@click="removeByIndex(value, index)"
>
mdi-delete
</v-icon>
</v-textarea>
</v-row>
</div>
</transition-group>
</draggable>
<div class="d-flex row justify-end">
<BulkAdd @bulk-data="addIngredient" class="mr-2" />
<v-btn color="secondary" dark @click="addIngredient" class="mr-4">
<v-icon>mdi-plus</v-icon>
</v-btn>
</div>
</div>
<div v-else>
<v-list-item
dense
v-for="(ingredient, index) in value"
:key="generateKey('ingredient', index)"
@click="toggleChecked(index)"
>
<v-checkbox
hide-details
:value="checked[index]"
class="pt-0 my-auto py-auto"
color="secondary"
>
</v-checkbox>
<v-list-item-content>
<vue-markdown
class="ma-0 pa-0 text-subtitle-1 dense-markdown"
:source="ingredient"
>
</vue-markdown>
</v-list-item-content>
</v-list-item>
</div>
</div>
</template>
<script>
import BulkAdd from "@/components/Recipe/Parts/Helpers/BulkAdd";
import VueMarkdown from "@adapttive/vue-markdown";
import draggable from "vuedraggable";
import utils from "@/utils";
export default {
components: {
BulkAdd,
draggable,
VueMarkdown,
},
props: {
value: {
type: Array,
},
edit: {
type: Boolean,
default: true,
},
},
data() {
return {
drag: false,
checked: [],
};
},
mounted() {
this.checked = this.value.map(() => false);
},
methods: {
addIngredient(ingredients = null) {
if (ingredients.length) {
this.value.push(...ingredients);
} else {
this.value.push("");
}
},
generateKey(item, index) {
return utils.generateUniqueKey(item, index);
},
updateIndex(data) {
this.$emit("input", data);
},
toggleChecked(index) {
this.$set(this.checked, index, !this.checked[index]);
},
removeByIndex(list, index) {
list.splice(index, 1);
},
},
};
</script>
<style >
.dense-markdown p {
margin: auto !important;
}
</style>

View file

@ -0,0 +1,148 @@
<template>
<div>
<h2 class="mb-4">{{ $t("recipe.instructions") }}</h2>
<div>
<div v-for="(step, index) in value" :key="index">
<v-app-bar
v-if="showTitleEditor[index]"
class="primary mx-1 mt-6"
dark
dense
rounded
>
<v-toolbar-title class="headline" v-if="!edit">
<v-app-bar-title v-text="step.title"> </v-app-bar-title>
</v-toolbar-title>
<v-text-field
v-if="edit"
class="headline pa-0 mt-5"
v-model="step.title"
dense
solo
flat
placeholder="Section Title"
background-color="primary"
>
</v-text-field>
</v-app-bar>
<v-hover v-slot="{ hover }">
<v-card
class="ma-1"
:class="[{ 'on-hover': hover }, isDisabled(index)]"
:elevation="hover ? 12 : 2"
:ripple="!edit"
@click="toggleDisabled(index)"
>
<v-card-title>
<v-btn
v-if="edit"
fab
x-small
color="white"
class="mr-2"
elevation="0"
@click="removeByIndex(value, index)"
>
<v-icon size="24" color="error">mdi-delete</v-icon>
</v-btn>
{{ $t("recipe.step-index", { step: index + 1 }) }}
<v-btn
v-if="edit"
text
color="primary"
class="ml-auto"
@click="toggleShowTitle(index)"
>
{{
!showTitleEditor[index] ? "Insert Section" : "Remove Section"
}}
</v-btn>
</v-card-title>
<v-card-text v-if="edit">
<v-textarea
auto-grow
dense
v-model="value[index]['text']"
:key="generateKey('instructions', index)"
rows="4"
>
</v-textarea>
</v-card-text>
<v-card-text v-else>
<vue-markdown :source="step.text"> </vue-markdown>
</v-card-text>
</v-card>
</v-hover>
</div>
</div>
</div>
</template>
<script>
import VueMarkdown from "@adapttive/vue-markdown";
import utils from "@/utils";
export default {
components: {
VueMarkdown,
},
props: {
value: {
type: Array,
},
edit: {
type: Boolean,
default: true,
},
},
data() {
return {
disabledSteps: [],
showTitleEditor: [],
};
},
mounted() {
this.showTitleEditor = this.value.map(x => this.validateTitle(x.title));
},
methods: {
generateKey(item, index) {
return utils.generateUniqueKey(item, index);
},
removeByIndex(list, index) {
list.splice(index, 1);
},
validateTitle(title) {
return !(title === null || title === "");
},
toggleDisabled(stepIndex) {
if (this.edit) return;
if (this.disabledSteps.includes(stepIndex)) {
let index = this.disabledSteps.indexOf(stepIndex);
if (index !== -1) {
this.disabledSteps.splice(index, 1);
}
} else {
this.disabledSteps.push(stepIndex);
}
},
isDisabled(stepIndex) {
if (this.disabledSteps.includes(stepIndex) && !this.edit) {
return "disabled-card";
} else {
return;
}
},
toggleShowTitle(index) {
const newVal = !this.showTitleEditor[index];
if (!newVal) {
this.value[index].title = "";
}
this.$set(this.showTitleEditor, index, newVal);
},
},
};
</script>
<style scoped>
</style>

View file

@ -0,0 +1,87 @@
<template>
<div v-if="value.length > 0 || edit">
<h2 class="my-4">{{ $t("recipe.note") }}</h2>
<v-card
class="mt-1"
v-for="(note, index) in value"
:key="generateKey('note', index)"
>
<div v-if="edit">
<v-card-text>
<v-row align="center">
<v-btn
fab
x-small
color="white"
class="mr-2"
elevation="0"
@click="removeByIndex(value, index)"
>
<v-icon color="error">mdi-delete</v-icon>
</v-btn>
<v-text-field
:label="$t('recipe.title')"
v-model="value[index]['title']"
></v-text-field>
</v-row>
<v-textarea
auto-grow
:placeholder="$t('recipe.note')"
v-model="value[index]['text']"
>
</v-textarea>
</v-card-text>
</div>
<div v-else>
<v-card-title class="py-2">
{{ note.title }}
</v-card-title>
<v-divider class="mx-2"></v-divider>
<v-card-text>
<vue-markdown :source="note.text"> </vue-markdown>
</v-card-text>
</div>
</v-card>
<div class="d-flex justify-end" v-if="edit">
<v-btn class="mt-1" color="secondary" dark @click="addNote">
<v-icon>mdi-plus</v-icon>
</v-btn>
</div>
</div>
</template>
<script>
import VueMarkdown from "@adapttive/vue-markdown";
import utils from "@/utils";
export default {
components: {
VueMarkdown,
},
props: {
value: {
type: Array,
},
edit: {
type: Boolean,
default: true,
},
},
methods: {
generateKey(item, index) {
return utils.generateUniqueKey(item, index);
},
addNote() {
this.value.push({ text: "" });
},
removeByIndex(list, index) {
list.splice(index, 1);
},
},
};
</script>
<style>
</style>

View file

@ -0,0 +1,101 @@
<template>
<div v-if="valueNotNull || edit">
<v-card class="mt-2">
<v-card-title class="py-2">
{{ $t("recipe.nutrition") }}
</v-card-title>
<v-divider class="mx-2"></v-divider>
<v-card-text v-if="edit">
<div v-for="(item, key, index) in value" :key="index">
<v-text-field
dense
:value="value[key]"
:label="labels[key].label"
:suffix="labels[key].suffix"
type="number"
autocomplete="off"
@input="updateValue(key, $event)"
></v-text-field>
</div>
</v-card-text>
<v-list dense v-if="showViewer" class="mt-0 pt-0">
<v-list-item v-for="(item, key, index) in labels" :key="index">
<v-list-item-content>
<v-list-item-title class="pl-4 text-subtitle-1 flex row ">
<div>{{ item.label }}</div>
<div class="ml-auto mr-1">{{ value[key] }}</div>
<div>{{ item.suffix }}</div>
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
</v-card>
</div>
</template>
<script>
export default {
props: {
value: {},
edit: {
type: Boolean,
default: true,
},
},
data() {
return {
labels: {
calories: {
label: this.$t("recipe.calories"),
suffix: this.$t("recipe.calories-suffix"),
},
fatContent: {
label: this.$t("recipe.fat-content"),
suffix: this.$t("recipe.grams"),
},
fiberContent: {
label: this.$t("recipe.fiber-content"),
suffix: this.$t("recipe.grams"),
},
proteinContent: {
label: this.$t("recipe.protein-content"),
suffix: this.$t("recipe.grams"),
},
sodiumContent: {
label: this.$t("recipe.sodium-content"),
suffix: this.$t("recipe.milligrams"),
},
sugarContent: {
label: this.$t("recipe.sugar-content"),
suffix: this.$t("recipe.grams"),
},
carbohydrateContent: {
label: this.$t("recipe.carbohydrate-content"),
suffix: this.$t("recipe.grams"),
},
},
};
},
computed: {
showViewer() {
return !this.edit && this.valueNotNull;
},
valueNotNull() {
for (const property in this.value) {
const valueProperty = this.value[property];
if (valueProperty && valueProperty !== "") return true;
}
return false;
},
},
methods: {
updateValue(key, value) {
this.$emit("input", { ...this.value, [key]: value });
},
},
};
</script>
<style lang="scss" scoped>
</style>

View file

@ -1,81 +0,0 @@
<template>
<div v-if="valueNotNull || edit">
<h2 class="my-4">{{$t('recipe.nutrition')}}</h2>
<div v-if="edit">
<div v-for="(item, key, index) in value" :key="index">
<v-text-field
dense
:value="value[key]"
:label="labels[key].label"
:suffix="labels[key].suffix"
type="number"
autocomplete="off"
@input="updateValue(key, $event)"
></v-text-field>
</div>
</div>
<div v-if="showViewer">
<v-list dense>
<v-list-item-group color="primary">
<v-list-item v-for="(item, key, index) in labels" :key="index">
<v-list-item-content>
<v-list-item-title class="pl-4 text-subtitle-1 flex row ">
<div>{{ item.label }}</div>
<div class="ml-auto mr-1">{{ value[key] }}</div>
<div>{{ item.suffix }}</div>
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list-item-group>
</v-list>
</div>
</div>
</template>
<script>
export default {
props: {
value: {},
edit: {
type: Boolean,
default: true,
},
},
data() {
return {
labels: {
calories: {
label: this.$t('recipe.calories'),
suffix:this.$t('recipe.calories-suffix'),
},
fatContent: { label: this.$t('recipe.fat-content'), suffix: this.$t('recipe.grams') },
fiberContent: { label: this.$t('recipe.fiber-content'), suffix: this.$t('recipe.grams') },
proteinContent: { label: this.$t('recipe.protein-content'), suffix: this.$t('recipe.grams') },
sodiumContent: { label: this.$t('recipe.sodium-content'), suffix: this.$t('recipe.milligrams') },
sugarContent: { label: this.$t('recipe.sugar-content'), suffix: this.$t('recipe.grams') },
},
};
},
computed: {
showViewer() {
return !this.edit && this.valueNotNull;
},
valueNotNull() {
for (const property in this.value) {
const valueProperty = this.value[property];
if (valueProperty && valueProperty !== "") return true;
}
return false;
},
},
methods: {
updateValue(key, value) {
this.$emit("input", { ...this.value, [key]: value });
},
},
};
</script>
<style lang="scss" scoped>
</style>

View file

@ -3,7 +3,7 @@
<v-card-text>
<v-row dense>
<ImageUploadBtn
class="mt-2"
class="my-1"
@upload="uploadImage"
:slug="value.slug"
@refresh="$emit('upload')"
@ -64,50 +64,7 @@
</v-row>
<v-row>
<v-col cols="12" sm="12" md="4" lg="4">
<h2 class="mb-4">{{ $t("recipe.ingredients") }}</h2>
<draggable
v-model="value.recipeIngredient"
@start="drag = true"
@end="drag = false"
>
<transition-group
type="transition"
:name="!drag ? 'flip-list' : null"
>
<div
v-for="(ingredient, index) in value.recipeIngredient"
:key="generateKey('ingredient', index)"
>
<v-row align="center">
<v-textarea
class="mr-2"
:label="$t('recipe.ingredient')"
v-model="value.recipeIngredient[index]"
append-outer-icon="mdi-menu"
mdi-move-resize
auto-grow
solo
dense
rows="1"
>
<v-icon
class="mr-n1"
slot="prepend"
color="error"
@click="removeByIndex(value.recipeIngredient, index)"
>
mdi-delete
</v-icon>
</v-textarea>
</v-row>
</div>
</transition-group>
</draggable>
<v-btn color="secondary" fab dark small @click="addIngredient">
<v-icon>mdi-plus</v-icon>
</v-btn>
<BulkAdd @bulk-data="addIngredient" />
<Ingredients :edit="true" v-model="value.recipeIngredient" />
<h2 class="mt-6">{{ $t("recipe.categories") }}</h2>
<CategoryTagSelector
@ -125,87 +82,23 @@
:tag-selector="true"
:show-label="false"
/>
<h2 class="my-4">{{ $t("recipe.notes") }}</h2>
<v-card
class="mt-1"
v-for="(note, index) in value.notes"
:key="generateKey('note', index)"
>
<v-card-text>
<v-row align="center">
<v-btn
fab
x-small
color="white"
class="mr-2"
elevation="0"
@click="removeByIndex(value.notes, index)"
>
<v-icon color="error">mdi-delete</v-icon>
</v-btn>
<v-text-field
:label="$t('recipe.title')"
v-model="value.notes[index]['title']"
></v-text-field>
</v-row>
<v-textarea
auto-grow
:label="$t('recipe.note')"
v-model="value.notes[index]['text']"
>
</v-textarea>
</v-card-text>
</v-card>
<v-btn class="mt-1" color="secondary" fab dark small @click="addNote">
<v-icon>mdi-plus</v-icon>
</v-btn>
<NutritionEditor v-model="value.nutrition" :edit="true" />
<Nutrition v-model="value.nutrition" :edit="true" />
<Assets v-model="value.assets" :edit="true" :slug="value.slug" />
<ExtrasEditor :extras="value.extras" @save="saveExtras" />
</v-col>
<v-divider class="my-divider" :vertical="true"></v-divider>
<v-col cols="12" sm="12" md="8" lg="8">
<h2 class="mb-4">{{ $t("recipe.instructions") }}</h2>
<div v-for="(step, index) in value.recipeInstructions" :key="index">
<v-hover v-slot="{ hover }">
<v-card
class="ma-1"
:class="[{ 'on-hover': hover }]"
:elevation="hover ? 12 : 2"
>
<v-card-title>
<v-btn
fab
x-small
color="white"
class="mr-2"
elevation="0"
@click="removeByIndex(value.recipeInstructions, index)"
>
<v-icon size="24" color="error">mdi-delete</v-icon>
</v-btn>
{{ $t("recipe.step-index", { step: index + 1 }) }}
</v-card-title>
<v-card-text>
<v-textarea
auto-grow
dense
v-model="value.recipeInstructions[index]['text']"
:key="generateKey('instructions', index)"
rows="4"
>
</v-textarea>
</v-card-text>
</v-card>
</v-hover>
</div>
<v-btn color="secondary" fab dark small @click="addStep">
<Instructions v-model="value.recipeInstructions" :edit="true" />
<div class="d-flex row justify-end mt-2">
<BulkAdd @bulk-data="appendSteps" class="mr-2" />
<v-btn color="secondary" dark @click="addStep" class="mr-4">
<v-icon>mdi-plus</v-icon>
</v-btn>
<BulkAdd @bulk-data="appendSteps" />
</div>
<Notes :edit="true" v-model="value.notes" />
<v-text-field
v-model="value.orgURL"
class="mt-10"
@ -219,22 +112,27 @@
<script>
const UPLOAD_EVENT = "upload";
import draggable from "vuedraggable";
import utils from "@/utils";
import BulkAdd from "./BulkAdd";
import ExtrasEditor from "./ExtrasEditor";
import BulkAdd from "@/components/Recipe/Parts/Helpers/BulkAdd";
import ExtrasEditor from "@/components/Recipe/Parts/Helpers/ExtrasEditor";
import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector";
import NutritionEditor from "./NutritionEditor";
import ImageUploadBtn from "./ImageUploadBtn.vue";
import ImageUploadBtn from "@/components/Recipe/Parts/Helpers/ImageUploadBtn";
import { validators } from "@/mixins/validators";
import Nutrition from "@/components/Recipe/Parts/Nutrition";
import Instructions from "@/components/Recipe/Parts/Instructions";
import Ingredients from "@/components/Recipe/Parts/Ingredients";
import Assets from "@/components/Recipe/Parts/Assets.vue";
import Notes from "@/components/Recipe/Parts/Notes.vue";
export default {
components: {
BulkAdd,
ExtrasEditor,
draggable,
CategoryTagSelector,
NutritionEditor,
Nutrition,
ImageUploadBtn,
Instructions,
Ingredients,
Assets,
Notes,
},
props: {
value: Object,
@ -242,7 +140,6 @@ export default {
mixins: [validators],
data() {
return {
drag: false,
fileObject: null,
};
},
@ -250,30 +147,6 @@ export default {
uploadImage(fileObject) {
this.$emit(UPLOAD_EVENT, fileObject);
},
toggleDisabled(stepIndex) {
if (this.disabledSteps.includes(stepIndex)) {
const index = this.disabledSteps.indexOf(stepIndex);
if (index !== -1) {
this.disabledSteps.splice(index, 1);
}
} else {
this.disabledSteps.push(stepIndex);
}
},
isDisabled(stepIndex) {
return this.disabledSteps.includes(stepIndex) ? "disabled-card" : null;
},
generateKey(item, index) {
return utils.generateUniqueKey(item, index);
},
addIngredient(ingredients = null) {
if (ingredients) {
this.value.recipeIngredient.push(...ingredients);
} else {
this.value.recipeIngredient.push("");
}
},
appendSteps(steps) {
this.value.recipeInstructions.push(
...steps.map(x => ({
@ -284,15 +157,9 @@ export default {
addStep() {
this.value.recipeInstructions.push({ text: "" });
},
addNote() {
this.value.notes.push({ text: "" });
},
saveExtras(extras) {
this.value.extras = extras;
},
removeByIndex(list, index) {
list.splice(index, 1);
},
validateRecipe() {
return this.$refs.form.validate();
},

View file

@ -1,62 +0,0 @@
<template>
<div>
<h2 class="mb-4">{{ $t("recipe.ingredients") }}</h2>
<v-list-item
dense
v-for="(ingredient, index) in ingredients"
:key="generateKey('ingredient', index)"
@click="toggleChecked(index)"
>
<v-checkbox
hide-details
:value="checked[index]"
class="pt-0 my-auto py-auto"
color="secondary"
>
</v-checkbox>
<v-list-item-content>
<vue-markdown
class="ma-0 pa-0 text-subtitle-1 dense-markdown"
:source="ingredient"
>
</vue-markdown>
</v-list-item-content>
</v-list-item>
</div>
</template>
<script>
import VueMarkdown from "@adapttive/vue-markdown";
import utils from "@/utils";
export default {
components: {
VueMarkdown,
},
props: {
ingredients: Array,
},
data() {
return {
checked: [],
};
},
mounted() {
this.checked = this.ingredients.map(() => false);
},
methods: {
generateKey(item, index) {
return utils.generateUniqueKey(item, index);
},
toggleChecked(index) {
this.$set(this.checked, index, !this.checked[index]);
},
},
};
</script>
<style >
.dense-markdown p {
margin: auto !important;
}
</style>

View file

@ -1,36 +0,0 @@
<template>
<div>
<h2 v-if="notes[0]" class="my-4">{{ $t("recipe.notes") }}</h2>
<v-card
class="mt-1"
v-for="(note, index) in notes"
:key="generateKey('note', index)"
>
<v-card-title> {{ note.title }}</v-card-title>
<v-card-text>
<vue-markdown :source="note.text"> </vue-markdown>
</v-card-text>
</v-card>
</div>
</template>
<script>
import VueMarkdown from "@adapttive/vue-markdown";
import utils from "@/utils";
export default {
props: {
notes: Array,
},
components: {
VueMarkdown,
},
methods: {
generateKey(item, index) {
return utils.generateUniqueKey(item, index);
},
},
};
</script>
<style>
</style>

View file

@ -1,67 +0,0 @@
<template>
<div>
<h2 class="mb-4">{{ $t("recipe.instructions") }}</h2>
<v-hover
v-for="(step, index) in steps"
:key="generateKey('step', index)"
v-slot="{ hover }"
>
<v-card
class="ma-1"
:class="[{ 'on-hover': hover }, isDisabled(index)]"
:elevation="hover ? 12 : 2"
@click="toggleDisabled(index)"
>
<v-card-title>{{
$t("recipe.step-index", { step: index + 1 })
}}</v-card-title>
<v-card-text>
<vue-markdown :source="step.text"> </vue-markdown>
</v-card-text>
</v-card>
</v-hover>
</div>
</template>
<script>
import VueMarkdown from "@adapttive/vue-markdown";
import utils from "@/utils";
export default {
props: {
steps: Array,
},
components: {
VueMarkdown,
},
data() {
return {
disabledSteps: [],
};
},
methods: {
toggleDisabled(stepIndex) {
if (this.disabledSteps.includes(stepIndex)) {
let index = this.disabledSteps.indexOf(stepIndex);
if (index !== -1) {
this.disabledSteps.splice(index, 1);
}
} else {
this.disabledSteps.push(stepIndex);
}
},
isDisabled(stepIndex) {
if (this.disabledSteps.includes(stepIndex)) {
return "disabled-card";
} else {
return;
}
},
generateKey(item, index) {
return utils.generateUniqueKey(item, index);
},
},
};
</script>
<style>
</style>

View file

@ -31,16 +31,29 @@
</v-row>
<v-row>
<v-col cols="12" sm="12" md="4" lg="4">
<Ingredients :ingredients="ingredients" />
<Ingredients :value="ingredients" :edit="false" />
<div v-if="medium">
<RecipeChips :title="$t('recipe.categories')" :items="categories" />
<RecipeChips
:title="$t('recipe.tags')"
:items="tags"
:isCategory="false"
/>
<Notes :notes="notes" />
<NutritionEditor :value="nutrition" :edit="false" />
<v-card class="mt-2" v-if="categories.length > 0">
<v-card-title class="py-2">
{{ $t("recipe.categories") }}
</v-card-title>
<v-divider class="mx-2"></v-divider>
<v-card-text>
<RecipeChips :items="categories" />
</v-card-text>
</v-card>
<v-card class="mt-2" v-if="tags.length > 0">
<v-card-title class="py-2">
{{ $t("recipe.tags") }}
</v-card-title>
<v-divider class="mx-2"></v-divider>
<v-card-text>
<RecipeChips :items="tags" :isCategory="false" />
</v-card-text>
</v-card>
<Nutrition :value="nutrition" :edit="false" />
<Assets :value="assets" :edit="false" :slug="slug" />
</div>
</v-col>
<v-divider
@ -50,14 +63,15 @@
></v-divider>
<v-col cols="12" sm="12" md="8" lg="8">
<Steps :steps="instructions" />
<Instructions :value="instructions" :edit="false" />
<Notes :value="notes" :edit="false" />
</v-col>
</v-row>
<div v-if="!medium">
<RecipeChips :title="$t('recipe.categories')" :items="categories" />
<RecipeChips :title="$t('recipe.tags')" :items="tags" />
<Notes :notes="notes" />
<NutritionEditor :value="nutrition" :edit="false" />
<Nutrition :value="nutrition" :edit="false" />
<Assets :value="assets" :edit="false" :slug="slug" />
</div>
<v-row class="mt-2 mb-1">
<v-col></v-col>
@ -82,24 +96,27 @@
</template>
<script>
import NutritionEditor from "@/components/Recipe/RecipeEditor/NutritionEditor";
import Nutrition from "@/components/Recipe/Parts/Nutrition";
import VueMarkdown from "@adapttive/vue-markdown";
import utils from "@/utils";
import RecipeChips from "./RecipeChips";
import Steps from "./Steps";
import Notes from "./Notes";
import Ingredients from "./Ingredients";
import Notes from "@/components/Recipe/Parts/Notes";
import Ingredients from "@/components/Recipe/Parts/Ingredients";
import Instructions from "@/components/Recipe/Parts/Instructions.vue";
import Assets from "../Parts/Assets.vue";
export default {
components: {
VueMarkdown,
RecipeChips,
Steps,
Notes,
Ingredients,
NutritionEditor,
Nutrition,
Instructions,
Assets,
},
props: {
name: String,
slug: String,
description: String,
ingredients: Array,
instructions: Array,
@ -110,6 +127,7 @@ export default {
yields: String,
orgURL: String,
nutrition: Object,
assets: Array,
},
data() {
return {

View file

@ -1,5 +1,6 @@
<template>
<div>
<slot name="open" v-bind="{ open }"> </slot>
<v-dialog
v-model="dialog"
:width="modalWidth + 'px'"
@ -25,7 +26,7 @@
Cancel
</v-btn>
<v-spacer></v-spacer>
<v-btn color="success" @click="$emit('submit')">
<v-btn color="success" @click="submitEvent">
Submit
</v-btn>
</slot>
@ -65,6 +66,10 @@ export default {
};
},
methods: {
submitEvent() {
this.$emit("submit");
this.close();
},
open() {
this.dialog = true;
},

View file

@ -105,6 +105,7 @@
"recent": "Recent"
},
"recipe": {
"assets": "Assets",
"add-key": "Add Key",
"api-extras": "API Extras",
"calories": "Calories",
@ -113,8 +114,9 @@
"delete-confirmation": "Are you sure you want to delete this recipe?",
"delete-recipe": "Delete Recipe",
"description": "Description",
"fat-content": "Fat Content",
"fiber-content": "Fiber Content",
"fat-content": "Fat",
"fiber-content": "Fiber",
"carbohydrate-content": "Carbohydrate",
"grams": "grams",
"image": "Image",
"ingredient": "Ingredient",
@ -132,13 +134,13 @@
"original-url": "Original URL",
"perform-time": "Cook Time",
"prep-time": "Prep Time",
"protein-content": "Protein Content",
"protein-content": "Protein",
"recipe-image": "Recipe Image",
"recipe-name": "Recipe Name",
"servings": "Servings",
"sodium-content": "Sodium Content",
"sodium-content": "Sodium",
"step-index": "Step: {step}",
"sugar-content": "Sugar Content",
"sugar-content": "Sugar",
"tags": "Tags",
"title": "Title",
"total-time": "Total Time",

View file

@ -105,6 +105,7 @@
"recent": "Recent"
},
"recipe": {
"assets": "Assets",
"add-key": "Add Key",
"api-extras": "API Extras",
"calories": "Calories",
@ -113,8 +114,9 @@
"delete-confirmation": "Are you sure you want to delete this recipe?",
"delete-recipe": "Delete Recipe",
"description": "Description",
"fat-content": "Fat Content",
"fiber-content": "Fiber Content",
"fat-content": "Fat",
"fiber-content": "Fiber",
"carbohydrate-content": "Carbohydrate",
"grams": "grams",
"image": "Image",
"ingredient": "Ingredient",
@ -132,13 +134,13 @@
"original-url": "Original URL",
"perform-time": "Cook Time",
"prep-time": "Prep Time",
"protein-content": "Protein Content",
"protein-content": "Protein",
"recipe-image": "Recipe Image",
"recipe-name": "Recipe Name",
"servings": "Servings",
"sodium-content": "Sodium Content",
"sodium-content": "Sodium",
"step-index": "Step: {step}",
"sugar-content": "Sugar Content",
"sugar-content": "Sugar",
"tags": "Tags",
"title": "Title",
"total-time": "Total Time",

View file

@ -105,6 +105,7 @@
"recent": "Recent"
},
"recipe": {
"assets": "Assets",
"add-key": "Add Key",
"api-extras": "API Extras",
"calories": "Calories",
@ -113,8 +114,9 @@
"delete-confirmation": "Are you sure you want to delete this recipe?",
"delete-recipe": "Delete Recipe",
"description": "Description",
"fat-content": "Fat Content",
"fiber-content": "Fiber Content",
"fat-content": "Fat",
"fiber-content": "Fiber",
"carbohydrate-content": "Carbohydrate",
"grams": "grams",
"image": "Image",
"ingredient": "Ingredient",
@ -132,13 +134,13 @@
"original-url": "Original URL",
"perform-time": "Cook Time",
"prep-time": "Prep Time",
"protein-content": "Protein Content",
"protein-content": "Protein",
"recipe-image": "Recipe Image",
"recipe-name": "Recipe Name",
"servings": "Servings",
"sodium-content": "Sodium Content",
"sodium-content": "Sodium",
"step-index": "Step: {step}",
"sugar-content": "Sugar Content",
"sugar-content": "Sugar",
"tags": "Tags",
"title": "Title",
"total-time": "Total Time",

View file

@ -105,6 +105,7 @@
"recent": "Recent"
},
"recipe": {
"assets": "Assets",
"add-key": "Add Key",
"api-extras": "API Extras",
"calories": "Calories",
@ -113,8 +114,9 @@
"delete-confirmation": "Are you sure you want to delete this recipe?",
"delete-recipe": "Delete Recipe",
"description": "Description",
"fat-content": "Fat Content",
"fiber-content": "Fiber Content",
"fat-content": "Fat",
"fiber-content": "Fiber",
"carbohydrate-content": "Carbohydrate",
"grams": "grams",
"image": "Image",
"ingredient": "Ingredient",
@ -132,13 +134,13 @@
"original-url": "Original URL",
"perform-time": "Cook Time",
"prep-time": "Prep Time",
"protein-content": "Protein Content",
"protein-content": "Protein",
"recipe-image": "Recipe Image",
"recipe-name": "Recipe Name",
"servings": "Servings",
"sodium-content": "Sodium Content",
"sodium-content": "Sodium",
"step-index": "Step: {step}",
"sugar-content": "Sugar Content",
"sugar-content": "Sugar",
"tags": "Tags",
"title": "Title",
"total-time": "Total Time",

View file

@ -105,6 +105,7 @@
"recent": "Seneste"
},
"recipe": {
"assets": "Assets",
"add-key": "Add Key",
"api-extras": "API Extras",
"calories": "Calories",
@ -113,8 +114,9 @@
"delete-confirmation": "Are you sure you want to delete this recipe?",
"delete-recipe": "Delete Recipe",
"description": "Beskrivelse",
"fat-content": "Fat Content",
"fiber-content": "Fiber Content",
"fat-content": "Fat",
"fiber-content": "Fiber",
"carbohydrate-content": "Carbohydrate",
"grams": "grams",
"image": "Image",
"ingredient": "Ingrediens",
@ -132,13 +134,13 @@
"original-url": "Oprindelig opskrift",
"perform-time": "Cook Time",
"prep-time": "Prep Time",
"protein-content": "Protein Content",
"protein-content": "Protein",
"recipe-image": "Recipe Image",
"recipe-name": "Opskriftens navn",
"servings": "Portioner",
"sodium-content": "Sodium Content",
"sodium-content": "Sodium",
"step-index": "Trin: {step}",
"sugar-content": "Sugar Content",
"sugar-content": "Sugar",
"tags": "Mærker",
"title": "Title",
"total-time": "Total Time",

View file

@ -105,6 +105,7 @@
"recent": "Neueste"
},
"recipe": {
"assets": "Assets",
"add-key": "Schlüssel hinzufügen",
"api-extras": "API Extras",
"calories": "Kalorien",
@ -113,8 +114,9 @@
"delete-confirmation": "Bist du dir sicher, dass du dieses Rezept löschen möchtest?",
"delete-recipe": "Rezept löschen",
"description": "Beschreibung",
"fat-content": "Fettgehalt",
"fiber-content": "Ballaststoffe",
"fat-content": "Fat",
"fiber-content": "Fiber",
"carbohydrate-content": "Carbohydrate",
"grams": "g",
"image": "Bild",
"ingredient": "Zutat",
@ -132,13 +134,13 @@
"original-url": "Ursprüngliche URL",
"perform-time": "Kochzeit",
"prep-time": "Vorbereitung",
"protein-content": "Eiweißgehalt",
"protein-content": "Protein",
"recipe-image": "Rezeptbild",
"recipe-name": "Rezeptname",
"servings": "Portionen",
"sodium-content": "Natriumgehalt",
"sodium-content": "Sodium",
"step-index": "Schritt {step}:",
"sugar-content": "Zuckergehalt",
"sugar-content": "Sugar",
"tags": "Schlagworte",
"title": "Titel",
"total-time": "Gesamtzeit",
@ -152,7 +154,7 @@
"exclude": "Ausschließen",
"include": "Einbeziehen",
"or": "Oder",
"and": "and",
"and": "und",
"search": "Suchen",
"tag-filter": "Schlagwortfilter"
},
@ -221,7 +223,7 @@
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"new-name": "Neuer Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",

View file

@ -105,6 +105,7 @@
"recent": "Recent"
},
"recipe": {
"assets": "Assets",
"add-key": "Add Key",
"api-extras": "API Extras",
"calories": "Calories",
@ -113,8 +114,9 @@
"delete-confirmation": "Are you sure you want to delete this recipe?",
"delete-recipe": "Delete Recipe",
"description": "Description",
"fat-content": "Fat Content",
"fiber-content": "Fiber Content",
"fat-content": "Fat",
"fiber-content": "Fiber",
"carbohydrate-content": "Carbohydrate",
"grams": "grams",
"image": "Image",
"ingredient": "Ingredient",
@ -132,13 +134,13 @@
"original-url": "Original URL",
"perform-time": "Cook Time",
"prep-time": "Prep Time",
"protein-content": "Protein Content",
"protein-content": "Protein",
"recipe-image": "Recipe Image",
"recipe-name": "Recipe Name",
"servings": "Servings",
"sodium-content": "Sodium Content",
"sodium-content": "Sodium",
"step-index": "Step: {step}",
"sugar-content": "Sugar Content",
"sugar-content": "Sugar",
"tags": "Tags",
"title": "Title",
"total-time": "Total Time",

View file

@ -161,6 +161,7 @@
"recent": "Recent"
},
"recipe": {
"assets": "Assets",
"add-key": "Add Key",
"api-extras": "API Extras",
"calories": "Calories",
@ -169,8 +170,9 @@
"delete-confirmation": "Are you sure you want to delete this recipe?",
"delete-recipe": "Delete Recipe",
"description": "Description",
"fat-content": "Fat Content",
"fiber-content": "Fiber Content",
"fat-content": "Fat",
"fiber-content": "Fiber",
"carbohydrate-content": "Carbohydrate",
"grams": "grams",
"ingredient": "Ingredient",
"ingredients": "Ingredients",
@ -187,7 +189,7 @@
"original-url": "Original URL",
"perform-time": "Cook Time",
"prep-time": "Prep Time",
"protein-content": "Protein Content",
"protein-content": "Protein",
"recipe-created": "Recipe created",
"recipe-creation-failed": "Recipe creation failed",
"recipe-deleted": "Recipe deleted",
@ -197,9 +199,9 @@
"recipe-update-failed": "Recipe update failed",
"recipe-updated": "Recipe updated",
"servings": "Servings",
"sodium-content": "Sodium Content",
"sodium-content": "Sodium",
"step-index": "Step: {step}",
"sugar-content": "Sugar Content",
"sugar-content": "Sugar",
"tags": "Tags",
"title": "Title",
"total-time": "Total Time",

View file

@ -105,6 +105,7 @@
"recent": "Recent"
},
"recipe": {
"assets": "Assets",
"add-key": "Add Key",
"api-extras": "API Extras",
"calories": "Calories",
@ -113,8 +114,9 @@
"delete-confirmation": "Are you sure you want to delete this recipe?",
"delete-recipe": "Delete Recipe",
"description": "Description",
"fat-content": "Fat Content",
"fiber-content": "Fiber Content",
"fat-content": "Fat",
"fiber-content": "Fiber",
"carbohydrate-content": "Carbohydrate",
"grams": "grams",
"image": "Image",
"ingredient": "Ingredient",
@ -132,13 +134,13 @@
"original-url": "Original URL",
"perform-time": "Cook Time",
"prep-time": "Prep Time",
"protein-content": "Protein Content",
"protein-content": "Protein",
"recipe-image": "Recipe Image",
"recipe-name": "Recipe Name",
"servings": "Servings",
"sodium-content": "Sodium Content",
"sodium-content": "Sodium",
"step-index": "Step: {step}",
"sugar-content": "Sugar Content",
"sugar-content": "Sugar",
"tags": "Tags",
"title": "Title",
"total-time": "Total Time",

View file

@ -105,6 +105,7 @@
"recent": "Recent"
},
"recipe": {
"assets": "Assets",
"add-key": "Add Key",
"api-extras": "API Extras",
"calories": "Calories",
@ -113,8 +114,9 @@
"delete-confirmation": "Are you sure you want to delete this recipe?",
"delete-recipe": "Delete Recipe",
"description": "Description",
"fat-content": "Fat Content",
"fiber-content": "Fiber Content",
"fat-content": "Fat",
"fiber-content": "Fiber",
"carbohydrate-content": "Carbohydrate",
"grams": "grams",
"image": "Image",
"ingredient": "Ingredient",
@ -132,13 +134,13 @@
"original-url": "Original URL",
"perform-time": "Cook Time",
"prep-time": "Prep Time",
"protein-content": "Protein Content",
"protein-content": "Protein",
"recipe-image": "Recipe Image",
"recipe-name": "Recipe Name",
"servings": "Servings",
"sodium-content": "Sodium Content",
"sodium-content": "Sodium",
"step-index": "Step: {step}",
"sugar-content": "Sugar Content",
"sugar-content": "Sugar",
"tags": "Tags",
"title": "Title",
"total-time": "Total Time",

View file

@ -105,6 +105,7 @@
"recent": "Récent"
},
"recipe": {
"assets": "Assets",
"add-key": "Ajouter une clé",
"api-extras": "Extras API",
"calories": "Calories",
@ -113,8 +114,9 @@
"delete-confirmation": "Êtes-vous sûr(e) de vouloir supprimer cette recette ?",
"delete-recipe": "Supprimer la recette",
"description": "Description",
"fat-content": "Lipides",
"fiber-content": "Fibres",
"fat-content": "Fat",
"fiber-content": "Fiber",
"carbohydrate-content": "Carbohydrate",
"grams": "grammes",
"image": "Image",
"ingredient": "Ingrédient",
@ -132,13 +134,13 @@
"original-url": "Recette originale",
"perform-time": "Temps de cuisson",
"prep-time": "Temps de préparation",
"protein-content": "Protéines",
"protein-content": "Protein",
"recipe-image": "Image de la recette",
"recipe-name": "Nom de la recette",
"servings": "Portions",
"sodium-content": "Sels",
"sodium-content": "Sodium",
"step-index": "Étape : {step}",
"sugar-content": "Glucides",
"sugar-content": "Sugar",
"tags": "Mots-clés",
"title": "Titre",
"total-time": "Temps total",
@ -152,7 +154,7 @@
"exclude": "Exclure",
"include": "Inclure",
"or": "Ou",
"and": "and",
"and": "et",
"search": "Rechercher",
"tag-filter": "Filtre par mots-clés"
},
@ -220,14 +222,14 @@
"webhook-url": "Lien du webhook"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"toolbox": "Boîte à outils",
"new-name": "Nouveau nom",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
"title-case-all": "Majuscules partout",
"no-unused-items": "Aucun élément inutilisé",
"remove-unused": "Supprimer orphelins",
"assign-all": "Assigner tout",
"bulk-assign": "Assigner en masse"
}
},
"user": {

View file

@ -105,6 +105,7 @@
"recent": "Recent"
},
"recipe": {
"assets": "Assets",
"add-key": "Add Key",
"api-extras": "API Extras",
"calories": "Calories",
@ -113,8 +114,9 @@
"delete-confirmation": "Are you sure you want to delete this recipe?",
"delete-recipe": "Delete Recipe",
"description": "Description",
"fat-content": "Fat Content",
"fiber-content": "Fiber Content",
"fat-content": "Fat",
"fiber-content": "Fiber",
"carbohydrate-content": "Carbohydrate",
"grams": "grams",
"image": "Image",
"ingredient": "Ingredient",
@ -132,13 +134,13 @@
"original-url": "Original URL",
"perform-time": "Cook Time",
"prep-time": "Prep Time",
"protein-content": "Protein Content",
"protein-content": "Protein",
"recipe-image": "Recipe Image",
"recipe-name": "Recipe Name",
"servings": "Servings",
"sodium-content": "Sodium Content",
"sodium-content": "Sodium",
"step-index": "Step: {step}",
"sugar-content": "Sugar Content",
"sugar-content": "Sugar",
"tags": "Tags",
"title": "Title",
"total-time": "Total Time",

View file

@ -105,6 +105,7 @@
"recent": "Recent"
},
"recipe": {
"assets": "Assets",
"add-key": "Add Key",
"api-extras": "API Extras",
"calories": "Calories",
@ -113,8 +114,9 @@
"delete-confirmation": "Are you sure you want to delete this recipe?",
"delete-recipe": "Delete Recipe",
"description": "Description",
"fat-content": "Fat Content",
"fiber-content": "Fiber Content",
"fat-content": "Fat",
"fiber-content": "Fiber",
"carbohydrate-content": "Carbohydrate",
"grams": "grams",
"image": "Image",
"ingredient": "Ingredient",
@ -132,13 +134,13 @@
"original-url": "Original URL",
"perform-time": "Cook Time",
"prep-time": "Prep Time",
"protein-content": "Protein Content",
"protein-content": "Protein",
"recipe-image": "Recipe Image",
"recipe-name": "Recipe Name",
"servings": "Servings",
"sodium-content": "Sodium Content",
"sodium-content": "Sodium",
"step-index": "Step: {step}",
"sugar-content": "Sugar Content",
"sugar-content": "Sugar",
"tags": "Tags",
"title": "Title",
"total-time": "Total Time",

View file

@ -105,6 +105,7 @@
"recent": "Recent"
},
"recipe": {
"assets": "Assets",
"add-key": "Add Key",
"api-extras": "API Extras",
"calories": "Calories",
@ -113,8 +114,9 @@
"delete-confirmation": "Are you sure you want to delete this recipe?",
"delete-recipe": "Delete Recipe",
"description": "Description",
"fat-content": "Fat Content",
"fiber-content": "Fiber Content",
"fat-content": "Fat",
"fiber-content": "Fiber",
"carbohydrate-content": "Carbohydrate",
"grams": "grams",
"image": "Image",
"ingredient": "Ingredient",
@ -132,13 +134,13 @@
"original-url": "Original URL",
"perform-time": "Cook Time",
"prep-time": "Prep Time",
"protein-content": "Protein Content",
"protein-content": "Protein",
"recipe-image": "Recipe Image",
"recipe-name": "Recipe Name",
"servings": "Servings",
"sodium-content": "Sodium Content",
"sodium-content": "Sodium",
"step-index": "Step: {step}",
"sugar-content": "Sugar Content",
"sugar-content": "Sugar",
"tags": "Tags",
"title": "Title",
"total-time": "Total Time",

View file

@ -105,6 +105,7 @@
"recent": "Recent"
},
"recipe": {
"assets": "Assets",
"add-key": "Add Key",
"api-extras": "API Extras",
"calories": "Calories",
@ -113,8 +114,9 @@
"delete-confirmation": "Are you sure you want to delete this recipe?",
"delete-recipe": "Delete Recipe",
"description": "Description",
"fat-content": "Fat Content",
"fiber-content": "Fiber Content",
"fat-content": "Fat",
"fiber-content": "Fiber",
"carbohydrate-content": "Carbohydrate",
"grams": "grams",
"image": "Image",
"ingredient": "Ingredient",
@ -132,13 +134,13 @@
"original-url": "Original URL",
"perform-time": "Cook Time",
"prep-time": "Prep Time",
"protein-content": "Protein Content",
"protein-content": "Protein",
"recipe-image": "Recipe Image",
"recipe-name": "Recipe Name",
"servings": "Servings",
"sodium-content": "Sodium Content",
"sodium-content": "Sodium",
"step-index": "Step: {step}",
"sugar-content": "Sugar Content",
"sugar-content": "Sugar",
"tags": "Tags",
"title": "Title",
"total-time": "Total Time",

View file

@ -105,6 +105,7 @@
"recent": "Recent"
},
"recipe": {
"assets": "Assets",
"add-key": "Add Key",
"api-extras": "API Extras",
"calories": "Calories",
@ -113,8 +114,9 @@
"delete-confirmation": "Are you sure you want to delete this recipe?",
"delete-recipe": "Delete Recipe",
"description": "Description",
"fat-content": "Fat Content",
"fiber-content": "Fiber Content",
"fat-content": "Fat",
"fiber-content": "Fiber",
"carbohydrate-content": "Carbohydrate",
"grams": "grams",
"image": "Image",
"ingredient": "Ingredient",
@ -132,13 +134,13 @@
"original-url": "Original URL",
"perform-time": "Cook Time",
"prep-time": "Prep Time",
"protein-content": "Protein Content",
"protein-content": "Protein",
"recipe-image": "Recipe Image",
"recipe-name": "Recipe Name",
"servings": "Servings",
"sodium-content": "Sodium Content",
"sodium-content": "Sodium",
"step-index": "Step: {step}",
"sugar-content": "Sugar Content",
"sugar-content": "Sugar",
"tags": "Tags",
"title": "Title",
"total-time": "Total Time",

View file

@ -105,6 +105,7 @@
"recent": "Recent(e)"
},
"recipe": {
"assets": "Assets",
"add-key": "Sleutel toevoegen",
"api-extras": "API Extras",
"calories": "Calorieën",
@ -113,8 +114,9 @@
"delete-confirmation": "Weet u zeker dat u dit recept wilt verwijderen?",
"delete-recipe": "Recept verwijderen",
"description": "Omschrijving",
"fat-content": "Vetten",
"fiber-content": "Vezels",
"fat-content": "Fat",
"fiber-content": "Fiber",
"carbohydrate-content": "Carbohydrate",
"grams": "gram",
"image": "Afbeelding",
"ingredient": "Ingrediënt",
@ -132,13 +134,13 @@
"original-url": "Oorspronkelijke URL",
"perform-time": "Kook tijd",
"prep-time": "Voorbereiding tijd",
"protein-content": "Eiwitten",
"protein-content": "Protein",
"recipe-image": "Recept afbeelding",
"recipe-name": "Receptnaam",
"servings": "Porties",
"sodium-content": "Sodium gehalte",
"sodium-content": "Sodium",
"step-index": "Stap: {step}",
"sugar-content": "Suiker gehalte",
"sugar-content": "Sugar",
"tags": "Labels",
"title": "Titel",
"total-time": "Totale Tijd",

View file

@ -105,6 +105,7 @@
"recent": "Recent"
},
"recipe": {
"assets": "Assets",
"add-key": "Add Key",
"api-extras": "API Extras",
"calories": "Calories",
@ -113,8 +114,9 @@
"delete-confirmation": "Are you sure you want to delete this recipe?",
"delete-recipe": "Delete Recipe",
"description": "Description",
"fat-content": "Fat Content",
"fiber-content": "Fiber Content",
"fat-content": "Fat",
"fiber-content": "Fiber",
"carbohydrate-content": "Carbohydrate",
"grams": "grams",
"image": "Image",
"ingredient": "Ingredient",
@ -132,13 +134,13 @@
"original-url": "Original URL",
"perform-time": "Cook Time",
"prep-time": "Prep Time",
"protein-content": "Protein Content",
"protein-content": "Protein",
"recipe-image": "Recipe Image",
"recipe-name": "Recipe Name",
"servings": "Servings",
"sodium-content": "Sodium Content",
"sodium-content": "Sodium",
"step-index": "Step: {step}",
"sugar-content": "Sugar Content",
"sugar-content": "Sugar",
"tags": "Tags",
"title": "Title",
"total-time": "Total Time",

View file

@ -4,65 +4,65 @@
"take-me-home": "Powrót na stronę główną"
},
"about": {
"about-mealie": "About Mealie",
"api-docs": "API Docs",
"api-port": "API Port",
"application-mode": "Application Mode",
"database-type": "Database Type",
"default-group": "Default Group",
"about-mealie": "O Mealie",
"api-docs": "Dokumentacja API",
"api-port": "Port API",
"application-mode": "Tryb aplikacji",
"database-type": "Rodzaj bazy danych",
"default-group": "Domyślna grupa",
"demo": "Demo",
"demo-status": "Demo Status",
"development": "Development",
"not-demo": "Not Demo",
"production": "Production",
"sqlite-file": "SQLite File",
"version": "Version"
"demo-status": "Status demo",
"development": "Wersja testowa",
"not-demo": "Nie demo",
"production": "Produkcyjna",
"sqlite-file": "Plik SQLite'a",
"version": "Wersja"
},
"general": {
"apply": "Apply",
"apply": "Zastosuj",
"cancel": "Anuluj",
"close": "Zamknij",
"confirm": "Potwierdź",
"create": "Utwórz",
"current-parenthesis": "(Current)",
"current-parenthesis": "(Bieżący)",
"delete": "Usuń",
"disabled": "Disabled",
"disabled": "Wyłączone",
"download": "Pobierz",
"edit": "Edytuj",
"enabled": "Włączone",
"field-required": "Field Required",
"filter": "Filter",
"friday": "Friday",
"get": "Get",
"groups": "Groups",
"field-required": "Pole jest wymagane",
"filter": "Filtruj",
"friday": "Piątek",
"get": "Zastosuj",
"groups": "Grupy",
"import": "Importuj",
"monday": "Monday",
"monday": "Poniedziałek",
"name": "Nazwa",
"no": "No",
"no": "Nie",
"ok": "OK",
"options": "Opcje",
"random": "Losowa",
"recent": "Recent",
"recent": "Najnowsze",
"recipes": "Przepisy",
"reset": "Reset",
"saturday": "Saturday",
"reset": "Resetuj",
"saturday": "Sobota",
"save": "Zapisz",
"settings": "Ustawienia",
"sort": "Sort",
"sort": "Sortuj",
"sort-alphabetically": "A-Z",
"submit": "Zatwierdź",
"sunday": "Sunday",
"sunday": "Niedziela",
"templates": "Szablony",
"themes": "Motywy",
"thursday": "Thursday",
"thursday": "Czwartek",
"token": "Token",
"tuesday": "Tuesday",
"tuesday": "Wtorek",
"update": "Uaktualnij",
"upload": "Wrzuć",
"upload": "Prześlij",
"url": "URL",
"users": "Users",
"wednesday": "Wednesday",
"yes": "Yes"
"users": "Użytkownicy",
"wednesday": "Środa",
"yes": "Tak"
},
"meal-plan": {
"create-a-new-meal-plan": "Utwórz nowy plan posiłku",
@ -70,13 +70,13 @@
"dinner-today": "Obiad dziś",
"edit-meal-plan": "Edytuj plan posiłku",
"end-date": "Data zakończenia",
"group": "Group (Beta)",
"meal-planner": "Meal Planner",
"group": "Grupa (Beta)",
"meal-planner": "Plan posiłków",
"meal-plans": "Plany posiłku",
"only-recipes-with-these-categories-will-be-used-in-meal-plans": "Only recipes with these categories will be used in Meal Plans",
"only-recipes-with-these-categories-will-be-used-in-meal-plans": "Tylko przepisy z tych kategorii będą używane w planach posiłków",
"planner": "Planer",
"quick-week": "Quick Week",
"shopping-list": "Shopping List",
"quick-week": "Szybki plan na tydzień",
"shopping-list": "Lista zakupów",
"start-date": "Data rozpoczęcia"
},
"migration": {
@ -93,73 +93,75 @@
},
"new-recipe": {
"bulk-add": "Dodanie zbiorcze",
"error-message": "Wygląda na to, że wystąpił błąd. Sprawdź log i debug/last_recipe.json aby zasięgnąć po więcej informacji.",
"from-url": "Z odnośnika",
"paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "Przeklej zawartość przepisu. Każda indywidualna linia traktowana będzie jako pozycja na liście",
"recipe-url": "Odnośnik przepisu",
"url-form-hint": "Copy and paste a link from your favorite recipe website"
"error-message": "Wygląda na to, że wystąpił błąd. Sprawdź log i debug/last_recipe.json aby sprawdzić co poszło nie tak.",
"from-url": "Importuj przepis",
"paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "Wklej zawartość przepisu. Każda indywidualna linia traktowana będzie jako pozycja na liście",
"recipe-url": "Adres URL przepisu",
"url-form-hint": "Skopiuj i wklej link ze swojej ulubionej strony z przepisami"
},
"page": {
"all-recipes": "All Recipes",
"home-page": "Home Page",
"recent": "Recent"
"all-recipes": "Wszystkie przepisy",
"home-page": "Strona Główna",
"recent": "Najnowsze"
},
"recipe": {
"assets": "Assets",
"add-key": "Dodaj klucz",
"api-extras": "Dodatki API",
"calories": "Calories",
"calories-suffix": "calories",
"calories": "Kalorie",
"calories-suffix": "kalorie",
"categories": "Kategorie",
"delete-confirmation": "Czy jesteś pewien, że chcesz usunąć ten przepis?",
"delete-recipe": "Usuń przepis",
"description": "Opis",
"fat-content": "Fat Content",
"fiber-content": "Fiber Content",
"grams": "grams",
"image": "Image",
"fat-content": "Fat",
"fiber-content": "Fiber",
"carbohydrate-content": "Carbohydrate",
"grams": "gram",
"image": "Zdjęcie",
"ingredient": "Składnik",
"ingredients": "Składniki",
"instructions": "Instrukcje",
"key-name-required": "Nazwa klucza jest wymagana",
"milligrams": "milligrams",
"milligrams": "miligram",
"new-key-name": "Nazwa nowego klucza",
"no-white-space-allowed": "Znaki niedrukowalne są niedozwolone",
"note": "Notatka",
"notes": "Notatki",
"nutrition": "Nutrition",
"nutrition": "Wartości odżywcze",
"object-key": "Klucz obiektu",
"object-value": "Wartość obiektu",
"original-url": "Oryginalny odnośnik",
"original-url": "Oryginalny adres URL",
"perform-time": "Czas gotowania",
"prep-time": "Czas przyrządzania",
"protein-content": "Protein Content",
"recipe-image": "Recipe Image",
"protein-content": "Protein",
"recipe-image": "Zdjęcie do przepisu",
"recipe-name": "Nazwa przepisu",
"servings": "Porcje",
"sodium-content": "Sodium Content",
"sodium-content": "Sodium",
"step-index": "Krok: {step}",
"sugar-content": "Sugar Content",
"tags": "Etykiety",
"sugar-content": "Sugar",
"tags": "Tagi",
"title": "Tytuł",
"total-time": "Czas całkowity",
"view-recipe": "Wyświetl przepis"
},
"search": {
"search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...",
"max-results": "Max Results",
"category-filter": "Category Filter",
"exclude": "Exclude",
"include": "Include",
"or": "Or",
"and": "and",
"search": "Search",
"tag-filter": "Tag Filter"
"search-mealie": "Przeszukaj Mealie (naciśnij /)",
"search-placeholder": "Szukaj...",
"max-results": "Maksymalna liczba wyników",
"category-filter": "Filtr kategorii",
"exclude": "Wyklucz",
"include": "Dołącz",
"or": "Lub",
"and": "oraz",
"search": "Szukaj",
"tag-filter": "Filtr tagów"
},
"settings": {
"add-a-new-theme": "Dodaj nowy motyw",
"admin-settings": "Admin Settings",
"available-backups": "Dostępne kopie zapsowe",
"admin-settings": "Ustawienia administratora",
"available-backups": "Dostępne kopie zapasowe",
"backup": {
"backup-tag": "Etykieta kopii zapasowej",
"create-heading": "Utwórz kopię zapasową",
@ -167,43 +169,43 @@
"partial-backup": "Częściowa kopia zapasowa"
},
"backup-and-exports": "Kopie zapasowe",
"backup-info": "Kopie zapasowe zapisywane są w standardowym formacie JSON wraz ze zdjęciami w systemie plików. W katalogu kopii zapasowej znajdziesz plik z rozszerzeniem .zip zawierający wszystkie przepisy i zdjęcia z bazy danych. Jeśli oznaczone zostały pliki markdown, one także znajdą się w pliku .zip. Aby zaimportować kopię, musi ona znajdować się w folderze kopii zapasowych. Kopie automatyczne tworzone są codziennie o godzinie 03:00.",
"change-password": "Change Password",
"backup-info": "Kopie zapasowe zapisywane są w standardowym formacie JSON wraz ze zdjęciami w systemie plików. W katalogu kopii zapasowej znajdziesz plik z rozszerzeniem .zip zawierający wszystkie przepisy i zdjęcia z bazy danych. Jeśli zaznaczone zostały pliki markdown, one także znajdą się w pliku .zip. Aby zaimportować kopię, musi ona znajdować się w folderze kopii zapasowych. Kopie automatyczne tworzone są codziennie o godzinie 03:00.",
"change-password": "Zmień hasło",
"current": "Wersja:",
"custom-pages": "Custom Pages",
"edit-page": "Edit Page",
"first-day-of-week": "First day of the week",
"custom-pages": "Niestandardowe strony",
"edit-page": "Edytuj stronę",
"first-day-of-week": "Pierwszy dzień tygodnia",
"homepage": {
"all-categories": "All Categories",
"card-per-section": "Card Per Section",
"home-page": "Home Page",
"home-page-sections": "Home Page Sections",
"show-recent": "Show Recent"
"all-categories": "Wszystkie kategorie",
"card-per-section": "Karty na sekcję",
"home-page": "Strona Główna",
"home-page-sections": "Sekcje strony głównej",
"show-recent": "Pokaż najnowsze"
},
"language": "Język",
"latest": "Najnowsza",
"local-api": "Lokalne API",
"locale-settings": "Locale settings",
"manage-users": "Manage Users",
"migrations": "Migrations",
"new-page": "New Page",
"page-name": "Page Name",
"profile": "Profile",
"remove-existing-entries-matching-imported-entries": "Remove existing entries matching imported entries",
"locale-settings": "Ustawienia językowe",
"manage-users": "Zarządzaj użytkownikami",
"migrations": "Migracje",
"new-page": "Nowa Strona",
"page-name": "Nazwa strony",
"profile": "Profil",
"remove-existing-entries-matching-imported-entries": "Usuń istniejące wpisy pasujące do importowanych wpisów",
"set-new-time": "Ustaw nowy czas",
"site-settings": "Site Settings",
"site-settings": "Ustawienia strony",
"theme": {
"accent": "Akcent",
"are-you-sure-you-want-to-delete-this-theme": "Czy jesteś pewien, że chcesz usunąć ten motyw?",
"choose-how-mealie-looks-to-you-set-your-theme-preference-to-follow-your-system-settings-or-choose-to-use-the-light-or-dark-theme": "Wybierz jak Mealie ma dla Ciebie wyglądać. Dostępne opcje to podążanie za odcieniem systemowym, bądź motyw jasny lub ciemny.",
"dark": "Ciemny",
"dark-mode": "Ciemny motyw",
"default-to-system": "Domyślny dla systemu",
"default-to-system": "Motyw systemowy",
"delete-theme": "Usuń motyw",
"error": "Błąd",
"info": "Informacja",
"light": "Jasny",
"primary": "Pierwszorzędny",
"primary": "Podstawowy",
"secondary": "Drugorzędny",
"select-a-theme-from-the-dropdown-or-create-a-new-theme-note-that-the-default-theme-will-be-served-to-all-users-who-have-not-set-a-theme-preference": "Wybierz motyw z rozwijanej listy bądź stwórz nowy. Domyślny motyw zostanie użyty dla wszystkich użytkowników którzy nie wybrali własnej preferencji.",
"success": "Powodzenie",
@ -217,63 +219,63 @@
"meal-planner-webhooks": "Webhooki planera posiłków",
"test-webhooks": "Testuj webhooki",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "Odnośniki poniżej otrzymają webhook zawierający dane o przepisie dla danego dnia. Aktualnie webhooki zostanę wykonane o",
"webhook-url": "Odnośnik webhooka"
"webhook-url": "URL webhooka"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
"toolbox": "Przybornik",
"new-name": "Nowa nazwa",
"recipes-effected": "Objęte przepisy",
"title-case-all": "Wszędzie wielkie litery",
"no-unused-items": "Brak nieużywanych elementów",
"remove-unused": "Usuń nieużywane",
"assign-all": "Przypisz wszystkie",
"bulk-assign": "Masowe przypisanie"
}
},
"user": {
"admin": "Admin",
"are-you-sure-you-want-to-delete-the-group": "Are you sure you want to delete <b>{groupName}<b/>?",
"are-you-sure-you-want-to-delete-the-link": "Are you sure you want to delete the link <b>{link}<b/>?",
"are-you-sure-you-want-to-delete-the-user": "Are you sure you want to delete the user <b>{activeName} ID: {activeId}<b/>?",
"confirm-group-deletion": "Confirm Group Deletion",
"confirm-link-deletion": "Confirm Link Deletion",
"confirm-password": "Confirm Password",
"confirm-user-deletion": "Confirm User Deletion",
"could-not-validate-credentials": "Could Not Validate Credentials",
"create-group": "Create Group",
"create-link": "Create Link",
"create-user": "Create User",
"current-password": "Current Password",
"e-mail-must-be-valid": "E-mail must be valid",
"edit-user": "Edit User",
"admin": "Administrator",
"are-you-sure-you-want-to-delete-the-group": "Czy na pewno chcesz usunąć <b>{groupName}<b/>?",
"are-you-sure-you-want-to-delete-the-link": "Czy na pewno chcesz usunąć link <b>{link}<b/>?",
"are-you-sure-you-want-to-delete-the-user": "Czy na pewno chcesz usunąć użytkownika <b>{activeName} ID: {activeId}<b/>?",
"confirm-group-deletion": "Potwierdź usunięcie grupy",
"confirm-link-deletion": "Potwierdź usunięcie linku",
"confirm-password": "Potwierdź hasło",
"confirm-user-deletion": "Potwierdź usunięcie użytkownika",
"could-not-validate-credentials": "Nie można zweryfikować danych logowania",
"create-group": "Utwórz Grupę",
"create-link": "Utwórz link",
"create-user": "Utwórz użytkownika",
"current-password": "Obecne hasło",
"e-mail-must-be-valid": "E-mail musi być poprawny",
"edit-user": "Edytuj użytkownika",
"email": "Email",
"full-name": "Full Name",
"group": "Group",
"group-id-with-value": "Group ID: {groupID}",
"group-name": "Group Name",
"groups": "Groups",
"groups-can-only-be-set-by-administrators": "Groups can only be set by administrators",
"link-id": "Link ID",
"link-name": "Link Name",
"full-name": "Imię i nazwisko",
"group": "Grupa",
"group-id-with-value": "ID grupy: {groupID}",
"group-name": "Nazwa grupy",
"groups": "Grupy",
"groups-can-only-be-set-by-administrators": "Grupy mogą być ustawiane tylko przez administratorów",
"link-id": "ID linku",
"link-name": "Nazwa linku",
"login": "Login",
"logout": "Logout",
"new-password": "New Password",
"new-user": "New User",
"logout": "Wyloguj się",
"new-password": "Nowe Hasło",
"new-user": "Nowy użytkownik",
"password": "Hasło",
"password-must-match": "Password must match",
"reset-password": "Reset Password",
"password-must-match": "Hasła muszą być takie same",
"reset-password": "Zresetuj hasło",
"sign-in": "Zaloguj się",
"sign-up-links": "Sign Up Links",
"total-mealplans": "Total MealPlans",
"total-users": "Total Users",
"upload-photo": "Upload Photo",
"use-8-characters-or-more-for-your-password": "Use 8 characters or more for your password",
"user-group": "User Group",
"user-id": "User ID",
"user-id-with-value": "User ID: {id}",
"user-password": "User Password",
"users": "Users",
"webhook-time": "Webhook Time",
"webhooks-enabled": "Webhooks Enabled"
"sign-up-links": "Linki do rejestracji",
"total-mealplans": "Ilość planów posiłków",
"total-users": "Ilość użytkowników",
"upload-photo": "Prześlij zdjęcie",
"use-8-characters-or-more-for-your-password": "Hasło musi składać się z 8 lub więcej znaków",
"user-group": "Grupa użytkownika",
"user-id": "ID użytkownika",
"user-id-with-value": "ID użytkownika: {id}",
"user-password": "Hasło użytkownika",
"users": "Użytkownicy",
"webhook-time": "Czas webhooka",
"webhooks-enabled": "Webhooki włączone"
}
}

View file

@ -105,6 +105,7 @@
"recent": "Recent"
},
"recipe": {
"assets": "Assets",
"add-key": "Add Key",
"api-extras": "API Extras",
"calories": "Calories",
@ -113,8 +114,9 @@
"delete-confirmation": "Are you sure you want to delete this recipe?",
"delete-recipe": "Delete Recipe",
"description": "Description",
"fat-content": "Fat Content",
"fiber-content": "Fiber Content",
"fat-content": "Fat",
"fiber-content": "Fiber",
"carbohydrate-content": "Carbohydrate",
"grams": "grams",
"image": "Image",
"ingredient": "Ingredient",
@ -132,13 +134,13 @@
"original-url": "Original URL",
"perform-time": "Cook Time",
"prep-time": "Prep Time",
"protein-content": "Protein Content",
"protein-content": "Protein",
"recipe-image": "Recipe Image",
"recipe-name": "Recipe Name",
"servings": "Servings",
"sodium-content": "Sodium Content",
"sodium-content": "Sodium",
"step-index": "Step: {step}",
"sugar-content": "Sugar Content",
"sugar-content": "Sugar",
"tags": "Tags",
"title": "Title",
"total-time": "Total Time",

View file

@ -105,6 +105,7 @@
"recent": "Recent"
},
"recipe": {
"assets": "Assets",
"add-key": "Adicionar Chave",
"api-extras": "Extras API",
"calories": "Calories",
@ -113,8 +114,9 @@
"delete-confirmation": "Tem a certeza que deseja eliminar esta receita?",
"delete-recipe": "Eliminar Receita",
"description": "Descrição",
"fat-content": "Fat Content",
"fiber-content": "Fiber Content",
"fat-content": "Fat",
"fiber-content": "Fiber",
"carbohydrate-content": "Carbohydrate",
"grams": "grams",
"image": "Image",
"ingredient": "Ingrediente",
@ -132,13 +134,13 @@
"original-url": "URL Original",
"perform-time": "Tempo de Cozedura",
"prep-time": "Tempo de Preparação",
"protein-content": "Protein Content",
"protein-content": "Protein",
"recipe-image": "Recipe Image",
"recipe-name": "Nome da Receita",
"servings": "Porções",
"sodium-content": "Sodium Content",
"sodium-content": "Sodium",
"step-index": "Passo: {step}",
"sugar-content": "Sugar Content",
"sugar-content": "Sugar",
"tags": "Etiquetas",
"title": "Título",
"total-time": "Tempo Total",

View file

@ -105,6 +105,7 @@
"recent": "Recent"
},
"recipe": {
"assets": "Assets",
"add-key": "Add Key",
"api-extras": "API Extras",
"calories": "Calories",
@ -113,8 +114,9 @@
"delete-confirmation": "Are you sure you want to delete this recipe?",
"delete-recipe": "Delete Recipe",
"description": "Description",
"fat-content": "Fat Content",
"fiber-content": "Fiber Content",
"fat-content": "Fat",
"fiber-content": "Fiber",
"carbohydrate-content": "Carbohydrate",
"grams": "grams",
"image": "Image",
"ingredient": "Ingredient",
@ -132,13 +134,13 @@
"original-url": "Original URL",
"perform-time": "Cook Time",
"prep-time": "Prep Time",
"protein-content": "Protein Content",
"protein-content": "Protein",
"recipe-image": "Recipe Image",
"recipe-name": "Recipe Name",
"servings": "Servings",
"sodium-content": "Sodium Content",
"sodium-content": "Sodium",
"step-index": "Step: {step}",
"sugar-content": "Sugar Content",
"sugar-content": "Sugar",
"tags": "Tags",
"title": "Title",
"total-time": "Total Time",

View file

@ -105,6 +105,7 @@
"recent": "Recent"
},
"recipe": {
"assets": "Assets",
"add-key": "Add Key",
"api-extras": "API Extras",
"calories": "Calories",
@ -113,8 +114,9 @@
"delete-confirmation": "Are you sure you want to delete this recipe?",
"delete-recipe": "Delete Recipe",
"description": "Description",
"fat-content": "Fat Content",
"fiber-content": "Fiber Content",
"fat-content": "Fat",
"fiber-content": "Fiber",
"carbohydrate-content": "Carbohydrate",
"grams": "grams",
"image": "Image",
"ingredient": "Ingredient",
@ -132,13 +134,13 @@
"original-url": "Original URL",
"perform-time": "Cook Time",
"prep-time": "Prep Time",
"protein-content": "Protein Content",
"protein-content": "Protein",
"recipe-image": "Recipe Image",
"recipe-name": "Recipe Name",
"servings": "Servings",
"sodium-content": "Sodium Content",
"sodium-content": "Sodium",
"step-index": "Step: {step}",
"sugar-content": "Sugar Content",
"sugar-content": "Sugar",
"tags": "Tags",
"title": "Title",
"total-time": "Total Time",

View file

@ -105,6 +105,7 @@
"recent": "Recent"
},
"recipe": {
"assets": "Assets",
"add-key": "Add Key",
"api-extras": "API Extras",
"calories": "Calories",
@ -113,8 +114,9 @@
"delete-confirmation": "Are you sure you want to delete this recipe?",
"delete-recipe": "Delete Recipe",
"description": "Description",
"fat-content": "Fat Content",
"fiber-content": "Fiber Content",
"fat-content": "Fat",
"fiber-content": "Fiber",
"carbohydrate-content": "Carbohydrate",
"grams": "grams",
"image": "Image",
"ingredient": "Ingredient",
@ -132,13 +134,13 @@
"original-url": "Original URL",
"perform-time": "Cook Time",
"prep-time": "Prep Time",
"protein-content": "Protein Content",
"protein-content": "Protein",
"recipe-image": "Recipe Image",
"recipe-name": "Recipe Name",
"servings": "Servings",
"sodium-content": "Sodium Content",
"sodium-content": "Sodium",
"step-index": "Step: {step}",
"sugar-content": "Sugar Content",
"sugar-content": "Sugar",
"tags": "Tags",
"title": "Title",
"total-time": "Total Time",

View file

@ -105,6 +105,7 @@
"recent": "Recent"
},
"recipe": {
"assets": "Assets",
"add-key": "Add Key",
"api-extras": "API Extras",
"calories": "Calories",
@ -113,8 +114,9 @@
"delete-confirmation": "Are you sure you want to delete this recipe?",
"delete-recipe": "Delete Recipe",
"description": "Beskrivning",
"fat-content": "Fat Content",
"fiber-content": "Fiber Content",
"fat-content": "Fat",
"fiber-content": "Fiber",
"carbohydrate-content": "Carbohydrate",
"grams": "grams",
"image": "Image",
"ingredient": "Ingrediens",
@ -132,13 +134,13 @@
"original-url": "Originalrecept",
"perform-time": "Cook Time",
"prep-time": "Prep Time",
"protein-content": "Protein Content",
"protein-content": "Protein",
"recipe-image": "Recipe Image",
"recipe-name": "Receptets namn",
"servings": "Portioner",
"sodium-content": "Sodium Content",
"sodium-content": "Sodium",
"step-index": "Steg: {step}",
"sugar-content": "Sugar Content",
"sugar-content": "Sugar",
"tags": "Taggar",
"title": "Title",
"total-time": "Total Time",

View file

@ -105,6 +105,7 @@
"recent": "Recent"
},
"recipe": {
"assets": "Assets",
"add-key": "Add Key",
"api-extras": "API Extras",
"calories": "Calories",
@ -113,8 +114,9 @@
"delete-confirmation": "Are you sure you want to delete this recipe?",
"delete-recipe": "Delete Recipe",
"description": "Description",
"fat-content": "Fat Content",
"fiber-content": "Fiber Content",
"fat-content": "Fat",
"fiber-content": "Fiber",
"carbohydrate-content": "Carbohydrate",
"grams": "grams",
"image": "Image",
"ingredient": "Ingredient",
@ -132,13 +134,13 @@
"original-url": "Original URL",
"perform-time": "Cook Time",
"prep-time": "Prep Time",
"protein-content": "Protein Content",
"protein-content": "Protein",
"recipe-image": "Recipe Image",
"recipe-name": "Recipe Name",
"servings": "Servings",
"sodium-content": "Sodium Content",
"sodium-content": "Sodium",
"step-index": "Step: {step}",
"sugar-content": "Sugar Content",
"sugar-content": "Sugar",
"tags": "Tags",
"title": "Title",
"total-time": "Total Time",

View file

@ -105,6 +105,7 @@
"recent": "Recent"
},
"recipe": {
"assets": "Assets",
"add-key": "Add Key",
"api-extras": "API Extras",
"calories": "Calories",
@ -113,8 +114,9 @@
"delete-confirmation": "Are you sure you want to delete this recipe?",
"delete-recipe": "Delete Recipe",
"description": "Description",
"fat-content": "Fat Content",
"fiber-content": "Fiber Content",
"fat-content": "Fat",
"fiber-content": "Fiber",
"carbohydrate-content": "Carbohydrate",
"grams": "grams",
"image": "Image",
"ingredient": "Ingredient",
@ -132,13 +134,13 @@
"original-url": "Original URL",
"perform-time": "Cook Time",
"prep-time": "Prep Time",
"protein-content": "Protein Content",
"protein-content": "Protein",
"recipe-image": "Recipe Image",
"recipe-name": "Recipe Name",
"servings": "Servings",
"sodium-content": "Sodium Content",
"sodium-content": "Sodium",
"step-index": "Step: {step}",
"sugar-content": "Sugar Content",
"sugar-content": "Sugar",
"tags": "Tags",
"title": "Title",
"total-time": "Total Time",

View file

@ -105,6 +105,7 @@
"recent": "Recent"
},
"recipe": {
"assets": "Assets",
"add-key": "Add Key",
"api-extras": "API Extras",
"calories": "Calories",
@ -113,8 +114,9 @@
"delete-confirmation": "Are you sure you want to delete this recipe?",
"delete-recipe": "Delete Recipe",
"description": "Description",
"fat-content": "Fat Content",
"fiber-content": "Fiber Content",
"fat-content": "Fat",
"fiber-content": "Fiber",
"carbohydrate-content": "Carbohydrate",
"grams": "grams",
"image": "Image",
"ingredient": "Ingredient",
@ -132,13 +134,13 @@
"original-url": "Original URL",
"perform-time": "Cook Time",
"prep-time": "Prep Time",
"protein-content": "Protein Content",
"protein-content": "Protein",
"recipe-image": "Recipe Image",
"recipe-name": "Recipe Name",
"servings": "Servings",
"sodium-content": "Sodium Content",
"sodium-content": "Sodium",
"step-index": "Step: {step}",
"sugar-content": "Sugar Content",
"sugar-content": "Sugar",
"tags": "Tags",
"title": "Title",
"total-time": "Total Time",

View file

@ -105,6 +105,7 @@
"recent": "最近"
},
"recipe": {
"assets": "Assets",
"add-key": "Add Key",
"api-extras": "API Extras",
"calories": "Calories",
@ -113,8 +114,9 @@
"delete-confirmation": "您确定要删除此食谱吗?",
"delete-recipe": "删除食谱",
"description": "描述",
"fat-content": "Fat Content",
"fiber-content": "Fiber Content",
"fat-content": "Fat",
"fiber-content": "Fiber",
"carbohydrate-content": "Carbohydrate",
"grams": "grams",
"image": "Image",
"ingredient": "材料",
@ -132,13 +134,13 @@
"original-url": "原食谱链接",
"perform-time": "烹饪时间",
"prep-time": "准备时间",
"protein-content": "Protein Content",
"protein-content": "Protein",
"recipe-image": "Recipe Image",
"recipe-name": "食谱名称",
"servings": "份量",
"sodium-content": "Sodium Content",
"sodium-content": "Sodium",
"step-index": "步骤:{step}",
"sugar-content": "Sugar Content",
"sugar-content": "Sugar",
"tags": "标签",
"title": "标题",
"total-time": "总时间",

View file

@ -105,6 +105,7 @@
"recent": "Recent"
},
"recipe": {
"assets": "Assets",
"add-key": "Add Key",
"api-extras": "API Extras",
"calories": "Calories",
@ -113,8 +114,9 @@
"delete-confirmation": "您確定要刪除此食譜嗎?",
"delete-recipe": "刪除食譜",
"description": "描述",
"fat-content": "Fat Content",
"fiber-content": "Fiber Content",
"fat-content": "Fat",
"fiber-content": "Fiber",
"carbohydrate-content": "Carbohydrate",
"grams": "grams",
"image": "Image",
"ingredient": "材料",
@ -132,13 +134,13 @@
"original-url": "原食譜鏈接",
"perform-time": "烹飪時間 / 執行時間",
"prep-time": "準備時間",
"protein-content": "Protein Content",
"protein-content": "Protein",
"recipe-image": "Recipe Image",
"recipe-name": "食譜名稱",
"servings": "份量",
"sodium-content": "Sodium Content",
"sodium-content": "Sodium",
"step-index": "步驟:{step}",
"sugar-content": "Sugar Content",
"sugar-content": "Sugar",
"tags": "標籤",
"title": "標題",
"total-time": "總時間",

View file

@ -1,85 +0,0 @@
<template>
<div>
<ImportDialog
:name="selectedName"
:date="selectedDate"
ref="import_dialog"
@import="importBackup"
@delete="deleteBackup"
/>
<v-row>
<v-col
:sm="6"
:md="6"
:lg="4"
:xl="4"
v-for="backup in backups"
:key="backup.name"
>
<v-card @click="openDialog(backup)">
<v-card-text>
<v-row align="center">
<v-col cols="12" sm="2">
<v-icon color="primary"> mdi-backup-restore </v-icon>
</v-col>
<v-col cols="12" sm="10">
<div>
<strong>{{ backup.name }}</strong>
</div>
<div>{{ $d(new Date(backup.date), "medium") }}</div>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
</v-row>
</div>
</template>
<script>
import ImportDialog from "./ImportDialog";
import { api } from "@/api";
export default {
props: {
backups: Array,
},
components: {
ImportDialog,
},
data() {
return {
selectedName: "",
selectedDate: "",
loading: false,
};
},
methods: {
openDialog(backup) {
this.selectedDate = backup.date;
this.selectedName = backup.name;
this.$refs.import_dialog.open();
},
async importBackup(data) {
this.$emit("loading");
let response = await api.backups.import(data.name, data);
let failed = response.data.failed;
let succesful = response.data.successful;
this.$emit("finished", succesful, failed);
},
deleteBackup(data) {
this.$emit("loading");
api.backups.delete(data.name);
this.selectedBackup = null;
this.backupLoading = false;
this.$emit("finished");
},
},
};
</script>
<style>
</style>

View file

@ -71,17 +71,6 @@ export default {
this.availableBackups = response.imports;
this.availableTemplates = response.templates;
},
deleteBackup() {
if (this.$refs.form.validate()) {
this.backupLoading = true;
api.backups.delete(this.selectedBackup);
this.getAvailableBackups();
this.selectedBackup = null;
this.backupLoading = false;
}
},
processFinished(data) {
this.getAvailableBackups();
this.backupLoading = false;

View file

@ -68,7 +68,6 @@ export default {
},
async openDialog() {
this.$refs.deleteDialog.open();
console.log(this.isTags);
if (this.isTags) {
this.deleteList = await api.tags.getEmpty();
} else {

View file

@ -51,6 +51,8 @@
:yields="recipeDetails.recipeYield"
:orgURL="recipeDetails.orgURL"
:nutrition="recipeDetails.nutrition"
:assets="recipeDetails.assets"
:slug="recipeDetails.slug"
/>
<VJsoneditor
@error="logError()"

View file

@ -36,7 +36,6 @@ const actions = {
async requestRecentRecipes() {
const payload = await api.recipes.allSummary(0, 30);
payload.sort((a, b) => (a.dateAdded > b.dateAdded ? -1 : 1));
console.log(payload);
const hash = Object.fromEntries(payload.map(e => [e.id, e]));
this.commit("setRecentRecipes", hash);
},
@ -44,7 +43,6 @@ const actions = {
const all = getters.getAllRecipes;
const payload = await api.recipes.allSummary(all.length, 9999);
const hash = Object.fromEntries([...all, ...payload].map(e => [e.id, e]));
console.log(hash);
this.commit("setAllRecipes", hash);
},

View file

@ -2,13 +2,14 @@ import uvicorn
from fastapi import FastAPI
from mealie.core import root_logger
# import utils.startup as startup
from mealie.core.config import APP_VERSION, settings
from mealie.routes import backup_routes, debug_routes, migration_routes, theme_routes, utility_routes
from mealie.routes import (backup_routes, debug_routes, migration_routes,
theme_routes, utility_routes)
from mealie.routes.groups import groups
from mealie.routes.mealplans import mealplans
from mealie.routes.recipe import all_recipe_routes, category_routes, recipe_crud_routes, tag_routes
from mealie.routes.recipe import (all_recipe_routes, category_routes,
recipe_assets, recipe_crud_routes,
tag_routes)
from mealie.routes.site_settings import all_settings
from mealie.routes.users import users
@ -37,6 +38,7 @@ def api_routers():
app.include_router(category_routes.router)
app.include_router(tag_routes.router)
app.include_router(recipe_crud_routes.router)
app.include_router(recipe_assets.router)
# Meal Routes
app.include_router(mealplans.router)
# Settings Routes
@ -50,11 +52,11 @@ def api_routers():
api_routers()
start_scheduler()
@app.on_event("startup")
def system_startup():
start_scheduler()
logger.info("-----SYSTEM STARTUP----- \n")
logger.info("------APP SETTINGS------")
logger.info(settings.json(indent=4, exclude={"SECRET", "DEFAULT_PASSWORD", "SFTP_PASSWORD", "SFTP_USERNAME"}))

View file

@ -9,7 +9,7 @@ from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
class Meal(SqlAlchemyBase):
__tablename__ = "meal"
id = sa.Column(sa.Integer, primary_key=True)
parent_id = sa.Column(sa.String, sa.ForeignKey("mealplan.uid"))
parent_id = sa.Column(sa.Integer, sa.ForeignKey("mealplan.uid"))
slug = sa.Column(sa.String)
name = sa.Column(sa.String)
date = sa.Column(sa.Date)

View file

@ -5,7 +5,7 @@ from mealie.db.models.model_base import SqlAlchemyBase
class ApiExtras(SqlAlchemyBase):
__tablename__ = "api_extras"
id = sa.Column(sa.Integer, primary_key=True)
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
parent_id = sa.Column(sa.Integer, sa.ForeignKey("recipes.id"))
key_name = sa.Column(sa.String, unique=True)
value = sa.Column(sa.String)

View file

@ -0,0 +1,22 @@
import sqlalchemy as sa
from mealie.db.models.model_base import SqlAlchemyBase
class RecipeAsset(SqlAlchemyBase):
__tablename__ = "recipe_assets"
id = sa.Column(sa.Integer, primary_key=True)
parent_id = sa.Column(sa.Integer, sa.ForeignKey("recipes.id"))
name = sa.Column(sa.String)
icon = sa.Column(sa.String)
file_name = sa.Column(sa.String)
def __init__(
self,
name=None,
icon=None,
file_name=None,
) -> None:
print("Asset Saved", name)
self.name = name
self.file_name = file_name
self.icon = icon

View file

@ -41,7 +41,7 @@ class Category(SqlAlchemyBase):
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String, index=True, nullable=False)
slug = sa.Column(sa.String, index=True, unique=True, nullable=False)
recipes = orm.relationship("RecipeModel", secondary=recipes2categories, back_populates="recipeCategory")
recipes = orm.relationship("RecipeModel", secondary=recipes2categories, back_populates="recipe_category")
@validates("name")
def validate_name(self, key, name):

View file

@ -6,7 +6,7 @@ class RecipeIngredient(SqlAlchemyBase):
__tablename__ = "recipes_ingredients"
id = sa.Column(sa.Integer, primary_key=True)
position = sa.Column(sa.Integer)
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
parent_id = sa.Column(sa.Integer, sa.ForeignKey("recipes.id"))
ingredient = sa.Column(sa.String)
def update(self, ingredient):

View file

@ -5,7 +5,8 @@ from mealie.db.models.model_base import SqlAlchemyBase
class RecipeInstruction(SqlAlchemyBase):
__tablename__ = "recipe_instructions"
id = sa.Column(sa.Integer, primary_key=True)
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
parent_id = sa.Column(sa.Integer, sa.ForeignKey("recipes.id"))
position = sa.Column(sa.Integer)
type = sa.Column(sa.String, default="")
text = sa.Column(sa.String)
title = sa.Column(sa.String)

View file

@ -5,7 +5,7 @@ from mealie.db.models.model_base import SqlAlchemyBase
class Note(SqlAlchemyBase):
__tablename__ = "notes"
id = sa.Column(sa.Integer, primary_key=True)
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
parent_id = sa.Column(sa.Integer, sa.ForeignKey("recipes.id"))
title = sa.Column(sa.String)
text = sa.Column(sa.String)

View file

@ -5,26 +5,29 @@ from mealie.db.models.model_base import SqlAlchemyBase
class Nutrition(SqlAlchemyBase):
__tablename__ = "recipe_nutrition"
id = sa.Column(sa.Integer, primary_key=True)
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
parent_id = sa.Column(sa.Integer, sa.ForeignKey("recipes.id"))
calories = sa.Column(sa.String)
fatContent = sa.Column(sa.String)
fiberContent = sa.Column(sa.String)
proteinContent = sa.Column(sa.String)
sodiumContent = sa.Column(sa.String)
sugarContent = sa.Column(sa.String)
fat_content = sa.Column(sa.String)
fiber_content = sa.Column(sa.String)
protein_content = sa.Column(sa.String)
carbohydrate_content = sa.Column(sa.String)
sodium_content = sa.Column(sa.String)
sugar_content = sa.Column(sa.String)
def __init__(
self,
calories=None,
fatContent=None,
fiberContent=None,
proteinContent=None,
sodiumContent=None,
sugarContent=None,
fat_content=None,
fiber_content=None,
protein_content=None,
sodium_content=None,
sugar_content=None,
carbohydrate_content=None,
) -> None:
self.calories = calories
self.fatContent = fatContent
self.fiberContent = fiberContent
self.proteinContent = proteinContent
self.sodiumContent = sodiumContent
self.sugarContent = sugarContent
self.fat_content = fat_content
self.fiber_content = fiber_content
self.protein_content = protein_content
self.sodium_content = sodium_content
self.sugar_content = sugar_content
self.carbohydrate_content = carbohydrate_content

View file

@ -1,16 +1,17 @@
import datetime
from datetime import date
from typing import List
import sqlalchemy as sa
import sqlalchemy.orm as orm
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
from mealie.db.models.recipe.api_extras import ApiExtras
from mealie.db.models.recipe.assets import RecipeAsset
from mealie.db.models.recipe.category import Category, recipes2categories
from mealie.db.models.recipe.ingredient import RecipeIngredient
from mealie.db.models.recipe.instruction import RecipeInstruction
from mealie.db.models.recipe.note import Note
from mealie.db.models.recipe.nutrition import Nutrition
from mealie.db.models.recipe.settings import RecipeSettings
from mealie.db.models.recipe.tag import Tag, recipes2tags
from mealie.db.models.recipe.tool import Tool
from sqlalchemy.ext.orderinglist import ordering_list
@ -26,23 +27,24 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
name = sa.Column(sa.String, nullable=False)
description = sa.Column(sa.String)
image = sa.Column(sa.String)
totalTime = sa.Column(sa.String)
prepTime = sa.Column(sa.String)
performTime = sa.Column(sa.String)
total_time = sa.Column(sa.String)
prep_time = sa.Column(sa.String)
perform_time = sa.Column(sa.String)
cookTime = sa.Column(sa.String)
recipeYield = sa.Column(sa.String)
recipe_yield = sa.Column(sa.String)
recipeCuisine = sa.Column(sa.String)
tools: List[Tool] = orm.relationship("Tool", cascade="all, delete-orphan")
tools: list[Tool] = orm.relationship("Tool", cascade="all, delete-orphan")
assets: list[RecipeAsset] = orm.relationship("RecipeAsset", cascade="all, delete-orphan")
nutrition: Nutrition = orm.relationship("Nutrition", uselist=False, cascade="all, delete-orphan")
recipeCategory: List = orm.relationship("Category", secondary=recipes2categories, back_populates="recipes")
recipe_category: list = orm.relationship("Category", secondary=recipes2categories, back_populates="recipes")
recipeIngredient: List[RecipeIngredient] = orm.relationship(
recipe_ingredient: list[RecipeIngredient] = orm.relationship(
"RecipeIngredient",
cascade="all, delete-orphan",
order_by="RecipeIngredient.position",
collection_class=ordering_list("position"),
)
recipeInstructions: List[RecipeInstruction] = orm.relationship(
recipe_instructions: list[RecipeInstruction] = orm.relationship(
"RecipeInstruction",
cascade="all, delete-orphan",
order_by="RecipeInstruction.position",
@ -51,12 +53,13 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
# Mealie Specific
slug = sa.Column(sa.String, index=True, unique=True)
tags: List[Tag] = orm.relationship("Tag", secondary=recipes2tags, back_populates="recipes")
dateAdded = sa.Column(sa.Date, default=date.today)
notes: List[Note] = orm.relationship("Note", cascade="all, delete-orphan")
settings = orm.relationship("RecipeSettings", uselist=False, cascade="all, delete-orphan")
tags: list[Tag] = orm.relationship("Tag", secondary=recipes2tags, back_populates="recipes")
date_added = sa.Column(sa.Date, default=date.today)
notes: list[Note] = orm.relationship("Note", cascade="all, delete-orphan")
rating = sa.Column(sa.Integer)
orgURL = sa.Column(sa.String)
extras: List[ApiExtras] = orm.relationship("ApiExtras", cascade="all, delete-orphan")
org_url = sa.Column(sa.String)
extras: list[ApiExtras] = orm.relationship("ApiExtras", cascade="all, delete-orphan")
@validates("name")
def validate_name(self, key, name):
@ -69,23 +72,25 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
name: str = None,
description: str = None,
image: str = None,
recipeYield: str = None,
recipeIngredient: List[str] = None,
recipeInstructions: List[dict] = None,
recipe_yield: str = None,
recipe_ingredient: list[str] = None,
recipe_instructions: list[dict] = None,
recipeCuisine: str = None,
totalTime: str = None,
prepTime: str = None,
total_time: str = None,
prep_time: str = None,
nutrition: dict = None,
tools: list[str] = [],
performTime: str = None,
tools: list[str] = None,
perform_time: str = None,
slug: str = None,
recipeCategory: List[str] = None,
tags: List[str] = None,
dateAdded: datetime.date = None,
notes: List[dict] = None,
recipe_category: list[str] = None,
tags: list[str] = None,
date_added: datetime.date = None,
notes: list[dict] = None,
rating: int = None,
orgURL: str = None,
org_url: str = None,
extras: dict = None,
assets: list = None,
settings: dict = None,
*args,
**kwargs
) -> None:
@ -95,77 +100,33 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
self.recipeCuisine = recipeCuisine
self.nutrition = Nutrition(**nutrition) if self.nutrition else Nutrition()
self.tools = [Tool(tool=x) for x in tools] if tools else []
self.recipeYield = recipeYield
self.recipeIngredient = [RecipeIngredient(ingredient=ingr) for ingr in recipeIngredient]
self.recipeInstructions = [
RecipeInstruction(text=instruc.get("text"), type=instruc.get("@type", None))
for instruc in recipeInstructions
self.recipe_yield = recipe_yield
self.recipe_ingredient = [RecipeIngredient(ingredient=ingr) for ingr in recipe_ingredient]
self.assets = [RecipeAsset(**a) for a in assets]
self.recipe_instructions = [
RecipeInstruction(text=instruc.get("text"), title=instruc.get("title"), type=instruc.get("@type", None))
for instruc in recipe_instructions
]
self.totalTime = totalTime
self.prepTime = prepTime
self.performTime = performTime
self.total_time = total_time
self.prep_time = prep_time
self.perform_time = perform_time
self.recipeCategory = [Category.create_if_not_exist(session=session, name=cat) for cat in recipeCategory]
self.recipe_category = [Category.create_if_not_exist(session=session, name=cat) for cat in recipe_category]
# Mealie Specific
self.settings = RecipeSettings(**settings) if settings else RecipeSettings()
print(self.settings)
self.tags = [Tag.create_if_not_exist(session=session, name=tag) for tag in tags]
self.slug = slug
self.dateAdded = dateAdded
self.date_added = date_added
self.notes = [Note(**note) for note in notes]
self.rating = rating
self.orgURL = orgURL
self.org_url = org_url
self.extras = [ApiExtras(key=key, value=value) for key, value in extras.items()]
def update(
self,
session,
name: str = None,
description: str = None,
image: str = None,
recipeYield: str = None,
recipeIngredient: List[str] = None,
recipeInstructions: List[dict] = None,
recipeCuisine: str = None,
totalTime: str = None,
tools: list[str] = [],
prepTime: str = None,
performTime: str = None,
nutrition: dict = None,
slug: str = None,
recipeCategory: List[str] = None,
tags: List[str] = None,
dateAdded: datetime.date = None,
notes: List[dict] = None,
rating: int = None,
orgURL: str = None,
extras: dict = None,
*args,
**kwargs
):
def update(self, *args, **kwargs):
"""Updated a database entry by removing nested rows and rebuilds the row through the __init__ functions"""
self.__init__(
session=session,
name=name,
description=description,
image=image,
recipeYield=recipeYield,
recipeIngredient=recipeIngredient,
recipeInstructions=recipeInstructions,
totalTime=totalTime,
recipeCuisine=recipeCuisine,
prepTime=prepTime,
performTime=performTime,
nutrition=nutrition,
tools=tools,
slug=slug,
recipeCategory=recipeCategory,
tags=tags,
dateAdded=dateAdded,
notes=notes,
rating=rating,
orgURL=orgURL,
extras=extras,
)
self.__init__(*args, **kwargs)

View file

@ -0,0 +1,18 @@
import sqlalchemy as sa
from mealie.db.models.model_base import SqlAlchemyBase
class RecipeSettings(SqlAlchemyBase):
__tablename__ = "recipe_settings"
id = sa.Column(sa.Integer, primary_key=True)
parent_id = sa.Column(sa.Integer, sa.ForeignKey("recipes.id"))
public = sa.Column(sa.Boolean)
show_nutrition = sa.Column(sa.Boolean)
show_assets = sa.Column(sa.Boolean)
landscape_view = sa.Column(sa.Boolean)
def __init__(self, public=True, show_nutrition=True, show_assets=True, landscape_view=True) -> None:
self.public = public
self.show_nutrition = show_nutrition
self.show_assets = show_assets
self.landscape_view = landscape_view

View file

@ -5,7 +5,7 @@ from mealie.db.models.model_base import SqlAlchemyBase
class Tool(SqlAlchemyBase):
__tablename__ = "tools"
id = sa.Column(sa.Integer, primary_key=True)
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
parent_id = sa.Column(sa.Integer, sa.ForeignKey("recipes.id"))
tool = sa.Column(sa.String)
def __init__(self, tool) -> None:

View file

@ -21,4 +21,4 @@ def get_shopping_list(
mealplan: MealPlanInDB
slugs = [x.slug for x in mealplan.meals]
recipes: list[Recipe] = [db.recipes.get(session, x) for x in slugs]
return [{"name": x.name, "recipeIngredient": x.recipeIngredient} for x in recipes if x]
return [{"name": x.name, "recipe_ingredient": x.recipe_ingredient} for x in recipes if x]

View file

@ -50,11 +50,11 @@ def get_all_recipes(
- description
- image
- recipeYield
- totalTime
- prepTime
- performTime
- total_time
- prep_time
- perform_time
- rating
- orgURL
- org_url
**Note:** You may experience problems with with query parameters. As an alternative
you may also use the post method and provide a body.
@ -78,11 +78,11 @@ def get_all_recipes_post(body: AllRecipeRequest, session: Session = Depends(gene
- description
- image
- recipeYield
- totalTime
- prepTime
- performTime
- total_time
- prep_time
- perform_time
- rating
- orgURL
- org_url
Refer to the body example for data formats.

View file

@ -0,0 +1,50 @@
import shutil
from fastapi import APIRouter, Depends, File, Form
from fastapi.datastructures import UploadFile
from mealie.core.config import app_dirs
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.schema.recipe import Recipe, RecipeAsset
from mealie.schema.snackbar import SnackResponse
from slugify import slugify
from sqlalchemy.orm.session import Session
from starlette.responses import FileResponse
router = APIRouter(prefix="/api/recipes", tags=["Recipe Assets"])
@router.get("/{recipe_slug}/asset")
async def get_recipe_asset(recipe_slug, file_name: str):
""" Returns a recipe asset """
file = app_dirs.RECIPE_DATA_DIR.joinpath(recipe_slug, file_name)
return FileResponse(file)
@router.post("/{recipe_slug}/asset", response_model=RecipeAsset)
def upload_recipe_asset(
recipe_slug: str,
name: str = Form(...),
icon: str = Form(...),
extension: str = Form(...),
file: UploadFile = File(...),
session: Session = Depends(generate_session),
current_user=Depends(get_current_user),
):
""" Upload a file to store as a recipe asset """
file_name = slugify(name) + "." + extension
asset_in = RecipeAsset(name=name, icon=icon, file_name=file_name)
dest = app_dirs.RECIPE_DATA_DIR.joinpath(recipe_slug, file_name)
dest.parent.mkdir(exist_ok=True, parents=True)
with dest.open("wb") as buffer:
shutil.copyfileobj(file.file, buffer)
if dest.is_file():
recipe: Recipe = db.recipes.get(session, recipe_slug)
recipe.assets.append(asset_in)
db.recipes.update(session, recipe_slug, recipe.dict())
return asset_in
else:
return SnackResponse.error("Failure uploading file")

View file

@ -56,6 +56,7 @@ def update_recipe(
""" Updates a recipe by existing slug and data. """
recipe: Recipe = db.recipes.update(session, recipe_slug, data.dict())
print(recipe.assets)
if recipe_slug != recipe.slug:
rename_image(original_slug=recipe_slug, new_slug=recipe.slug)
@ -64,7 +65,7 @@ def update_recipe(
@router.patch("/{recipe_slug}")
def update_recipe(
def patch_recipe(
recipe_slug: str,
data: dict,
session: Session = Depends(generate_session),

View file

@ -1,12 +1,23 @@
import datetime
from typing import Any, List, Optional
from typing import Any, Optional
from fastapi_camelcase import CamelModel
from mealie.db.models.recipe.recipe import RecipeModel
from pydantic import BaseModel, validator
from pydantic import BaseModel, Field, validator
from pydantic.utils import GetterDict
from slugify import slugify
class RecipeSettings(CamelModel):
public: bool = True
show_nutrition: bool = True
show_assets: bool = True
landscape_view: bool = True
class Config:
orm_mode = True
class RecipeNote(BaseModel):
title: str
text: str
@ -15,34 +26,45 @@ class RecipeNote(BaseModel):
orm_mode = True
class RecipeStep(BaseModel):
class RecipeStep(CamelModel):
title: Optional[str] = ""
text: str
class Config:
orm_mode = True
class Nutrition(BaseModel):
calories: Optional[str]
fatContent: Optional[str]
fiberContent: Optional[str]
proteinContent: Optional[str]
sodiumContent: Optional[str]
sugarContent: Optional[str]
class RecipeAsset(CamelModel):
name: str
icon: str
file_name: Optional[str]
class Config:
orm_mode = True
class RecipeSummary(BaseModel):
class Nutrition(CamelModel):
calories: Optional[str]
fat_content: Optional[str]
protein_content: Optional[str]
carbohydrate_content: Optional[str]
fiber_content: Optional[str]
sodium_content: Optional[str]
sugar_content: Optional[str]
class Config:
orm_mode = True
class RecipeSummary(CamelModel):
id: Optional[int]
name: str
slug: Optional[str] = ""
image: Optional[Any]
description: Optional[str]
recipeCategory: Optional[List[str]] = []
tags: Optional[List[str]] = []
recipe_category: Optional[list[str]] = []
tags: Optional[list[str]] = []
rating: Optional[int]
class Config:
@ -52,26 +74,28 @@ class RecipeSummary(BaseModel):
def getter_dict(_cls, name_orm: RecipeModel):
return {
**GetterDict(name_orm),
"recipeCategory": [x.name for x in name_orm.recipeCategory],
"recipe_category": [x.name for x in name_orm.recipe_category],
"tags": [x.name for x in name_orm.tags],
}
class Recipe(RecipeSummary):
recipeYield: Optional[str]
recipeIngredient: Optional[list[str]]
recipeInstructions: Optional[list[RecipeStep]]
recipe_yield: Optional[str]
recipe_ingredient: Optional[list[str]]
recipe_instructions: Optional[list[RecipeStep]]
nutrition: Optional[Nutrition]
tools: Optional[list[str]] = []
totalTime: Optional[str] = None
prepTime: Optional[str] = None
performTime: Optional[str] = None
total_time: Optional[str] = None
prep_time: Optional[str] = None
perform_time: Optional[str] = None
# Mealie Specific
dateAdded: Optional[datetime.date]
notes: Optional[List[RecipeNote]] = []
orgURL: Optional[str]
settings: Optional[RecipeSettings]
assets: Optional[list[RecipeAsset]] = []
date_added: Optional[datetime.date]
notes: Optional[list[RecipeNote]] = []
org_url: Optional[str] = Field(None, alias="orgURL")
extras: Optional[dict] = {}
class Config:
@ -81,8 +105,8 @@ class Recipe(RecipeSummary):
def getter_dict(_cls, name_orm: RecipeModel):
return {
**GetterDict(name_orm),
"recipeIngredient": [x.ingredient for x in name_orm.recipeIngredient],
"recipeCategory": [x.name for x in name_orm.recipeCategory],
"recipe_ingredient": [x.ingredient for x in name_orm.recipe_ingredient],
"recipe_category": [x.name for x in name_orm.recipe_category],
"tags": [x.name for x in name_orm.tags],
"tools": [x.tool for x in name_orm.tools],
"extras": {x.key_name: x.value for x in name_orm.extras},
@ -93,22 +117,22 @@ class Recipe(RecipeSummary):
"name": "Chicken and Rice With Leeks and Salsa Verde",
"description": "This one-skillet dinner gets deep oniony flavor from lots of leeks cooked down to jammy tenderness.",
"image": "chicken-and-rice-with-leeks-and-salsa-verde.jpg",
"recipeYield": "4 Servings",
"recipeIngredient": [
"recipe_yield": "4 Servings",
"recipe_ingredient": [
"1 1/2 lb. skinless, boneless chicken thighs (4-8 depending on size)",
"Kosher salt, freshly ground pepper",
"3 Tbsp. unsalted butter, divided",
],
"recipeInstructions": [
"recipe_instructions": [
{
"text": "Season chicken with salt and pepper.",
},
],
"slug": "chicken-and-rice-with-leeks-and-salsa-verde",
"tags": ["favorite", "yummy!"],
"recipeCategory": ["Dinner", "Pasta"],
"recipe_category": ["Dinner", "Pasta"],
"notes": [{"title": "Watch Out!", "text": "Prep the day before!"}],
"orgURL": "https://www.bonappetit.com/recipe/chicken-and-rice-with-leeks-and-salsa-verde",
"org_url": "https://www.bonappetit.com/recipe/chicken-and-rice-with-leeks-and-salsa-verde",
"rating": 3,
"extras": {"message": "Don't forget to defrost the chicken!"},
}
@ -126,7 +150,7 @@ class Recipe(RecipeSummary):
class AllRecipeRequest(BaseModel):
properties: List[str]
properties: list[str]
limit: Optional[int]
class Config:

View file

@ -83,7 +83,7 @@ class ImportDatabase:
del recipe_dict["categories"]
try:
del recipe_dict["_id"]
del recipe_dict["dateAdded"]
del recipe_dict["date_added"]
except:
pass
# Migration from list to Object Type Data

View file

@ -144,7 +144,7 @@ class MigrationBase(BaseModel):
"""Calls the rewrite_alias function and the Cleaner.clean function on a
dictionary and returns the result unpacked into a Recipe object"""
recipe_dict = self.rewrite_alias(recipe_dict)
recipe_dict = Cleaner.clean(recipe_dict, url=recipe_dict.get("orgURL", None))
recipe_dict = Cleaner.clean(recipe_dict, url=recipe_dict.get("org_url", None))
return Recipe(**recipe_dict)

View file

@ -37,7 +37,7 @@ class NextcloudDir:
class NextcloudMigration(MigrationBase):
key_aliases: Optional[list[MigrationAlias]] = [
MigrationAlias(key="tags", alias="keywords", func=helpers.split_by_comma),
MigrationAlias(key="orgURL", alias="url", func=None),
MigrationAlias(key="org_url", alias="url", func=None),
]

View file

@ -1,9 +1,16 @@
from mealie.core.config import app_dirs, settings
#! I don't like it either!
SQLITE_FILE = app_dirs.SQLITE_DIR.joinpath("test.db")
SQLITE_FILE.unlink(missing_ok=True)
settings.SQLITE_FILE = SQLITE_FILE
import json
import requests
from fastapi.testclient import TestClient
from mealie.app import app
from mealie.core.config import app_dirs, settings
from mealie.db.db_setup import generate_session, sql_global_init
from mealie.db.init_db import init_db
from pytest import fixture
@ -12,10 +19,6 @@ from tests.app_routes import AppRoutes
from tests.test_config import TEST_DATA
from tests.utils.recipe_data import build_recipe_store, get_raw_no_image, get_raw_recipe
SQLITE_FILE = app_dirs.SQLITE_DIR.joinpath("test.db")
SQLITE_FILE.unlink(missing_ok=True)
TestSessionLocal = sql_global_init(SQLITE_FILE, check_thread=False)
init_db(TestSessionLocal())